スポンサーリンク

コードが書ける!数式が書ける!AAが書ける!スタンプが貼れる!

無料の匿名掲示板型SNS「このはちゃんねる

新規会員募集中!

C言語でインタプリタの動的型付け、抽象オブジェクトを実装する

326, 2020-05-21

目次

インタプリタの動的型付け

C言語でインタプリタを実装して、型を動的型付けにする場合は、抽象オブジェクトの実装が必要になる。
今回はこの抽象オブジェクトの実装を簡単なサンプルコードで紹介する。

型定数

まず前提として、今回実装する抽象オブジェクトは整数型(int)と文字列型(str)を型として持っているとする。
その場合、型を表す定数をまず定義する。

typedef enum {
    OBJ_TYPE_INT,
    OBJ_TYPE_STR,
} object_type_t;

この定数は typedefobject_type_t として型宣言しておく。

オブジェクトを表す構造体

そして実際に抽象オブジェクトを定義するわけだが、これには構造体を使う。構造体には先ほどの型を持たせる。

typedef struct {
    object_type_t type;
} object_t;

object_t のメンバ変数 type で、このオブジェクトの型を動的に変更するわけである。
そして、この構造体に実際に型ごとのデータを持たせる。今回持たせるデータは整数と文字列のデータだ。これには場合によって共用体を使う。

typedef struct {
    object_type_t type;
    union {
        // type == OBJ_TYPE_STR
        char *string;

        // type == OBJ_TYPE_INT
        int32_t integer;
    } value;
} object_t;

共用体変数 value のメンバ変数 string は文字列型のデータである文字列への 32 ビットまたは 64 ビットのポインタ、そしてメンバ変数 integer は整数型のデータである 32ビットの整数である。
今回は共用体でメモリの大きさを節約する構造にしているが、これは別に共用体は使わなくてもいい。たとえば↓のように定義することも出来る。

typedef struct {
    object_type_t type;
    char *string;
    int32_t integer;
} object_t;

共用体を使う場合は設計が少し複雑になるので、実験的な実装では共用体は使わないほうが良いかも知れない。私も今実装しているインタプリタのオブジェクトでは、共用体は使わずに設計している。

先ほどの共用体を使った構造の話に戻すが、まずメンバ変数 type によってオブジェクトの方を判別する。
そして、その型によって関数などのオブジェクトへのアクセス方法や参照方法を動的に変更するわけである。
こうすると、C 言語で抽象的な型を表現することが出来る。

たとえばオブジェクトの内部構造をファイルオブジェクトに吐き出す関数があったとして、引数には object_t へのポインタなどを渡すわけだが、関数の内部ではオブジェクトの型、つまり type を参照し、その値によって吐き出すオブジェクトのデータを動的に変えるわけである。

コンストラクタ/デストラクタ

先ほどのオブジェクトを生成するコンストラクタとデストラクタを定義する。ちなみに C にはそういった機能は言語レベルで備わっているわけではなく、ここでは便宜的にそういうった呼称を使っている。

まずデストラクタだ。

void
obj_del(object_t *self) {
    if (!self) {
        return;
    }

    switch (self->type) {
    case OBJ_TYPE_STR:
        free(self->value.string);
        break;
    }

    free(self);
}

デストラクタの役目はオブジェクトの廃棄である。つまり、メモリからオブジェクトを開放する。このオブジェクトはメモリの動的確保で常に確保される前提になっている。で、このデストラクタではその動的に確保したオブジェクトのメモリを開放するわけである。
switch 文で type を判別し、型ごとの廃棄を実行している。文字列型(OBJ_TYPE_STR)の場合、value.string には動的確保された文字列へのアドレスが入ることになっている。このため、この廃棄でその動的確保したメモリを開放している。
このデストラクタで、型ごとの廃棄を行うわけである。今回は整数と文字列だが、たとえば配列や辞書などを定義する場合は、このデストラクタにもそれぞれの廃棄処理を追記する必要がある。

次にコンストラクタ。

object_t *
obj_new(object_type_t type) {
    object_t *self = calloc(1, sizeof(*self));
    if (!self) {
        perror("calloc");
        exit(1);
    }

    self->type = type;

    return self;
}

まず最もシンプルなコンストラクタ obj_new を定義する。これは型からオブジェクトを動的に確保するだけの関数。メモリの確保に失敗した場合は死ぬ。
そして次に型ごとのコンストラクタを作る。

