人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

複数のmrubyインタプリタ間でデータを共有して使う方法

本記事は、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を作りました。

github.com

今回はその使い方を説明します。

典型的な使い方

例えば、とある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_initmrb_udptr_copymrb_udptr_setmrb_udptr_getmrb_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は以下になります。

github.com

ふむふむなるほど、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を使ってデータをうまく共有するようなメソッドを書いておけば、色々便利になると思われますので是非ご活用下さい。