LoginSignup
22
15

More than 5 years have passed since last update.

ツナでもわかるmruby[7回目:mrbgemsの作り方メモ(勉強中)]

Posted at

ツナでもわかるとかいいつつ、今回はツナではわかりそうもありません...(´ω`;)

ここらでmrbgemを作りたかったので、作りつつ調べた事をメモしてます。

ちなみに作ってみたのはmruby-mqttというmrubyでMQTTクライアントを実現する為のライブラリ。といいつつ実はただのpahoのラッパーです。

注意:

勉強しつつ更新していっているので、内容には間違いが有る可能性が高いです。

ディレクトリ構造

ディレクトリは以下のような構造になるようです。

mruby-クラス名/ mrbgem.rake

               src    / クラス名.c
                        [その他のCソースファイル]

               mrblib / クラス名.rb
                        [その他のmrubyソースファイル]

               test / [テストファイル]

ファイル名は必ずしもクラス名でなくてもいいと思いますが、スタイル上の問題でそのようにする事が多いようです。

このディレクトリを適当な位置において、build_config.rb内で

build_config.rb
  conf.gem 'ディレクトリ名'

のように指定しておけば一緒にビルドされます。mrubyのディレクトリ外でもいいみたい。

Example

build_config.rb
  conf.gem '../mruby-mqtt'

mrbgem.rake

下記はmruby-timemrbgem.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でメソッドを定義していく。

sample.c
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

mrbgems/mruby-fiber/src/fiber.c
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でされていました。以下のようなオブジェクトのタイプが有るようです。

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

mrbgems/mruby-fiber/src/fiber.c
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

src/gc.c
mrb_define_class_method(mrb, gc, "start", gc_start, MRB_ARGS_NONE());

mrubyに含まれているmrbgemsでの例

Stringクラスを拡張するmruby-string-utf8の初期化関数を抜粋します。

mrbges/mruby-string-utf8/src/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

mruby-string-utf8
void
mrb_mruby_string_utf8_gem_final(mrb_state* mrb)
{
}

これはどういうタイミングで呼ばれるのか、まだよくわかっていません...

コード中で使用できるAPI関数

引数の取得

src/class.c
mrb_int
mrb_get_args(mrb_state *mrb, const char *format, ...)

mruby側から渡される引数はmrb_get_argsを使って受け取ります。

引数のフォーマットには以下が有るようです。

src/class.c
  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の関数を幾つか見てみます。

mrbgems/mruby-time/src/mruby-time.c
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から

mrbgems/mruby-time/src/mruby-time.c
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における例です。

mrbgems/mruby-fiber/src/fiber.c
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に引数の数を受け取っています。

インスタンス変数に値を代入する

src/valiable.c
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クラスにある関数を見てみます。

src/hash.c
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に、受け取ったオブジェクトを代入しています。

インスタンス変数の値を得る

valiable.c
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の配列)を得る

src/string.c
char *
mrb_str_to_cstr(mrb_state *mrb, mrb_value str0)
  • mrb: mrb_stateへのポインタ
  • str0: Stringクラスのインスタンス
  • 戻り値: charの配列への参照

Cの数値からmrubyの数値を得る

mruby.h
mrb_fixnum_value(c)
  • c: Cの数値(int, float, double いずれも可?)
  • 戻り値: Fixnumクラスのインスタンス

Cの文字列からシンボルを得る

mruby.h
mrb_intern_lit(mrb, lit)
  • mrb: mrb_stateへのポインタ
  • lit: Cの文字列
  • 戻り値: Symbolクラスのインスタンス

mrubyのメソッドを実行する

mrb_funcall
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_raise(mrb, エラークラス, "エラーメッセージ");
  • mrb: mrb_stateへのポインタ(mrb_state *)
  • エラークラス: mrubyのエラーを表現するクラス()
  • "エラーメッセージ": エラー発生時に表示するエラーメッセージ

エラークラスの所は、mrb_class_get("クラス名文字列")を使って得られたクラスを渡す。
ちなみにエラーのクラスに関してはinclude/mruby.h内のマクロで定義してあるので、これを使うみたい。

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関数を見てみます。

src/string.c
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も用意してありました。

src/string.c
mrb_raisef(mrb, E_ARGUMENT_ERROR, "wrong number of arguments (%S for 1)", mrb_fixnum_value(argc));

同じくstring.c内で使用されていました。値を埋め込みたい位置に%Sを埋め込んで、引数として値を渡すと、mrb_valueならよしなに表示してくれるようです。

とりあえず今はここまで...なにか解ったらまた追記します。。

22
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
15