RustでもGCを使いたい
はじめに
Rustには優れた所有権システムおよびRc<T>
/Arc<T>
があるので通常はGCを必要としないのですが、どうしても循環参照(それもrc::Weak<T>
では解決できないタイプの)が必要となるケースというのが多数存在します。具体的には言語処理系および…… いや言語処理系くらいしかないな。しかしながら、言語処理系を実装するにあたってRustを選択するのは一般的な選択肢なので、これはゆゆしき問題です。
gcクレートによるGC導入
GC機能を提供するクレートはいくつかありますが、何も考えずシングルスレッドでGCしたいだけならgcクレートが手軽です。
Rc<T>
/RefCell<T>
の代わりにgc::Gc<T>
/gc::GcCell<T>
を使用するだけでOK。管理したい型にはgc::Trace
およびgc::Finalize
traitを実装する必要があります。derive
したいときはgc_deriveクレートも追加してください。
// Gc<T>で管理したい値は Trace および Finalize traitを実装している必要がある。
// 単純な場合は自動で導出可能。
#[derive(Debug, Clone, PartialEq, Eq, Trace, Finalize)]
pub enum Value {
Int(i32),
Array(Gc<GcCell<Vec<Value>>>),
}
// 自分自身を要素とする配列
fn new_cycled_value() -> Value{
let array = Value::Array(Gc::new(GcCell::new(Vec::new())));
let Value::Array(inner) = &array else { panic!(); };
inner.borrow_mut().push(array.clone());
array
}
Trace
trait
Trace
traitはメモリ管理のために必要で、実装では自分が持っているフィールドのうちGc<T>
を含むものを全部マークする必要があります。これはgc::custom_trace!
マクロを使って実装することができます。第一引数には自身を参照するためのシンボル(this
)、第二引数にはトレース処理の実装(必要なフィールドに対してmark()
を呼ぶ)を指定します。
unsafe impl Trace for Value {
custom_trace!(this, {
match this {
Value::Int(_) => {}
Value::Array(x) => { mark(x) }
}
});
}
Finalize
trait
Finalize
traitはGCにおけるDrop
相当で、メモリが回収されたときに実行したい処理を記述します。普通は必要ないので #[derive(Finalize)]
で生成される空実装を使えばよし。
GCのタイミング
ゴミはGc::new()
のタイミングで必要に応じて回収されますが、gc::force_collect();
を呼ぶことで明示的に回収することも可能です。
GC機能を提供するその他のクレート
GC管理されたポインタの表現が一種類しかないもの(Rc<T>
代替系、簡単)と二種類あるもの(Rootとそれ以外、パフォーマンスが良さそうだがAPIは面倒)に別れます。arenaとついてるのは後者。
boa_gc
Rustで書かれたJS処理系Boaに使用されているGCライブラリ。Boaは元々gcクレートを使用しており、それをベースにした独自実装に切り替えたという経緯があるため、APIはかなり似ています。独自機能としてはweak referenceおよびWeakMap
があります。
gc-arena
Lua VMのRust実装であるpiccoroや、RustによるFlash PlayerエミュレーターRuffleで使用されているGCライブラリ。
jsonnet-gcmodule
JSONテンプレート言語Jsonnetで使われているGCライブラリ。std::rc::Rc
感覚で使えて、明示的に回収関数を呼ぶことで相互参照された値も回収される。
dumpster
こちらもRc感覚で使えるGCライブラリ。マルチスレッドにも対応。作者による解説
moving_gc_arena
gc-arenaと同じくGCポインタがusize
一個で済むタイプ。APIはこちらのほうが楽そう。
safe-gc
unsafe
一切なしでGCライブラリを作ってやろうという野心的なこころみ。作者による解説
その他
お前らはGCの実装が好きすぎる。
Discussion