ツナでもわかるとかいいつつ、今回はツナではわかりそうもありません...(´ω`;)
ここらでmrbgemを作りたかったので、作りつつ調べた事をメモしてます。
ちなみに作ってみたのはmruby-mqttというmrubyでMQTTクライアントを実現する為のライブラリ。といいつつ実はただのpahoのラッパーです。
注意:
勉強しつつ更新していっているので、内容には間違いが有る可能性が高いです。
##ディレクトリ構造
ディレクトリは以下のような構造になるようです。
mruby-クラス名/ mrbgem.rake
src / クラス名.c
[その他のCソースファイル]
mrblib / クラス名.rb
[その他のmrubyソースファイル]
test / [テストファイル]
ファイル名は必ずしもクラス名でなくてもいいと思いますが、スタイル上の問題でそのようにする事が多いようです。
このディレクトリを適当な位置において、build_config.rb
内で
conf.gem 'ディレクトリ名'
のように指定しておけば一緒にビルドされます。mrubyのディレクトリ外でもいいみたい。
####Example
conf.gem '../mruby-mqtt'
mrbgem.rake
下記はmruby-timeのmrbgem.rake
。このような内容でmrbgemそれぞれに必ず必要
MRuby::Gem::Specification.new('mruby-time') do |spec|
spec.license = 'MIT'
spec.author = 'mruby developers'
spec.summary = 'standard Time class'
end
-
MRuby::Gem::Specification.new
の引数にmrbgem名を渡す - spec.license: ライセンス
- spec.author: 開発者
- spec.summary: このmrbgemの説明
Cソースファイルの編集
最低限、初期化関数と終了時関数が必要
初期化時の関数
下記のようにinit関数を定義しておくと呼ばれるっぽい
この中で新しいクラスをmrb_define_class
で定義するか、mrb->string_class
のように既存のクラスを引っ張ってきてポインタ変数に入れておく。
そして、そのクラス構造体に対してmrb_define_method
でメソッドを定義していく。
void
mrb_mruby_クラス名_gem_init(mrb_state* mrb) {
/*...*/
}
mrubyクラス構造体の宣言
新たなクラスを定義する場合
struct RClass *klass;
すでに定義されているクラスを拡張する場合
struct RClass *string_klass = mrb->string_class;
新しいクラスの定義
mrb_define_class(mrb, "クラス名", 継承元クラス);
- mrb: mrb_stateへのポインタ
- クラス名: Ruby側でのクラス名を表す文字列
- 継承元クラス: 定義するクラスの継承元クラス(スーパークラス)
Example
struct RClass *c;
c = mrb_define_class(mrb, "Fiber", mrb->object_class);
MRB_SET_INSTANCE_TT(c, MRB_TT_FIBER);
MRB_TT_[某]の定義はsrc/object.c
でされていました。以下のようなオブジェクトのタイプが有るようです。
static const struct types {
unsigned char type;
const char *name;
} builtin_types[] = {
{MRB_TT_FALSE, "false"},
{MRB_TT_TRUE, "true"},
{MRB_TT_FIXNUM, "Fixnum"},
{MRB_TT_SYMBOL, "Symbol"}, /* :symbol */
{MRB_TT_MODULE, "Module"},
{MRB_TT_OBJECT, "Object"},
{MRB_TT_CLASS, "Class"},
{MRB_TT_ICLASS, "iClass"}, /* internal use: mixed-in module holder */
{MRB_TT_SCLASS, "SClass"},
{MRB_TT_PROC, "Proc"},
{MRB_TT_FLOAT, "Float"},
{MRB_TT_ARRAY, "Array"},
{MRB_TT_HASH, "Hash"},
{MRB_TT_STRING, "String"},
{MRB_TT_RANGE, "Range"},
{MRB_TT_FILE, "File"},
{MRB_TT_DATA, "Data"}, /* internal use: wrapped C pointers */
{-1, 0}
};
インスタンスメソッドの定義
mrb_define_method(mrb, klass, "インスタンスメソッド名", 関連付けるC関数, 引数タイプ)
- mrb: mrb_state型のポインタ
- klass: インスタンスメソッドを定義するクラスを表す構造体(RClass)へのポインタ
- "インスタンスメソッド名": 定義するRuby側でのメソッド名(文字列)
- 関連付けるC関数: 関連付けるC側での関数
- 引数タイプ: 引数の渡し方を表すマクロ
- MRB_ARGS_REQ(n): n個の引数
- MRB_ARGS_OPT(n): オプショナルなn個の引数
- MRB_ARGS_ARG(n1,n2): n1個の引数と、オプショナルなn2個の引数
- MRB_ARGS_REST(): ?
- MRB_ARGS_POST(n): ?
- MRB_ARGS_KEY(n1,n2): 名前付き引数。n1個のキーワードと、値が収まる辞書n2
- MRB_ARGS_BLOCK(): ブロック引数
- MRB_ARGS_ANY(): 任意の数の引数。配列で受け取る
- MRB_ARGS_NONE(): 引数なし
Example
mrb_define_method(mrb, c, "initialize", fiber_init, MRB_ARGS_NONE());
mrb_define_method(mrb, c, "resume", fiber_resume, MRB_ARGS_ANY());
クラスメソッドの定義
mrb_define_class_method(mrb, klass, "クラスメソッド名", 関連付けるC関数, 引数タイプ);
- mrb: mrb_state型のポインタ
- klass: クラスメソッドを定義するクラスを表す構造体(RClass)へのポインタ
- ruby_method_name: 定義するRuby側でのメソッド名(文字列)
- 関連付けるC関数: 関連付けるC側での関数
- 引数タイプ: 引数の渡し方を表すマクロ
- MRB_ARGS_REQ(n): n個の引数
- MRB_ARGS_OPT(n): オプショナルなn個の引数
- MRB_ARGS_ARG(n1,n2): n1個の引数と、オプショナルなn2個の引数
- MRB_ARGS_REST(): ?
- MRB_ARGS_POST(n): ?
- MRB_ARGS_KEY(n1,n2): 名前付き引数。n1個のキーワードと、値が収まる自書n2
- MRB_ARGS_BLOCK(): ブロック引数
- MRB_ARGS_ANY(): 任意の数の引数。配列で受け取る
- MRB_ARGS_NONE(): 引数なし
Example
mrb_define_class_method(mrb, gc, "start", gc_start, MRB_ARGS_NONE());
mrubyに含まれているmrbgemsでの例
Stringクラスを拡張するmruby-string-utf8
の初期化関数を抜粋します。
void
mrb_mruby_string_utf8_gem_init(mrb_state* mrb)
{
struct RClass * s = mrb->string_class;
mrb_define_method(mrb, s, "size", mrb_str_size, MRB_ARGS_NONE());
mrb_define_method(mrb, s, "length", mrb_str_size, MRB_ARGS_NONE());
mrb_define_method(mrb, s, "index", mrb_str_index_m, MRB_ARGS_ANY());
/* ... */
}
終了時の関数
void
mrb_mruby_クラス名_gem_final(mrb_state* mrb)
{
}
Example
void
mrb_mruby_string_utf8_gem_final(mrb_state* mrb)
{
}
これはどういうタイミングで呼ばれるのか、まだよくわかっていません...
##コード中で使用できるAPI関数
###引数の取得
mrb_int
mrb_get_args(mrb_state *mrb, const char *format, ...)
mruby側から渡される引数はmrb_get_args
を使って受け取ります。
引数のフォーマットには以下が有るようです。
format specifiers:
string mruby type C type note
----------------------------------------------------------------------
o: Object [mrb_value]
C: class/module [mrb_value]
S: String [mrb_value]
A: Array [mrb_value]
H: Hash [mrb_value]
s: String [char*,mrb_int] Receive two arguments.
z: String [char*] NUL terminated string.
a: Array [mrb_value*,mrb_int] Receive two arguments.
f: Float [mrb_float]
i: Integer [mrb_int]
b: Boolean [mrb_bool]
n: Symbol [mrb_sym]
d: Data [void*,mrb_data_type const] 2nd argument will
be used to check
data type so
it won't be
modified
&: Block [mrb_value]
*: rest argument [mrb_value*,mrb_int] Receive the rest of
the arguments as
an array.
|: optional Next argument of '|' and
later are optional.
//
?: optional given [mrb_bool] true if preceding
argument (optional) is
given.
Example
例として、まずはmruby-timeの関数を幾つか見てみます。
static mrb_value
mrb_time_local(mrb_state *mrb, mrb_value self)
{
mrb_int ayear = 0, amonth = 1, aday = 1,
ahour = 0, amin = 0, asec = 0, ausec = 0;
mrb_get_args(mrb, "i|iiiiii",
&ayear, &amonth, &aday, &ahour, &amin, &asec, &ausec);
return mrb_time_wrap(mrb, mrb_class_ptr(self),
time_mktime(mrb, ayear, amonth, aday,
ahour, amin, asec, ausec,
MRB_TIMEZONE_LOCAL));
}
ここでは必須の引数は先頭のyearだけで、あとはオプショナルとなるため"i|iiiiii"
というフォーマットを渡しています。
年、月、日、時、分、秒、マイクロ秒がそれぞれayear,amonth...
へ代入されます。
同じくmruby-timeから
static mrb_value
mrb_time_eq(mrb_state *mrb, mrb_value self)
{
mrb_value other;
struct mrb_time *tm1, *tm2;
mrb_bool eq_p;
mrb_get_args(mrb, "o", &other);
tm1 = DATA_CHECK_GET_PTR(mrb, self, &mrb_time_type, struct mrb_time);
tm2 = DATA_CHECK_GET_PTR(mrb, other, &mrb_time_type, struct mrb_time);
eq_p = tm1 && tm2 && tm1->sec == tm2->sec && tm1->usec == tm2->usec;
return mrb_bool_value(eq_p);
}
ここではTimeオブジェクト同士の比較をするのですが、比較相手のTimeオブジェクトを受け取るのに、「オブジェクト」を表す"o"
をフォーマットとして渡しています。引数として渡されるオブジェクトへのポインタがotherへ代入されます。
最後に、mruby-fiberにおける例です。
static mrb_value
fiber_yield(mrb_state *mrb, mrb_value self)
{
mrb_value *a; // 引数の配列へのポインタが入る変数
mrb_int len; // 引数の数が入る変数
mrb_get_args(mrb, "*", &a, &len);
return mrb_fiber_yield(mrb, len, a);
}
フォーマットに"*"
を指定して、変数a
に引数の配列を、変数len
に引数の数を受け取っています。
インスタンス変数に値を代入する
void
mrb_iv_set(mrb_state *mrb, mrb_value obj, mrb_sym sym, mrb_value v)
- mrb: mrb_stateへのポインタ
- mrb_value: インスタンス変数を設定するオブジェクトへの参照
- sym: インスタンス変数名を表すシンボル
- v: 設定する値
Example
例としてHashクラスにある関数を見てみます。
static mrb_value
mrb_hash_set_default(mrb_state *mrb, mrb_value hash)
{
mrb_value ifnone;
mrb_get_args(mrb, "o", &ifnone);
mrb_hash_modify(mrb, hash);
mrb_iv_set(mrb, hash, mrb_intern_lit(mrb, "ifnone"), ifnone);
RHASH(hash)->flags &= ~(MRB_HASH_PROC_DEFAULT);
return ifnone;
}
最初にmrg_get_args
で変数ifnone
にオブジェクトを受け取り、自身(hash
)のインスタンス変数名ifnone
に、受け取ったオブジェクトを代入しています。
###インスタンス変数の値を得る
mrb_value
mrb_iv_get(mrb_state *mrb, mrb_value obj, mrb_sym sym)
- mrb: mrb_stateへのポインタ
- obj: インスタンス変数を保持しているオブジェクト
- sym: インスタンス変数名のシンボル
- 戻り値: mrubyオブジェクト
###Cの文字列(charの配列)からStringクラスのオブジェクトを得る
mrb_value
mrb_str_new_cstr(mrb_state *mrb, const char *p)
- mrb: mrb_stateへのポインタ
- p: charの配列への参照
- 戻り値: Stringクラスのインスタンス
###StringクラスのオブジェクトからCの文字列(charの配列)を得る
char *
mrb_str_to_cstr(mrb_state *mrb, mrb_value str0)
- mrb: mrb_stateへのポインタ
- str0: Stringクラスのインスタンス
- 戻り値: charの配列への参照
###Cの数値からmrubyの数値を得る
mrb_fixnum_value(c)
- c: Cの数値(int, float, double いずれも可?)
- 戻り値: Fixnumクラスのインスタンス
###Cの文字列からシンボルを得る
mrb_intern_lit(mrb, lit)
- mrb: mrb_stateへのポインタ
- lit: Cの文字列
- 戻り値: Symbolクラスのインスタンス
###mrubyのメソッドを実行する
mrb_funcall(mrb, obj, "メソッド名", 引数の数, 引数1, 引数2 ...)
- mrb: mrb_stateへのポインタ(mrb_state *)
- obj: メソッド呼び出しの対象となるオブジェクト。(mrb_value)
- "メソッド名": 呼び出すメソッド名。文字列で渡す。(char*)
- 引数の数: メソッドに渡す引数の数。(int)
- 引数1, 引数2, ....: 「引数の数」で指定した分だけ引数を列挙する。(mrb_value)
C側からmrubyのメソッドを実行する。第2引数はメソッド呼び出しの対象となるオブジェクト、第3引数にメソッド名を文字列で渡す。第4引数は、その後に続く引数の数で、移行、渡された引数の数だけ引数が続く。
###mrubyのエラーを発生させる
mrb_raise(mrb, エラークラス, "エラーメッセージ");
- mrb: mrb_stateへのポインタ(mrb_state *)
- エラークラス: mrubyのエラーを表現するクラス()
- "エラーメッセージ": エラー発生時に表示するエラーメッセージ
エラークラスの所は、mrb_class_get("クラス名文字列")
を使って得られたクラスを渡す。
ちなみにエラーのクラスに関してはinclude/mruby.h
内のマクロで定義してあるので、これを使うみたい。
/* macros to get typical exception objects
note:
+ those E_* macros requires mrb_state* variable named mrb.
+ exception objects obtained from those macros are local to mrb
*/
#define E_RUNTIME_ERROR (mrb_class_get(mrb, "RuntimeError"))
#define E_TYPE_ERROR (mrb_class_get(mrb, "TypeError"))
#define E_ARGUMENT_ERROR (mrb_class_get(mrb, "ArgumentError"))
#define E_INDEX_ERROR (mrb_class_get(mrb, "IndexError"))
#define E_RANGE_ERROR (mrb_class_get(mrb, "RangeError"))
#define E_NAME_ERROR (mrb_class_get(mrb, "NameError"))
#define E_NOMETHOD_ERROR (mrb_class_get(mrb, "NoMethodError"))
#define E_SCRIPT_ERROR (mrb_class_get(mrb, "ScriptError"))
#define E_SYNTAX_ERROR (mrb_class_get(mrb, "SyntaxError"))
#define E_LOCALJUMP_ERROR (mrb_class_get(mrb, "LocalJumpError"))
#define E_REGEXP_ERROR (mrb_class_get(mrb, "RegexpError"))
#define E_NOTIMP_ERROR (mrb_class_get(mrb, "NotImplementedError"))
#define E_FLOATDOMAIN_ERROR (mrb_class_get(mrb, "FloatDomainError"))
#define E_KEY_ERROR (mrb_class_get(mrb, "KeyError"))
####Example
例としてstringクラスで定義されているmrb_str_new_static関数を見てみます。
mrb_value
mrb_str_new_static(mrb_state *mrb, const char *p, size_t len)
{
struct RString *s;
if (len >= MRB_INT_MAX) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "string size too big");
}
..
}
渡された文字列の長さがMRB_INT_MAX
以上だった場合に例外を発生しています。
また、エラーメッセージを動的に生成する為のmrb_raisef
も用意してありました。
mrb_raisef(mrb, E_ARGUMENT_ERROR, "wrong number of arguments (%S for 1)", mrb_fixnum_value(argc));
同じくstring.c内で使用されていました。値を埋め込みたい位置に%S
を埋め込んで、引数として値を渡すと、mrb_valueならよしなに表示してくれるようです。
とりあえず今はここまで...なにか解ったらまた追記します。。