mruby関連の情報が何かと少なく私も苦労したので、何かの役に立つかと思い投稿してみます。
尚、現時点での正規リリースであるmruby1.2を対象としています。
(追記:2019/11/20 : 現時点最新版 mruby2.10でも問題ありませんでした)
Githubに上がってる最新のコミットで動くかどうかはわかりません。
#知っておきたいこと
mrubyはやっぱりrubyという名前がついてるだけあって、しっかりrubyしてます。APIを拡張する場合なんかもその辺のこと知ってるかどうかで色々と混乱しなくなると思うので、rubyは知らないけどmrubyってのがあるから使ってみるか、みたいな人は(俺とか)予めrubyについて勉強しておくと楽になります。
#アプリケーションの情報の登録など
ぶっちゃけこの記事読む人はこの項目が読みたいんじゃないかな、ということで早速。
mrubyスクリプトからアプリケーションを操作する際には、スクリプトのAPI用として用意した関数をコールバックさせる事で実現するわけですが、コールバックの際に渡される引数はmrb_state*とmrb_value(コール元になったオブジェクト。インスタンスメソッドとして用意したAPIではインスタンス自身が渡される)だけなのでアプリケーション固有の情報をグローバル変数で持ってたりしない限りはこの中からアプリケーションの情報を引っ張り出す必要があります。
まずはこの時に欲しい情報をmrb_stateに保存し、引っ張り出す方法について。
幾つかのやり方があると思いますが、とりあえず方法を1個適当に。
##C/C++構造体やクラスのポインタをmrubyの定数として保存する
mrubyに定数値を設定するC/C++用APIとして
MRB_API void mrb_define_global_const(mrb_state *mrb, const char *name, mrb_value val);
というのが用意されています。
nameはmrubyから参照する時の定数名、valは実際に保存する値です。
mrb_valueというのはvariantな型で、Objectが入ったり整数が入ったりブール値が入ったりする型なのですが、このmrb_valueにポインタを定数として設定してしまいます。
mrb_valueにポインタ値を保存する方法も幾つかありそうですが、私は
MRB_INLINE mrb_value mrb_fixnum_value(mrb_int i)
という関数を使いました。
1つ注意しておかなければいけないのは、mrb_int型というのはmrubyのビルド時にMRB_INT64
定数等をdefine(mrbconf.h
で定義)していない限り、通常は32Bit型ですので、64Bitアプリを作っている最中は32Bit毎に分ける必要があります。mrb_cptr_valueという関数も定義されており、これを使えばポインタがそのまま保存出来そうに見えるのですが、これについては使ってないのでわかりません(将来的に削除されそうな気がして怖い)
さて、例えば
mrb_define_global_const(mrb, "TestConst", val) ;
としてTestConst定数に値を保存したとしましょう。この値をコールバックされた関数から読みたい訳ですが、上で書いたようにコールバックされた関数に渡されるのはmrb_stateとmrb_value(C++でいうThisとして)だけなので、この中から情報を取得する必要があります。
mrb_define_global_constで定義した定数はグローバル定数としてどこからでも参照出来るのですが、実際にはmrb->object_classというクラスオブジェクトの子として登録されます。
ですので、
mrb_value value = mrb_const_get(mrb, mrb_obj_value(mrb->object_class), mrb_intern_lit(mrb, "TestConst")) ;
という感じでvalueにmrb_value型の定数が取れます。
mrb_value型の定数が取れたら、mrb_fixnum_valueでmrb_value化していた時は
mrb_int addressValue = mrb_int(mrb, value) ;
とすればaddressValueに32Bit値(か64Bit値か16Bit値)が取れます。
##コールバック関数をAPIとして登録する
rubyでは基本的に、何らかのメソッドを登録する際は何らかのクラス、ないしモジュールのメンバとして登録するのが基本という考えになってるっぽいのでそれに従っておいた方が何かと良いと思います。
ここではクラスを作り、そのクラスのクラスメソッド(クラスインスタンスが無くても呼べるメソッド。C++ではstaticなメソッドみたいなもん)を1個作ってみます。
###クラスを作る
mrubyのクラスのC/C++からみた型はstruct RClass *
となっています。
ポインタ型ですので実体がどこかにありますが、mrubyのシステムがよしなに管理してくれるようなのでC/C++のように、作ったポインタを間違えて上書きしてしまうとリークする、というような事もなさげです。
ここでは仮に
struct RClass *TestClass ;
TestClass = mrb_define_class(mrb, "TestClass", mrb->object_class) ;
としてクラスを定義します。
mrb_define_classの第3引数は、作成するクラスのスーパークラスを定義します。既存のクラスの派生クラスではないのであればmrb->object_class
がスーパークラスになります。
このクラスに必須の引数1つと可変長の引数が後に続くかもしれないクラスメソッドtest_method
を作ってみます。C/C++側の実装はtest_method_cという名前を仮に設定しておきますが、名前に制限がある訳ではありません。
mrb_define_class_method(mrb, TestClass, "test_method", test_method_c, MRB_ARGS_REQ(1)|MRB_ARGS_ANY()) ;
mrb_define_class_method
については色々なサイト、Qiita内にも説明が沢山あるのでそちらを見て下さい。
可変長引数については説明が少ないと思いますので以下に書きます。
mrb_value test_method_c(mrb_state *mrb, mrb_value self)
{
// この辺にアプリケーションの情報を取る処理が入ると思いねぇ
// 引数として整数と、残りの引数を取得する
mrb_int arg1 ;
mrb_value *leftArgs ;
mrb_int leftArgCount ;
mrb_get_args(mrb, "i*", &arg1, &leftArgs, &leftArgCount) ;
// arg1には1個目の引数が整数として入ります
// leftArgsは引数配列の先頭ポインタという感じで、leftArgCountに配列要素数が入ります
// 引数に応じた処理を以下に
for(int i = 0 ; i < leftArgCount ; i ++) {
if(mrb_fixnum_p(leftArgs[i])) {
// 整数引数に応じた処理
} else
if(mrb_float_p(leftArgs[i])) {
// 浮動小数点引数に応じた処理
} else
if(mrb_string_p(leftArgs[i])) {
// 文字列引数に応じた処理
} else
・
・
・
}
// メソッド処理本体
}
こんな感じになります。
mrb_fixnum_p
とかmrb_string_p
とかいうのはmrb_valueに実際には何の型の値が入っているのかを判定するマクロになっています。
ここでmrb_valueについて。
mrb_valueは上で、variantみたいな、と書きましたが整数を入れた後文字列として読もうとしても文字列変換をしてくれる訳ではないので、実際には何の型が入っているのかを判定する必要があります。
判定用のマクロはmrubyのinclude/mruby/value.h
に定義されていますので参照して下さい。
##アプリケーション内でのmruby VM開始から終了まで
以下に、上で書いた事を実際に使ったサンプルコードを書いてみます。
#include <stdio.h>
#include <stdlib.h>
#include <mruby.h>
#include <mruby/variable.h>
#include <mruby/string.h>
#include <mruby/compile.h>
mrb_value test_method_c(mrb_state *mrb, mrb_value self)
{
mrb_int valInt ;
mrb_int arg1 ;
mrb_value *leftArgs ;
mrb_int leftArgCount ;
int i ;
mrb_value value = mrb_const_get(mrb, mrb_obj_value(mrb->object_class), mrb_intern_lit(mrb, "TestConst")) ;
valInt = mrb_int(mrb, value) ;
// 引数として整数と、残りの引数を取得する
mrb_get_args(mrb, "i*", &arg1, &leftArgs, &leftArgCount) ;
// arg1には1個目の引数が整数として入ります
// leftArgsは引数配列の先頭ポインタという感じで、leftArgCountに配列要素数が入ります
printf("以下に引数を列挙します\n") ;
printf("最初の引数 %d\n", arg1) ;
// 引数に応じた処理を以下に
for(i = 0 ; i < leftArgCount ; i ++) {
if(mrb_fixnum_p(leftArgs[i])) {
// 整数引数に応じた処理
mrb_int tmpvalInt ;
tmpvalInt = mrb_int(mrb, leftArgs[i]) ;
printf("整数:%d\n", tmpvalInt) ;
} else
if(mrb_float_p(leftArgs[i])) {
// 浮動小数点引数に応じた処理
mrb_float tmpvalFloat ;
tmpvalFloat = mrb_float(leftArgs[i]) ;
printf("浮動小数点数:%f\n", tmpvalFloat) ;
} else
if(mrb_string_p(leftArgs[i])) {
// 文字列引数に応じた処理
char* tmpStr ;
tmpStr = mrb_str_to_cstr(mrb, leftArgs[i]) ;
printf("文字列:%s\n", tmpStr) ;
} else {
printf("整数、浮動小数点数、文字列以外の引数\n") ;
}
}
// メソッド処理本体
printf("引数終了\n") ;
printf("埋め込まれている数値は%d\n", valInt) ;
return self ;
}
mrb_state *mrubyStart(void *embedData)
{
mrb_state *mrb ;
struct RClass *TestClass ;
mrb_int valInt ;
mrb_value val ;
mrb = mrb_open() ;
TestClass = mrb_define_class(mrb, "TestClass", mrb->object_class) ;
valInt = (mrb_int)(embedData) ;
val = mrb_fixnum_value(valInt) ;
mrb_define_global_const(mrb, "TestConst", val) ;
mrb_define_class_method(mrb, TestClass, "test_method", test_method_c, MRB_ARGS_REQ(1)|MRB_ARGS_ANY()) ;
return mrb ;
}
int main(int argc, char *argv[])
{
FILE *file ;
mrb_state *mrb ;
fpos_t size ;
char *scriptBuf ;
if(argc < 2) {
return -1 ;
}
file = fopen(argv[1], "r") ;
if(file == NULL) {
printf("有効なファイル名ではありません:%s\n", argv[1]) ;
return -1 ;
}
mrb = mrubyStart((void *)12345) ;
mrb_load_file(mrb, file) ;
/*
// 文字列を直接食わせたい場合はこんな感じ
fseek(file, 0, SEEK_END) ;
fgetpos(file, &size) ;
fseek(file, 0, SEEK_SET) ;
scriptBuf = malloc(size) ;
fread(scriptBuf, 1, size, file) ;
mrb_load_string(mrb, scriptBuf) ;
free(scriptBuf) ;
*/
fclose(file) ;
mrb_close(mrb) ;
return 0 ;
}
このプログラムにこのようなrubyスクリプト
puts("in ruby")
TestClass.test_method(0, 1, 2.3, "in c")
を読み込ませると
in ruby
以下に引数を列挙します
最初の引数 0
整数:1
浮動小数点数:2.300000
文字列:in c
引数終了
埋め込まれている数値は12345
こういう結果になります。
#文字コードの話
mruby自身が文字列を認識して、例えば文字列の長さを計算する場合等は基本的にはバイト単位で扱う(従来のC言語的扱い)。
mrbconf.h
にはMRB_UTF8_STRING
という定義があり、これがdefineされていた場合はmrubyは文字列をUTF8として扱おうとするのだけど、例えばmrubyスクリプト自体がSJISで書かれていて、アプリケーションに文字列を渡すAPIを作った場合、文字列はSJISでアプリケーションに渡される。
mruby自身は文字コード変換などは行わないので、SJISのテキストが基本としてまかり通るWindows等では注意が必要になるかもしれない。
mrbgemsを作る方法等は色々なサイトに情報がありますのでそれらを参照して頂くとして、その上で上記の知識を覚えておけばアプリケーションの拡張用の言語としてmrubyを使う事が可能になると思います。