ある日,何気なくTwitterを眺めていると,
「GCのことが知りたい」
突然そう思った.
GCで,最も基本的な方式の一つに,参照カウントがある. 元々名前と原理を大まかには知っていたので,とりあえず実装してみることにした.
こうして出来たのがrefcount.c である.
簡単な実装ではあるけれど,これはこうやって使う.
#include "./refcount.h"
int main(int argc, const char *argv[])
{
Person* p1 = new_person("musou1500", 24);
Ref ref1 = new_ref(p1, person_destruct);
Person* p2 = new_person("musou1501", 24);
Ref ref2 = new_ref(p2, person_destruct);
printf("ref2 = ref1;\n");
ref_assign(&ref2, &ref1);
// ここで `p2` が開放される
printf("ref1 = NULL;\n");
ref_assign(&ref1, NULL);
printf("ref2 = NULL;\n");
ref_assign(&ref1, NULL);
// ここで `p1` が開放される
return 0;
}
感動した.簡単ながら,ちゃんと動いている. ちゃんと動いて入るのだが,これを実際に使うことを考えるとなかなか面倒くさいと思う.
GCしたければ Ref
オブジェクトを通す必要があるし,
忘れずに逐一 ref_assign
を呼び出す必要がある.
人間にそんなことができるのか.
そうなると,独自の言語を実装してみよう,となるのは自然な発想だと言える. 「GC付きのC言語」のような,簡単な言語を実装し,遊んでみようというわけだ.
ヒープにメモリ領域を確保し,Ref
のようなオブジェクトに引き渡すような構文があれば,
言語の使用者はその内部実装を意識することなく,GCを利用できるはずだ.
そこで,以下のような alloc
構文を考えた.
// ヒープにメモリ領域を確保し,初期化する
var p1 = alloc Person {
.name = "musou1500",
.age = 15
};
var p2 = alloc Person {
.name = "musou1501",
.age = 15
};
// p2の参照先 "musou1501" が開放される
p2 = p1;
// "musou1500" が開放される
p1 = null;
p2 = null;
// 配列をヒープに確保する
var str = alloc char[7];
細かいことは後で考えるとして,とりあえずこれで良さそうだ.
そういうわけで,この言語のコンパイラを開発し始めた. 僕はこの言語を "Reference Count" にちなんで,「RC」と呼ぶことにした.
レポジトリはここにある. musou1500/learn_gc
今はひたすらパーサとトークナイザを書いている段階で, 型チェックとコード生成器はまだ着手できてない.
HaskellやElmなんかでも導入されている,パイプライン演算子があると良さそうだ. 例えば,以下のように使える.
return readline()
|> s_ucfirst(_),
|> s_concat("Hello", _)
|> println(_)
s_ucfirst
と s_concat
は以下のように振る舞う.
s_ucfirst
渡された文字列の最初の文字を大文字にしたものを返す.s_concat
2つの文字列を連結して返す
// "Apple"
s_ucfirst("apple");
// "Hello, World"
s_concat("Hello, ", "World");
これらの関数は,新しくヒープにメモリ領域を確保して返す. しかし,RCにはGCが付いているので,引数に渡した文字列をどうしようとか,新しく確保されたメモリ領域はいつ開放すれば良いだろうとか,そういうことを考える必要はない.
例えば,RustやC++ではRAIIっぽいメモリ管理をサポートしていて,GC無しでのメモリ管理が可能になっている.
今後,そういった機能を取り入れられるようにしたい. ただ,現状知識もなく,そういうことを想定した言語設計をしているわけでもないのでできるかどうかわからない.