C言語でインタプリタの動的型付け、抽象オブジェクトを実装する
目次
インタプリタの動的型付け
C言語でインタプリタを実装して、型を動的型付けにする場合は、抽象オブジェクトの実装が必要になる。
今回はこの抽象オブジェクトの実装を簡単なサンプルコードで紹介する。
型定数
まず前提として、今回実装する抽象オブジェクトは整数型(int)と文字列型(str)を型として持っているとする。
その場合、型を表す定数をまず定義する。
:::c
typedef enum {
OBJ_TYPE_INT,
OBJ_TYPE_STR,
} object_type_t;
この定数は typedef
で object_type_t
として型宣言しておく。
オブジェクトを表す構造体
そして実際に抽象オブジェクトを定義するわけだが、これには構造体を使う。構造体には先ほどの型を持たせる。
:::c
typedef struct {
object_type_t type;
} object_t;
object_t
のメンバ変数 type
で、このオブジェクトの型を動的に変更するわけである。
そして、この構造体に実際に型ごとのデータを持たせる。今回持たせるデータは整数と文字列のデータだ。これには場合によって共用体を使う。
:::c
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ビットの整数である。
今回は共用体でメモリの大きさを節約する構造にしているが、これは別に共用体は使わなくてもいい。たとえば↓のように定義することも出来る。
:::c
typedef struct {
object_type_t type;
char *string;
int32_t integer;
} object_t;
共用体を使う場合は設計が少し複雑になるので、実験的な実装では共用体は使わないほうが良いかも知れない。私も今実装しているインタプリタのオブジェクトでは、共用体は使わずに設計している。
先ほどの共用体を使った構造の話に戻すが、まずメンバ変数 type
によってオブジェクトの方を判別する。
そして、その型によって関数などのオブジェクトへのアクセス方法や参照方法を動的に変更するわけである。
こうすると、C 言語で抽象的な型を表現することが出来る。
たとえばオブジェクトの内部構造をファイルオブジェクトに吐き出す関数があったとして、引数には object_t
へのポインタなどを渡すわけだが、関数の内部ではオブジェクトの型、つまり type
を参照し、その値によって吐き出すオブジェクトのデータを動的に変えるわけである。
コンストラクタ/デストラクタ
先ほどのオブジェクトを生成するコンストラクタとデストラクタを定義する。ちなみに C にはそういった機能は言語レベルで備わっているわけではなく、ここでは便宜的にそういうった呼称を使っている。
まずデストラクタだ。
:::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
には動的確保された文字列へのアドレスが入ることになっている。このため、この廃棄でその動的確保したメモリを開放している。
このデストラクタで、型ごとの廃棄を行うわけである。今回は整数と文字列だが、たとえば配列や辞書などを定義する場合は、このデストラクタにもそれぞれの廃棄処理を追記する必要がある。
次にコンストラクタ。
:::c
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
を定義する。これは型からオブジェクトを動的に確保するだけの関数。メモリの確保に失敗した場合は死ぬ。
そして次に型ごとのコンストラクタを作る。
:::c
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
の仕様は、引数のオブジェクトの中身を引数のファイルオブジェクトに書き込むだけである。
この関数は↓のように定義される。
:::c
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関数
今まで定義してきた関数などを使ったサンプルコードは↓のようになる。
:::c
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
でメモリリークなどをチェックしてパスした。
:::c
#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;
}