東京Ruby会議11での発表「アプリケーションへのRubyインタープリターの組み込み」とOSS Gateワークショップ2016-05-28 #tkrk11 #oss_gate - 2016-06-01 - ククログ

ククログ

株式会社クリアコード > ククログ > 東京Ruby会議11での発表「アプリケーションへのRubyインタープリターの組み込み」とOSS Gateワークショップ2016-05-28 #tkrk11 #oss_gate

東京Ruby会議11での発表「アプリケーションへのRubyインタープリターの組み込み」とOSS Gateワークショップ2016-05-28 #tkrk11 #oss_gate

5月28日に開催された東京Ruby会議11で「アプリケーションへのRubyインタープリターの組み込み」と題して、アプリケーションにRubyを組み込む実装について話しました。

関連リンク:

質疑応答の補足

内容は前述のスライドや発表動画を参照してください。ここでは発表後の質疑応答の内容について補足します。

質問:ruby_sysinit()は呼ばなくていいの?

ruby_sysinit()は呼ばなくていいの?」に対する当日の回答は「ruby_sysinit()の説明は省略した」だったのですが、どうして省略したかを補足します。

Rubyインタプリターを組み込んだアプリケーションの1つであるrubyコマンドの実装(main.c)を見るとruby_sysinit()を呼んでいます。そのため、Rubyインタプリターの初期化には必要なAPIにみえます。

しかし、ruby_sysinit()のコメントには次のように書いています。ざっくり言うとrubyコマンドを初期化するためのもので、Rubyインタプリターを組み込むときはこの関数を呼ぶんじゃなくて自分で初期化してね、と言っています。

Initializes the process for ruby(1). This function assumes this process is ruby(1) and it has just started. Usually programs that embeds CRuby interpreter should not call this function, and should do their own initialization.

中身を見ると、コマンドライン引数と標準入出力を初期化しています。Windowsで動く場合はもっと初期化しています。

Rubyインタプリターを組み込んだアプリケーション例として紹介したmilter managerではruby_sysinit()を呼んでいません。コマンドライン引数は自前で処理しています。

ただ、ruby_sysinit()を呼ばないというのはmilter managerがWindowsをサポートしていないからできることです。Windows用の初期化をしているrb_w32_sysinit()をチラ見するとわかりますが、この関数がやっている処理のうち、自分に必要な分はどこかを判断してそれらを自前で実装することは難しいでしょう。(Windowsに詳しい人ならそうでもないかもしれません。)

そのため、Windowsでも動くアプリケーションにRubyインタプリターを組み込む場合はruby_sysinit()を呼ぶのがよいでしょう。(コメントでは呼ぶなと書いていますが。)

質問:共有ライブラリーの方にアプリケーションのメイン関数を渡せばアプリケーションでRUBY_INIT_STACKを呼ばなくていいんじゃない?

スライドで言うと以下のページの実装についての質問です。

以下のようにしてembedded_init()内でapplication_main()を呼ぶようにすればいいんじゃない?という話です。

int
main(void)
{
  embedded_ruby_module = dlopen();
  embedded_ruby_init = dlsym(embedded_ruby_module, "embedded_init");
  embedded_ruby_init(application_main);
}
void
embedded_init(main_func application_main)
{
  RUBY_INIT_STACK;
  /* ... */
  application_main();
}

これに対する回答は「RubyインタプリターとPythonインタプリターを一緒に組み込めないのでやりたいことを実現できない」でした。一緒に組み込む時のイメージは次の通りです。

void
main(void)
{
  embedded_ruby_module = dlopen();
  embedded_ruby_init = dlsym(embedded_ruby_module, "embedded_init");
  embedded_ruby_init(application_main);
  /* ↓はアプリケーションのメイン関数の前に実行しないといけないけど、
     ↑でメイン関数が実行されちゃう。 */

  embedded_python_module = dlopen();
  embedded_python_init = dlsym(embedded_python_module, "embedded_init");
  embedded_python_init(application_main);
}

それに対する別案が「他のインタプリターの初期化もやる関数を渡せば?」でした。こんなイメージです。

void
main(void)
{
  embedded_ruby_module = dlopen();
  embedded_ruby_init = dlsym(embedded_ruby_module, "embedded_init");
  embedded_python_module = dlopen();
  embedded_python_init = dlsym(embedded_python_module, "embedded_init");

  init_func init_functions[] = {
    embedded_python_init,
    application_main,
    NULL
  };
  embedded_ruby_init(run_init_functions, init_functions);
}

void
run_init_functions(init_func *init_functions)
{
  if (init_functions[0]) {
    init_functions[0]();
    run_init_functions(init_functions + 1);
  } else {
    application_main();
  }
}

質疑応答はここで時間切れでした。

たしかにこれで動きそうです。

会議後にはこんなアイディアもありました。

これはどういうことかというと、アプリケーション側ではRUBY_INIT_STACKに必要なデータを用意するだけにして、実際の呼び出しは別の共有ライブラリーの方に移す、という実装にすればいいんじゃない?ということです。

RUBY_INIT_STACKruby_init_stack()というアドレスを引数にとるAPIを呼び出しているだけなので、このアイディアも実現できます。こんなイメージです。

