mrbgemはどのように「組み込まれる」か


mrubyアドベントカレンダーの 15日の記事です。風邪ということで本当に草葉の陰状態でした...ご勘弁ください ><

mrubyは、C言語の中から使う分には、 mrb_open() を呼び出すだけでmrb_state*を作成して使えるので非常に簡単なAPIとなっています。以下はmruby-cliが生成するCのコードの抜粋ですが、ARGVの定義なんかを除けば(これも数行)実質やっていることは2行です。

mrb_state *mrb = mrb_open();
// ...
mrb_funcall(mrb, mrb_top_self(mrb), "__main__", 1, ARGV);

一方で、このmrb_state*の中には、変な言い方ですがmrbgemが「組み込まれた」形になるわけですが、イメージが湧き辛かったこともあり、どのような仕組みになっているのか追いかけてみたので、その話をします。

現在のmasterバージョン d9049c10 ではmrb_openの実態はmrb_open_allocf()で、これは場合によって使うmalloc/freeなどの関数を入れ替えられるよう指定できるようにした関数です。その中でmrb_open_core()を呼んでmrb_state*を作っています。

MRB_API mrb_state*
mrb_open_core(mrb_allocf f, void *ud)
{
  static const mrb_state mrb_state_zero = { 0 };
  static const struct mrb_context mrb_context_zero = { 0 };
  mrb_state *mrb;

  mrb = (mrb_state *)(f)(NULL, NULL, sizeof(mrb_state), ud);
  if (mrb == NULL) return NULL;

  *mrb = mrb_state_zero;
  mrb->allocf_ud = ud;
  mrb->allocf = f;
  mrb->atexit_stack_len = 0;

  mrb_gc_init(mrb, &mrb->gc);
  mrb->c = (struct mrb_context*)mrb_malloc(mrb, sizeof(struct mrb_context));
  *mrb->c = mrb_context_zero;
  mrb->root_c = mrb->c;

  mrb_init_core(mrb); // mrubyの組み込みクラスなどを初期化していく関数

  return mrb;
}

mrb_open_core以降、DISABLE_GEMSが定義されていなければmrb_init_mrbgems()を呼び出すというわけです。

MRB_API mrb_state*
mrb_open_allocf(mrb_allocf f, void *ud)
{
  mrb_state *mrb = mrb_open_core(f, ud);

  if (mrb == NULL) {
    return NULL;
  }

#ifndef DISABLE_GEMS
  mrb_init_mrbgems(mrb);
  mrb_gc_arena_restore(mrb, 0);
#endif
  return mrb;
}

このmrb_init_mrbgems()がキモです。実はこの関数は、mrubyのソースコードの中では定義されていません。

どこで定義されているかというと、ビルド中に自動生成されるmruby/build/${BUILD_HOST}/mrbgems/gem_init.cの中なのですね。

void
mrb_init_mrbgems(mrb_state *mrb) {
  GENERATED_TMP_mrb_mruby_sprintf_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_print_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_math_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_time_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_struct_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_compar_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_enum_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_string_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_numeric_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_array_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_hash_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_range_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_proc_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_symbol_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_random_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_object_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_objectspace_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_fiber_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_enumerator_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_enum_lazy_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_toplevel_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_error_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_kernel_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_class_ext_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_io_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_process_gem_init(mrb);
  GENERATED_TMP_mrb_mruby_exec_gem_init(mrb);
  mrb_state_atexit(mrb, mrb_final_mrbgems);
}

このそれぞれのGENERATED_TMP_*関数はどこに生成されているかというと、それぞれのmrbgemをビルドする途中で作成しています。例えば mruby-process を依存に含めていると、以下のようなファイルを作成します。

作られる場所は mruby/build/${BUILD_HOST}/mrbgems/mruby-process/gem_init.c です。

/*
 * This file is loading the irep
 * Ruby GEM code.
 *
 * IMPORTANT:
 *   This file was generated!
 *   All manual changes will get lost.
 */
#include <stdlib.h>
#include <mruby.h>
#include <mruby/irep.h>
/* dumped in little endian order.
   use `mrbc -E` option for big endian CPU. */
#include <stdint.h>
extern const uint8_t gem_mrblib_irep_mruby_process[];
const uint8_t
#if defined __GNUC__
__attribute__((aligned(4)))
#elif defined _MSC_VER
__declspec(align(4))
#endif
gem_mrblib_irep_mruby_process[] = {
0x45,0x54,0x49,0x52,0x30,0x30,0x30,0x34,0x6c,0xec,0x00,0x00,0x07,0xd9,0x4d,0x41,
0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x04,0xd8,0x30,0x30,
0x30,0x30,0x00,0x00,0x00,0x40,0x00,0x01,0x00,0x02,0x00,0x02,0x00,0x00,0x00,0x07,
//....
0x00,0x00,0x05,0x00,0x02,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,
0x00,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};
void mrb_mruby_process_gem_init(mrb_state *mrb);
void mrb_mruby_process_gem_final(mrb_state *mrb);

void GENERATED_TMP_mrb_mruby_process_gem_init(mrb_state *mrb) {
  int ai = mrb_gc_arena_save(mrb);
  mrb_mruby_process_gem_init(mrb);
  mrb_load_irep(mrb, gem_mrblib_irep_mruby_process);
  if (mrb->exc) {
    mrb_print_error(mrb);
    exit(EXIT_FAILURE);
  }
  mrb_gc_arena_restore(mrb, ai);
}

void GENERATED_TMP_mrb_mruby_process_gem_final(mrb_state *mrb) {
  mrb_mruby_process_gem_final(mrb);
}

GENERATED_TMP_mrb_mruby_process_gem_init()で二つのことをしているのがわかります。

  • mrb_state*を引数に、Cで定義したmgem初期化処理であるmrb_mruby_process_gem_init(mrb_state*)を呼び出す
  • その後、Rubyで定義したgemの内容をmrubyのバイトコードに変換したものを、gem_mrblib_irep_mruby_process変数に格納し、mrb_load_irepで同じmrb_state*に読み込ませる

これで、Cで定義したmgemの実装も、Rubyで定義した実装も、両方読み込まれるという訳ですね。これを、依存しているmgemの数だけ作成して、全てをリンクして、最終的にあなただけの mrb_open() が完成する、という感じです。

よく、mruby-cliなんかの強みで、ワンバイナリのクライアントが作れて便利という話がありますが、やっていることとしてはRubyで書いた処理に関してはバイトコード化して、ソースコードに組み込める(mrbc コマンドを使えばすぐにできます)のでそうしている、というイメージを持っていただければと。

(そういうことなので、外部ライブラリなんかを使っているmgemは、自力でスタティックリンクしないとワンバイナリ配布はできないこともあります。まあ、スタティックリンクさえすればなんでもありではありますが...)


mgemの活用に関して、特にCRubyに慣れている方の場合だいぶ考え方が違うのでそこを認識すると幸せなのかもなあと思い、雑に書いてみました。