mrubyからc経由でgoの関数を呼ぶ


go言語はビルド時にbuildmode=c-archiveを指定すると、静的ライブラリを生成することができます。

$ go build -buildmode=c-archive main.go
$ ls
main.a main.go main.h

今回はこれをmrubyに組み込んでみたいと思います。

mruby-mrbgem-template で mrbgemのひな型を作成

以下のコマンドでひな型を作成し、作成したディレクトリに移動します。

$ git clone https://github.com/matsumotory/mruby-mrbgem-template
$ cd mruby-mrbgem-template
$ rake
$ cd mruby-example

go側の実装

golang の Windows 版が buildmode=c-archive をサポートした。 を参考に作成します。

main.go
package main

import (
    "C"
    "fmt"
)

//export PrintLine
func PrintLine(text *C.char) {
    fmt.Println(C.GoString(text))
}

func main() {
}

今回は単純に関数の引数で渡された文字列をfmt.Println()で表示し、文字列をそのまま返す。というだけの関数PrintLineを作成しています。ファイルはmruby-exampleの下にgoというディレクトリを作成し、その中に置きました。

goディレクトリに移動し、以下のようにビルドします。

$ go build -buildmode=c-archive -o libmain.a main.go

mruby(c)側の実装

基本的な構造はmruby-mrbgem-templateが作ってくれているので、ひな型mrbgemの中のsrcディレクトリに移動し、mrb_example.cを編集していきます。

今回は単純にgo側で定義した関数を呼び出して、戻り値で戻ってきた値を返して、mrubyスクリプトとgoの関数をブリッジする関数を足します。

mrb_exmaple.c
static mrb_value mrb_example_hi(mrb_state *mrb, mrb_value self)
{
  return mrb_str_new_cstr(mrb, "hi!!");
}

// 今回はこの部分の関数を定義
static mrb_value mrb_example_hi2(mrb_state *mrb, mrb_value self)
{
  extern char* PrintLine(char*);
  char *ret;
  ret = PrintLine("hi!! from go");
  return mrb_str_new_cstr(mrb, ret);
}

void mrb_mruby_example_gem_init(mrb_state *mrb)
{
  struct RClass *example;
  example = mrb_define_class(mrb, "Example", mrb->object_class);
  mrb_define_method(mrb, example, "initialize", mrb_example_init, MRB_ARGS_REQ(1));
  mrb_define_method(mrb, example, "hello", mrb_example_hello, MRB_ARGS_NONE());
  mrb_define_class_method(mrb, example, "hi", mrb_example_hi, MRB_ARGS_NONE());
  mrb_define_class_method(mrb, example, "hi2", mrb_example_hi2, MRB_ARGS_NONE()); // あと、この部分の忘れずに追加
  DONE;
}

mrbgem.rake の編集

コードはできたので、次にビルドする為の設定を書いていきます。まずはmrbgem.rakeにgoのライブラリをリンクしましょう。私の環境がWindowsなので、余分にライブラリを追加していますが、macならmainのみで大丈夫なはずです。

mrbgem.rake
MRuby::Gem::Specification.new('mruby-example') do |spec|
  spec.license = 'MIT'
  spec.authors = 'your-name'

  # この2行を追加
  spec.linker.libraries << %w(main ws2_32 winmm)
  spec.linker.library_paths << File.join(__dir__, "go")
end

.travis_build_config.rb の編集

macの人は不要かもしれません。私の環境では以下の設定が必要でした。

.travis_build_config.rb
MRuby::Build.new do |conf|
  toolchain :gcc
  conf.gembox 'default'
  conf.gem File.expand_path(File.dirname(__FILE__))
  conf.enable_test

  conf.cc do |cc|
    cc.command = 'x86_64-w64-mingw32-gcc'
  end

  conf.linker do |linker|
    linker.command = 'x86_64-w64-mingw32-gcc'
  end

  conf.archiver do |archiver|
    archiver.command = 'x86_64-w64-mingw32-gcc-ar'
  end
end

ビルド

mruby-mrbgem-templateで作成したひな型mrbgemにはRakefileがついていて、コマンド一発でビルドしてくれます。

$ rake

実行

ビルドが成功すると、./mruby/binの下にmrubyの実行ファイルができています。試しに以下のスクリプトを実行してみましょう。

test.rb
puts Example.hi
puts Example.hi2
$ ./mruby/bin/mruby test.rb
hi!!
hi!! from go
hi!! from go

無事にgo側でfmt.Println()した内容と戻り値をputsした内容の両方が表示されていますね。

まとめ

mrubyを使って、goの関数の呼び出しと戻り値を使ってのmrubyスクリプトの実行を行いました。
go特有の並列処理をうまく組み合わせることができれば、もっと面白いことができるかもしれません。
また、C言語に限らず、mrubyを拡張する一つの方法になるのかもしれませんし、
逆にgo言語をmrubyで拡張していくことができるかもしれませんね。

今後の課題

もし、mrbgemの一部としてgoのソースを含めるのであれば、静的ライブラリのビルドも自動でできるようにしていきたい