siren では、mruby を組み込んでコマンドドリブンなインタプリタを実装していたので、クラスをまともに使っていませんでしたが、コマンドが多くなるにつれ管理が煩雑になった事や、Ruby のクラスとして強力な機能を実装する目的もあり、改めて mruby のクラスまわりの実装を調べてみました。
C コード上で Ruby クラスを作成
クラスの新規作成
mrb_define_class() を使います。引数は mrb_state*, クラス名, 親クラス。
RClass* prclass = mrb_define_class(mrb, "Myclass", mrb->object_class);
RClass*が返ってきます。C コード中では、このポインタでクラスを指定します。
クラスにメソッドを突っ込む
新規作成したクラス Myclass の中身は空っぽなので、メソッドを突っ込んでみます。
mrb_define_method(mrb, prclass, "myfunc", myfunc, ARGS_NONE());
引数は mrb_state*, ターゲットとなるクラスの RClass*, メソッド名, 実体のC関数, 引数定義です。 RClass* を mrb->kernel_module にすると、Kernel 名前空間に入ってグローバル関数のように使えます。
引数定義
ARGS_NONE() ... 引数なし
MRB_ARGS_REQ(2) | MRB_ARGS_OPT(1) ... 必須引数が2個、省略可能引数が1個
実体の関数
インターフェイスは、戻り値と引数の型に注意するだけでOKです。
mrb_value myfunc(mrb_state* mrb, mrb_value self)
{
mrb_int a;
mrb_int b;
int argc = mrb_get_args(mrb, "ii", &a, &b);
return mrb_fixnum_value(a + b);
}
引数は mrb_get_args() で取得します。
定義済みのクラスを文字列から取得する
名前が分かっているなら RClass* を保持しておく必要はありません。
RClass* prclass2 = mrb_class_get(mrb, "Myclass");
クラスのインスタンス変数を設定・取得する
mrb_obj_iv_set() と mrb_obj_iv_get() を使用します。
RClass* my_class = mrb_define_class(mrb, "Test", mrb->object_class);
mrb_value obj = mrb_class_new_instance(mrb, 0, NULL, my_class);
RObject* pobj = mrb_obj_ptr(obj);
mrb_sym sym = mrb_intern(mrb, "asdf", strlen("asdf"));
mrb_obj_iv_set(mrb, pobj, sym, mrb_fixnum_value(1244));
return mrb_obj_iv_get(mrb, pobj, sym);
クラスからインスタンスを作成
定義したクラスからインスタンスを作成するには mrb_class_new_instance() を使うようです。 引数は mrb_state*, コンストラクタの引数の数、引数、RClass* です。
mrb_value obj = mrb_class_new_instance(mrb, 0, NULL, prclass2);
引数がない場合、0, NULL で良いみたいです。引数は
mrb_value args[3];
args[0] = mrb_fixnum_value(123);
args[1] = mrb_float_value(mrb, 10.4);
args[2] = mrb_nil_value();
のようにこしらえます。
メソッドの削除
コーディングして試してみていませんが、mruby.h に
void mrb_undef_class_method(mrb_state*, struct RClass*, const char*);
がありました。
メソッドを実行する
mrb_funcall(), mrb_funcall_argv(), mrb_funcall_with_block() の3種類が mruby.h に定義されています。
mrb_funcall(mrb, obj, "myfunc", 0);
Ruby スクリプトとして書いたクラスを C プログラムに組み込む
C コードで動的に Ruby のクラスを作れるのは便利なんですが、動的に生成する必要もないものは、普通の Ruby スクリプトとして書いたりデバッグしたりする方が効率的です。
mruby をビルドすると bin ディレクトリに生成される mrbc を使うと、Ruby のスクリプトファイルをバイトコードに変換し、C の配列として扱える C ソースコードファイルに変換してくれます。
Ruby スクリプトから C ソースコードへ変換
次のような内容の vector.rb を準備します。
class Vector
attr_accessor :x, :y, :z
end
mrbc を使ってコンパイルします。
mrbc -BVector -ovector.c vector.rb
# -B<バイトコード配列名> -o<出力ファイル名> <入力ファイル名>
次のような内容の vector.c が生成されます。
#include <stdint.h>
const uint8_t Vector[] = {
0x52,0x49,0x54,0x45,0x30,0x30,0x30,0x32,0xe8,0xe5,0x00,0x00,0x00,0xa7,0x4d,0x41,
0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0x89,0x30,0x30,
0x30,0x30,0x00,0x00,0x00,0x33,0x00,0x01,0x00,0x03,0x00,0x01,0x00,0x00,0x00,0x05,
0x00,0x80,0x00,0x05,0x01,0x00,0x00,0x05,0x00,0x80,0x00,0x43,0x00,0x80,0x00,0x45,
0x00,0x00,0x00,0x4a,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x06,0x56,0x65,
0x63,0x74,0x6f,0x72,0x00,0x00,0x00,0x00,0x4a,0x00,0x01,0x00,0x06,0x00,0x00,0x00,
0x00,0x00,0x06,0x00,0x80,0x00,0x06,0x01,0x00,0x00,0x84,0x01,0x80,0x01,0x04,0x02,
0x00,0x01,0x84,0x00,0x80,0x01,0xa0,0x00,0x80,0x00,0x29,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x04,0x00,0x0d,0x61,0x74,0x74,0x72,0x5f,0x61,0x63,0x63,0x65,0x73,0x73,
0x6f,0x72,0x00,0x00,0x01,0x78,0x00,0x00,0x01,0x79,0x00,0x00,0x01,0x7a,0x00,0x45,
0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};
アプリケーションをビルドするたびに変換するのもなんですので、ここまでの手順を Makefile なんかに書いておけば良いでしょう。
バイトコード配列を使う
アプリケーションの Makefile やプロジェクト設定で vector.c をビルド対象にして、このバイトコード配列を使っていきます。vector.c は、スクリプト側に変更をかけるたびに上書きで済ませたいので、使うコードは別のファイルがいいでしょう。
別ファイルにて
extern const uint8_t Vector[];
しておくと安心です。
実際の使い方はかなり簡単で、
mrb_load_irep(mrb, Vector);
だけでバイトコードが mrb_state* 空間にロードされます。 あとは、前述のとおり
RClass* prclass = mrb_class_get(mrb, "Vector");
という具合に、名前文字列から RClass* を取得できます。
オマケ
クラスとは関係ありませんが、随所に仕込んでおきたい例外とエラーの表現。
未実装例外
return mrb_exc_new(mrb, E_NOTIMP_ERROR, NULL, 0);
引数エラー例外(メッセージ付)
static const char m[] = "No such specified object.";
return mrb_exc_new(mrb, E_ARGUMENT_ERROR, m, sizeof(m) - 1);
今のところ分かってないこと
new したインスタンスを mrb_state 空間から動的に取得してくる方法が分かりません。インスタンスを取得するにしても、C コード側からは結局は RObject のポインタとしてしか表現されない(名前文字列みたいな「ラベル」もないですし)ので、後から使いたいオブジェクトはどこかにポインタを覚えさせておく必要があるのかもと予想してます。
あんまりよくコードを読んでませんが mrb_state の宣言に RObject* メンバがぶらさがっているんですが、このあたりが実体なのかも。