void
main(void)
{
  int address;

  embedded_ruby_module = dlopen();
  embedded_ruby_init = dlsym(embedded_ruby_module, "embedded_init");
  embedded_ruby_init(&address);

  application_main();
}
void
embedded_init(int *address)
{
  ruby_init_stack((VALUE *)address);
  /* ... */
}

milter managerでも実装できます。(アプリケーション本体に手を入れられないケースではこの方法の実装は難しいでしょう。)

「アプリケーションがRUBY_INIT_STACKを呼ばないといけない問題」は複数の方法で解決できますね!このような場で話をするといろんなアイディアを聞けて便利ですね!みなさんも積極的に実装の話をしてはいかがでしょうか。

RUBY_INIT_STACK問題が解決したので、複数言語のインタプリターを組み込んだケースのことを想像してみたところ、次はforkで問題にあたりそうです。Ruby以外の言語が複数のスレッドを作っている場合が問題になりそうです。

外部ライブラリーとGC

質疑応答では触れられませんでしたが、外部のライブラリーが使用しているメモリー量をRuby(mrubyもCRubyも)のGCシステムが知らないために適切なタイミングでGCが動かない問題についても補足します。

発表では「外部からRubyのGCシステムにメモリー使用量を通知する仕組みが必要だと思う」と話しました。

CRubyのTypedDataにはメモリー使用量を返すAPIがありますが、それは使えないはずです。それを使うと、GCシステム側が情報をpullする仕組みになるからです。pullする仕組みにすると、いつpullするの?pullした情報をどうやって管理するの?あたりが大変になりそうです。

通知する仕組みだとこれらの問題を解決できそうです。

ということで、gc-triggerという拡張ライブラリーを作ってみました。このライブラリーは他の拡張ライブラリーに使用メモリー通知用のAPIを提供します。他の拡張ライブラリーがこのAPIを使ってメモリー使用量を通知すると、適切なタイミングでGC.startを実行します。

本来はCで提供しているAPIを呼んだほうがよいですが、テスト用・説明用にRubyのAPIも用意したので、そっちを使って使い方を説明します。

ここでは、例としてrcairoを使います。rcairoは画像を扱うからです。画像を扱う拡張ライブラリーはRuby管理外のメモリー使用量が大きめなので、この問題に遭遇しやすいケースなのです。

次のコードは1000個画像オブジェクトを生成します。どの画像オブジェクトも参照されていないのですぐにGCで回収できます。

require "cairo"

width = 6000
height = 6000
1000.times do |i|
  Cairo::ImageSurface.new(:argb32, width, height)
end

gc-triggerでメモリー使用量の増減を通知するコードは次のようになります。

require "gc-trigger"
require "cairo"

def image_surface_finalizer(size)
  lambda do |id|
    GCTrigger.update_memory_usage(-size)
  end
end

def create_image_surface(width, height)
  size = 4 * width * height
  surface = Cairo::ImageSurface.new(:argb32, width, height)
  GCTrigger.update_memory_usage(size)
  ObjectSpace.define_finalizer(surface, image_surface_finalizer(size))
  surface
end

width = 6000
height = 6000
1000.times do |i|
  create_image_surface(width, height)
end

画像オブジェクトのサイズは、単純化して「1画素のサイズ(4バイト)×縦×横」で計算しています。

それぞれの場合でのメモリー使用量をグラフにすると次のようになります。従来の方はメモリー使用量が安定しませんが、gc-triggerを使った方はメモリー使用量が一定になります。

メモリー使用量

発表で話したアイディアを動くようにしてみました。これで通じるでしょうか。

OSS Gateワークショップ2016-05-28

発表でも紹介しましたが、OSS Gateワークショップ2016-05-28を東京Ruby会議11と同時開催しました。東京Ruby会議11でポスターを展示していたので、そこではじめて知ったという方も多かったのではないでしょうか。

OSS GateおよびOSS Gateワークショップを紹介していて痛感したことは「口頭での説明でないとちゃんと伝えられない」ということでした。たとえば、「私はすごくないのでメンターできないです。。。」に対して口頭では伝わるように回答できますが、資料では伝わらないです。これだとスケールしないので口頭での説明でなくても伝わる資料の作成が必要です。

ポスターを展示しての紹介は想像以上に効果があり、OSS Gateワークショップ2016-07-30の登録者が10人以上増えました。東京Ruby会議11実行委員会に協力してもらえて本当によかったです。ありがとうございました。

ちなみに、想像以上に登録者が増えたおかげでメンターが足りなそうです。OSS開発に参加したことがある人(技術力不問)でOSSの開発に参加する人が増えるといいなぁと思う人は「メンター」としてOSS Gateワークショップ2016-07-30に登録してください。すでに定員オーバーでキャンセル待ちでの登録になりますが、気にしないでください。キャンセル待ち扱いでも気にせずに当日来てください。入れます。

まとめ

5月28日に開催された東京Ruby会議11での発表とOSS Gateワークショップ2016-05-28について補足しました。

東京Ruby会議11の参加者アンケートはまだ回答を受け付けているので参加した方はぜひ回答してください。