object_t *
obj_new_integer(int32_t integer) {
    object_t *self = obj_new(OBJ_TYPE_INT);

    self->value.integer = integer;

    return self;
}

object_t *
obj_new_string(const char *string) {
    object_t *self = obj_new(OBJ_TYPE_STR);

    self->value.string = strdup(string);

    return self;
}

obj_new_integer は整数型のオブジェクトを作りたい時に使うコンストラクタで、obj_new_string は文字列型のオブジェクトを作りたい時に使うコンストラクタである。
内部では型を指定してオブジェクトを作り、そのオブジェクトに値を設定している。

これでオブジェクトを生成し、破棄することが出来るようになった。

オブジェクトの中身を吐き出す

今回作った抽象オブジェクトを実際に使ってみる。
ここではオブジェクトの中身を吐き出す関数 obj_dump を作ることにする。
obj_dump の仕様は、引数のオブジェクトの中身を引数のファイルオブジェクトに書き込むだけである。
この関数は↓のように定義される。

void
obj_dump(object_t *self, FILE *fout) {
    switch (self->type) {
    case OBJ_TYPE_INT:
        fprintf(fout, "<int: %d>\n", self->value.integer);
        break;
    case OBJ_TYPE_STR:
        fprintf(fout, "<str: \"%s\">\n", self->value.string);
        break;
    }
}

今回作ったオブジェクトを扱う処理全般に言える話だが、オブジェクトを扱う際、最初にオブジェクトの型を switch 文などで判別する。そして、その型に応じて処理を動的に変更する。
obj_dump では型に応じて参照するオブジェクトのデータとファイルオブジェクトへの書き込み内容を変えている。

main関数

今まで定義してきた関数などを使ったサンプルコードは↓のようになる。

int
main(void) {
    object_t *intobj = obj_new_integer(10);
    object_t *strobj = obj_new_string("hi");

    obj_dump(intobj, stdout);
    obj_dump(strobj, stdout);

    obj_del(intobj);
    obj_del(strobj);
    return 0;
}

やってることはコンストラクタでオブジェクトを生成し、そのオブジェクトの中身を stdout に吐き出し、最後にオブジェクトをデストラクタで廃棄しているだけのコードである。
このコードの実行結果は↓のようになる。

<int: 10>
<str: "hi">

作ってみるとわかるが、やってることはそんなに複雑じゃない。要は、switch 文による型判別である。これを書けばあとはオブジェクトごとの処理を書けばいいので、難しい処理ではない。

コード全文

最後にコード全文を載せる。このコードは valgrind でメモリリークなどをチェックしてパスした。

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

typedef enum {
    OBJ_TYPE_INT,
    OBJ_TYPE_STR,
} object_type_t;

typedef struct {
    object_type_t type;
    union {
        // type == OBJ_TYPE_STR
        char *string;

        // type == OBJ_TYPE_INT
        int32_t integer;
    } value;
} object_t;

void
obj_del(object_t *self) {
    if (!self) {
        return;
    }

    switch (self->type) {
    case OBJ_TYPE_STR:
        free(self->value.string);
        break;
    }

    free(self);
}

object_t *
obj_new(object_type_t type) {
    object_t *self = calloc(1, sizeof(*self));
    if (!self) {
        perror("calloc");
        exit(1);
    }

    self->type = type;

    return self;
}

object_t *
obj_new_integer(int32_t integer) {
    object_t *self = obj_new(OBJ_TYPE_INT);

    self->value.integer = integer;

    return self;
}

object_t *
obj_new_string(const char *string) {
    object_t *self = obj_new(OBJ_TYPE_STR);

    self->value.string = strdup(string);

    return self;
}

void
obj_dump(object_t *self, FILE *fout) {
    switch (self->type) {
    case OBJ_TYPE_INT:
        fprintf(fout, "<int: %d>\n", self->value.integer);
        break;
    case OBJ_TYPE_STR:
        fprintf(fout, "<str: \"%s\">\n", self->value.string);
        break;
    }
}

int
main(void) {
    object_t *intobj = obj_new_integer(10);
    object_t *strobj = obj_new_string("hi");

    obj_dump(intobj, stdout);
    obj_dump(strobj, stdout);

    obj_del(intobj);
    obj_del(strobj);
    return 0;
}

投稿者名です。64字以内で入力してください。

必要な場合はEメールアドレスを入力してください(全体に公開されます)。

投稿する内容です。

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク