本記事は、mruby advent calendar 2016の23日目の記事です。22日は、ore_publicさんのruby製のコマンドラインツールをmrubyに置き換えるでした。
mrubyインタプリタ、mrubyの実装的にはmrb_state構造体なのですが、一つのプロセスの中で複数mrb_stateを作らざるを得ない場合に、複数のmrb_state間でデータを共有して使いたい場合がありませんか?ふむふむなるほど、やはり皆さん共有したいようですね。
これまでは、mrb_state間でデータをコピーする際にドキュメントに書かれていない知識を使って頑張ってやる必要があったのですが、それを簡単にできるように、mrubyのC APIを拡張するmrbgemのmruby-pointerを作りました。
今回はその使い方を説明します。
典型的な使い方
例えば、とあるmrb_state上で実行したメソッドの中で作られたポインタを、別のmrb_stateのメソッドの中で使いたい場合に、mruby-pointerを使えば以下のように簡単に実現できます。以下のコードでやっていることは、
mrb_src
インタプリタ上でPter.set('I am pter')
メソッドを実行し、引数の文字列データを保存するmrb_src
インタプリタからmrb_dst
インタプリタにその保存したデータのポインタをコピーするmrb_dst
インタプリタからそのデータを使ってメソッドを定義し実行する
になります。以下コードです。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "mrb_pointer.h" #include "mruby.h" #include "mruby/compile.h" static mrb_value mrb_pter_set(mrb_state *mrb, mrb_value self) { char *str; mrb_get_args(mrb, "z", &str); mrb_udptr_set(mrb, (void *)strdup(str)); return self; } static mrb_value mrb_pter_get(mrb_state *mrb, mrb_value self) { char *str; str = (char *)mrb_udptr_get(mrb); return mrb_str_new_cstr(mrb, str); } const char *pter_set_code = "Pter.set('I am pter')"; const char *pter_get_code = "puts Pter.get"; int main(int argc, char *argv[]) { mrb_state *mrb_src = mrb_open(); mrb_state *mrb_dst = mrb_open(); struct RClass *pter_src = mrb_define_class(mrb_src, "Pter", mrb_src->object_class); struct RClass *pter_dst = mrb_define_class(mrb_dst, "Pter", mrb_dst->object_class); mrb_define_class_method(mrb_src, pter_src, "set", mrb_pter_set, MRB_ARGS_REQ(1)); mrb_define_class_method(mrb_dst, pter_dst, "get", mrb_pter_get, MRB_ARGS_NONE()); /* create a shared ptr object */ mrb_udptr_init(mrb_src); /* set ptr on mrb_src into the shared ptr object */ mrb_load_string(mrb_src, pter_set_code); /* copy the shared ptr object from mrb_src to mrb_dst */ mrb_udptr_copy(mrb_src, mrb_dst); /* get ptr on mrb_src from shared ptr object on mrb_dst */ mrb_load_string(mrb_dst, pter_get_code); mrb_udptr_free(mrb_src); mrb_close(mrb_src); mrb_close(mrb_dst); return 0; }
これらを実現するためのC APIである、mrb_udptr_init
、mrb_udptr_copy
、mrb_udptr_set
、mrb_udptr_get
、mrb_udptr_free
を提供しています。詳しくはmruby-pointerのinclude/
以下のヘッダを見ると良いでしょう。
ふむふむ、簡単ですね。
$ ./bintest/shared_ptr I am pter
redisのコネクションデータをインタプリタ間でコピーする
より実用的には、ミドルウェアにmrubyを組み込んでいる状況で、ミドルウェア起動時とミドルウェアに対するリクエスト処理のフックで、どうしてもmrb_stateを分離しないといけない場合があります。そういった場合でも、ミドルウェア起動時に特定のmrb_state内でredisにコネクションを貼り、そのコネクションデータをリクエスト処理時の別のmrb_stateで使いまわすことができるようになります。
以下にサンプルコードを示します。
#include <stdio.h> #include "mrb_pointer.h" #include "mruby.h" #include "mruby/compile.h" int main(int argc, char *argv[]) { mrb_state *mrb_src = mrb_open(); mrb_state *mrb_dst = mrb_open(); /* create a shared ptr object */ mrb_udptr_init(mrb_src); /* set ptr on mrb_src into the shared ptr object */ mrb_load_string(mrb_src, "Redis.connect_set_raw '127.0.0.1', 6379"); /* copy the shared ptr object from mrb_src to mrb_dst */ mrb_udptr_copy(mrb_src, mrb_dst); /* get ptr on mrb_src from shared ptr object on mrb_dst */ mrb_load_string(mrb_dst, "redis = Redis.new; redis['hoge'] = 'foo'; p redis['hoge']"); mrb_udptr_free(mrb_src); mrb_close(mrb_src); mrb_close(mrb_dst); return 0;
このように、mrb_src
で貼ったredisのコネクションデータのポインタをmrb_dst
で使いまわせるように、mruby-redisのメソッドをmruby-pointerを使って実装しておくと、簡単にmrb_state間でデータを共有することできます。実際にmruby-pointerを使ってmruby-redisに上記のようなメソッドを実装したPRは以下になります。
ふむふむなるほど、Redis.connect_set_raw
によってredisにコネクション貼っておいて、そのポインタをコピー可能な領域に保存しておき、引数無しでRedis.new
メソッドを実行した時は、そのデータ領域からコネクションデータを使って、redisオブジェクトを作るようになっていますね。なるほど便利。
ふむふむ、やはり簡単ですね。
その他C APIをmrbgem化するための知見
inlcudeファイルの見せ方
mrubyのアプリ組み込みで、libmruby.flags.mak
にすべてのmrbgemの必要なflag情報がまとまっているのですが、C APIとしてmrbgemを使う場合は、そのC APIのヘッダファイルをアプリのビルド側から見えるようにする必要があります。そのためには、mruby-pointer/include
以下に有効にしたいヘッダファイルをおいた上で、mrbgem.rake
の中に以下のように記述しましょう。
MRuby::Gem::Specification.new('mruby-pointer') do |spec| spec.license = 'MIT' spec.authors = 'MATSUMOTO Ryosuke' spec.version = '0.0.1' spec.summary = 'Provide mruby C API which shared pointer between two mrb_states' spec.mruby.cc.include_paths << "#{spec.dir}/include" end
spec.mruby.cc.include_paths
を使うことで、例えば今回のmrb_pointer.h
のincludeファイル場所がlibmruby.flags.mak
に反映されるため、アプリ組み込み時には以下のようなMakefile
を書く事で動的にヘッダファイルの居場所を取得できるようになります。
MRUBY_POINTER_ROOT=$(shell pwd)/.. MRUBY_ROOT=$(MRUBY_POINTER_ROOT)/mruby MRUBY_CFLAGS=$(shell $(MRUBY_ROOT)/bin/mruby-config --cflags) MRUBY_LDFLAGS=$(shell $(MRUBY_ROOT)/bin/mruby-config --ldflags) MRUBY_LDFLAGS_BEFORE_LIBS=$(shell $(MRUBY_ROOT)/bin/mruby-config --ldflags-before-libs) MRUBY_LIBS=$(shell $(MRUBY_ROOT)/bin/mruby-config --libs)
C APIのテストにはbintest機能を使う
mrubyのテストは基本的にRuby側でテストを行うので、mruby本体の機能としてCのテスト機能はもっていません。ですので、mruby本体の機能を使ってテストする場合はbintest機能を使うと良いでしょう。mruby-pointerの場合は、ステート間でデータをコピーするようなCのコードを書いた上で、それをbintest
ディレクトリの中にMakefile
と共に配置して、build_config.rb
でbintestをenabledにした上で、以下のような簡単なテストコードを書いています。Cのコードは上記で紹介した2つのコードを使っています。
mruby-pointer/build_config.rb
MRuby::Build.new do |conf| toolchain :gcc conf.gembox 'full-core' conf.gem File.expand_path(File.dirname(__FILE__)) conf.gem :mgem => 'mruby-redis' conf.enable_test conf.enable_bintest end
mruby-pointer/bintest/mrb_pointer.rb
assert('mruby-pointer bin test') do assert_equal "I am pter\n", `../bintest/shared_ptr` end assert('mruby-pointer bin test for redis') do assert_equal "\"foo\"\n", `../bintest/shared_redis_ctx` end
これが、mrubyのtest時に自動で解釈され、テストされます。
まとめ
ということで、これまでmrb_state間でデータを共有する際に、できるにはできたんですが、そのやり方があまりドキュメントになかったり、定番の方法があまり定まっていなかったということで、それを解決するためにmruby-pointerを作って、使い方を本エントリで示しました。
皆さんも、どうしてもmrb_stateをわけないと行けない状況でも、mruby-pointerを使ってデータをうまく共有するようなメソッドを書いておけば、色々便利になると思われますので是非ご活用下さい。