書いた人:星一
C# と Ruby を連携させるための (バッド) ノウハウを解説します。 Ruby から .NET の機能を呼び出したりすることが出来ます。
巷では、 IronRuby とか DLR とか色々話題になっていますが、今回の方法はそんなの関係ありません。 IronRuby は絶賛開発中ですが、ここで扱う方法は MatzRuby そのものを使うため、 Ruby の機能を制限なく活用しつつ、今すぐ試すことができます。ただし、拡張ライブラリのようなものが作れるわけではなく、閉じた環境でしか実行できないものになります。
ここで扱う方法は忘れて、将来は素直に IronRuby を使ったほうがいいと思います。
というわけで、 C# で C の関数を呼び出して実行するのがメインになります。
以下のものを準備します。
今回は Windows でのみ動作確認を行いました。 Linux でも Mono を使えば出来るかもしれません。
今回はコンソールアプリケーションで説明します。
名前は別に何でもいいです。ここでは「RubyTest」というプロジェクト名、名前空間であるとして説明します。
Windows 用の Ruby のライブラリを手に入れます。 mswin32 版でも cygwin 版でも mingw 版でも、どれでも OK なはずです1。
今回は mswin32 版の、 1.8.x 系統を使います2。
下図のように、 DLL を追加してやります。
さらに DLL の「出力ディレクトリにコピー」プロパティを「常にコピーする」に設定します。
参照の設定は要りません。
現在 Ruby 1.8.x の、 64bit Windows 向けのバイナリは存在しません。そのため、そのまま C# をコンパイルしてしまうと、 DLL の関数呼び出し時に例外 (BadImageFormatException) が投げられます。デフォルトでは、 C# のコンパイルのターゲットは “Any CPU” となっており、 C の DLL と連携する際、想定する DLL の形式が実行環境依存になってしまうからです。 64 bit 版の Windows で試す場合は、 C# コンパイルのターゲットを 32 bit (x86) 用にして、 32 bit 互換モードで実行させてやる必要があります。
Visual C# Express の場合、
という手順でビルドの詳細が設定できるようになり、ターゲットを設定することが出来るようになります。
Ruby の Hello, World のスクリプトを、 C# から実行させましょう。標準出力 (コンソールに出力) はまだできないので、ファイルに出力します。
Ruby の DLL で定義されている関数を、 C# で使えるように「ポーティング」してあげます。説明するのがだるいので、実例をさっさと挙げてしまいます。
(1): DllImportAttribute を使用するための using です。 DllImportAttribute 属性はメソッドに付加する属性で、外部 DLL で定義されていることを表します。
(2): VALUE は、 Ruby のオブジェクトを一意に表す整数型です。整数型や nil などの特殊な値を除いて、ポインタの値そのものです。
今回使用する Ruby の DLL では VALUE は 32 bit 整数型 (のはず) であり、 C# では int (System.Int32) と同じです。ソースコード上で、 C# の int と、 VALUE の意味での int が混合するのは紛らわしいので、 (2) のようにエイリアスを作ってやります。
(3): DLL のパスを表す文字列定数です。拡張子は要りません。
以下、必要最小限の関数だけ定義してやります。
(4): C で定義された ruby_init() を C# で使う場合、このように記述します。 extern は C# の予約語です。この関数は引数がなく、返り値の型 void なので非常に素直に書けます。こんなのばかりだといいのですが、そういうわけには行きませんね。
(5): rb_eval_string_protect(char *script, int *state) 関数のポーティングです。この関数は、文字列の script を Ruby スクリプトとして評価します。例外発生時に、 state が NULL でない場合に例外オブジェクトの VALUE 値が代入されます。戻り値は評価された Ruby オブジェクトをあらわす VALUE です。
protect のつかない、 rb_eval_string 関数は今回は使いませんでした。 rb_eval_string 関数を使用した場合、スクリプト評価時に例外が発生すると、 C# 側で予期せぬエラーが生じるからです。
C で定義された関数を C# から呼び出すために、 .NET の「マーシャリング」を利用します。マーシャリングとは、 .NET の型とネイティブ型を相互変換する機能です。 C の型と C# (.NET) の型には一定のルールがあり3、非常に複雑ですが、適当に抜粋すると以下のようになります。
C | C# |
---|---|
char | byte |
32 bit 整数 | int |
64 bit 整数 | long |
float | float |
double | double |
ポインタ | ポインタ |
ポインタ | IntPtr 構造体 |
ポインタ | 参照渡し (ref) |
ポインタ | 配列 |
char ポインタ | string |
関数ポインタ | デリゲート |
今回、 char* を byte[] 型にしました。文字コードに自由を効かせたいためです。
「マーシャリング」という言葉は Ruby の Marshal と紛らわしいので、以後「C/C# のデータ変換」と呼びます。
(6): バイトの配列を引数に取るのはちょいと使いづらいので、文字列を引数に取るバージョンを定義してあげます。
C# の string から byte[] にする際、文字列の最後にナルバイトを付加しています。
さっそく “Hello, World!” を出力するプログラムを書いてみましょう。
“Hello, World!” と書かれた hello.txt ファイルが生成されていれば成功です。
標準入力をコンソールに出すためには色々面倒な手順が必要なので、後ほど解説します。
標準出力をコンソールに出すまでの手順を紹介します。
Qnil などの定数を逐一 C# で定義してやる必要があります。全部定義するのは非常に面倒なので、よく使うものだけ定義しておきましょう。必要になったら付け足せばよいのです。
上から順に、 false、 true、 nil を表す値です。
Ruby の型を使って C# 上でごにょごにょやりたい場合は、 T_* 系の定数も定義する必要があります。ここでは割愛します。
VALUE も int も全く同じなので、キャストする必要は全然ないのですが、見やすくするために書いておきます。
Ruby 文字列を C# 文字列に変換するための処理を書きます。 Ruby の関数 rb_string_value_cstr を使用します。ところで、 Ruby 文字列を C 文字列に変換する際には、は StringValuePtr マクロを使うのが普通なのですが、マクロは展開されてしまっているために C# で使用することはできません。
(1): rb_string_value_cstr 関数をポーティングします。この関数は、 Ruby の String オブジェクトの値 (VALUE) から C の文字列を取得します。引数は VALUE * 型であり、 C# では VALUE の参照型 (ref) とできます。返り値は char* 型であり、これは IntPtr 構造体型とします。 IntPtr は、プラットフォーム非依存な、ポインタのラッパーです。
(2): StringValuePtr メソッドを定義します。引数に Ruby の文字列の VALUE をとります。
(3): 内部で rb_string_value_cstr メソッドを使用し、文字列の char * なポインタを取得します。
(4): IntPtr を適当なポインタ型にキャストします。 1 バイト単位で操作するので、ここでは byte * にキャストします。
unsafe ブロックが必要なため、 unsafe が有効になるようにコンパイルしてください。プロジェクトのプロパティの「アンセーフ コードの許可」という項目で設定できるはずです。
(5): NULL バイトが出るまでループを回し、文字列の長さを取得します。 C# は条件式に bool 値しか書けないので、ちゃんと (不) 等式を書く必要があります。
(6): 最終的に、 IntPtr 型の値 (ptr) から指定した長さ分だけ、 byte の配列に変換します。
StringValuePtr メソッドを使う、必要最小限と思われるサンプルを次に書いてみました。コンソールに “hoge” と出力されれば成功です。
Ruby のメソッドを定義するためのインターフェイスとして、以下のような関数が用意されています。
rb_define_method は普通のインスタンスメソッドを定義します。 rb_define_singleton_method は特異メソッドを定義します。
argc は引数の数を表します。
まず、三番目の引数のためのデリゲートを作ります。 C の関数ポインタは引数の型があいまいですが、 C# のデリゲードの型は厳格なので、引数の個数が異なるものは別のデリゲートとして定義してやる必要があります。
引数の数はいくらでも定義できますが4、とりあえずこの程度にしておきます。
引数を見れば分かりますが、最初の値は self (メソッドのレシーバ) で、それ以降の引数が、実際のメソッドの引数となります。
次に、 rb_define_method および rb_define_singleton_method をポーティングします。 argc はデリゲートの型から判別できるため冗長なので、 C# から使う場合は省略できるようにしてしまいます。
上記ソースを見ていただければお分かりの通り、 C の関数を呼ぶ前にデリゲートを static なリスト (MethodDelegates) に保存しています。これはデリゲートが GC によって回収されてしまうのを防ぐためです。 C# (マネージド) のデリゲートは C (アンマネージド) では関数ポインタとなりますが、 C の関数ポインタをコールするときに、対応する C# のデリゲートが存在するかどうかは、 .NET のほうでちゃんと管理する必要があるのです。5。回収されてしまったデリゲートをコールしようとするとエラーになってしまいます。それを防ぐために、デリゲートへの static な参照を保持することにしました。
実際に使う例は、以下の通り。動作させるためには、プロジェクトの参照設定に “System.Windows.Forms” を追加する必要があります。 String クラスにメソッド “show_message” を定義してみました。 Ruby の文字列をダイアログボックスに表示してくれます。なんと日本語も OK です。
いままで、 Ruby スクリプトで puts などの標準出力メソッドを使用しても、コンソールには何も出ませんでした。原因はよくわかりません。リファレンスによると6、Ruby では標準出力を変えるには「この変数 ($>) の値を別の IO に再設定すればよい」ということが分かります。 $> に代入されている値のデフォルト値は定数 STDOUT ですので、今回は STDOUT に write という特異メソッドを上書き定義してやることで、標準出力の挙動を変えることにします。
Ruby 風擬似コードを書くと、以下のようになります。
C# で書いてやると、次の通りになります。
使用例は以下の通りです。
これでやっとこさ標準出力がコンソールに出ました。おめでとうございます。
Fixnum の場合、シフト演算を使って DLL の関数を呼び出さずに済ませる方法があります。そのようにすると、 C/C# のデータ変換のコストがかからないので、処理が高速化されます。ここでは説明しません。
rb_num2int 関数を使います。
マクロの NUM2INT という名前で使いたい! という理由だけでラッパーを作ってみたり。以下同様。
rb_int2inum 関数を使います。
前述の通り、 StringValuePtr を使います。
rb_str_new2 関数をポーティングします。
本来 C では
と定義されており、 C の char * 型を C# byte[] 型としました。この関数のほかに、 string を引数にとるメソッドを別途作りました。最初の rb_eval_string_protect と同じ要領です。
Ruby のオブジェクトのメソッドを呼び出すためには、 rb_funcall 関数を使います。 rb_funcall 関数の定義は以下の通り。
ID は変数名やメソッド名を一意に定める値で、 Symbol のことです。文字列から Symbol を取得するには、 rb_intern 関数を使います。
ID は単なる整数値なので、 C# では int として扱えます。
両方を C# で定義してやると、以下のようになります。
rb_define_class 関数を使います。
rb_define_class 関数は親クラスの指定が必須ですが、 rb_cObject (Object クラスを表す VALUE) が無くて困ることになります。 rb_cObject はグローバル変数ですが、 C/C# のデータ変換ができないのです。 rb_eval_string_protect を使って強引に切り抜けましょう。
実行環境はそのアプリケーション内でのみ限定されます。拡張ライブラリのような使い方はできません。
RPG ツクール XPのように完全に閉じた世界の Ruby のような、特殊な用途ならば大丈夫でしょう。
C/C# のデータ変換は、通常の関数呼び出しに比べて大きなコストがかかります。高速化のためには、以下の工夫が必要でしょう。
Ruby の GC と C# の GC は独立して動きます。 Ruby のオブジェクトと C# のオブジェクトとを関連付ける場合、片方が GC で回収されてしまうなどの不整合を起こさないようにしましょう。
C# 側では連想配列などを使って常に参照を保持するようにし、 Ruby のオブジェクトを回収のタイミングをフックして、 C# のオブジェクトを回収させるようにすればよいでしょう。 ObjectSpace モジュールの define_finalizer メソッドを使いましょう。
本稿では、 C# と Ruby を連携させる方法について述べました。本稿で紹介する方法を使うことによって、 C# における .NET ライブラリの豊富さと、 Ruby のスクリプトの書きやすさの両方のメリットを受けることができます。また、ポーティング作業を通じて、 Ruby の C API やマーシャリング (C/C# データ変換) を学ぶことができます。
しかしながら、 Ruby で .NET のライブラリを使いたいという目的ならば、今後は IronRuby を使ったほうがよいでしょう。
Star Engine というゲームライブラリを開発しております。スーパーファミコン風なノスタルジックなゲームを簡単に作るためのライブラリです。 C# と Ruby の組合せを使っています。
画像操作などの速度が要求される部分は C# で書かれており、ゲームを作る「楽しい」部分は Ruby で書けるようになっています。
C# で書くことによって、 .NET の恩恵をそのまま受けることが出来ました。特に画像処理やフォント周りとかが楽チンでした7。
本稿を書くにあたって、 NyaRuRu さん、 arton さん、ささださん (順不同) にアドバイスをいただきました。この場を借りて厚く御礼申し上げます。
星一。大学院生。色々と面倒くさがり屋。好きなアーティストは Michael Jackson です。
差異は、http://www.garbagecollect.jp/ruby/mswin32/ja/documents/mswin32.htmlを参照。 ↩
ライセンスが GPL ですので、公開する際は注意してください。 ↩
参考: http://msdn2.microsoft.com/ja-jp/library/04fy9ya1(VS.80).aspx ↩
README.EXT.ja によると、 16 個が限界らしい。 ↩
厳密には、 C# のデリゲートと C の関数ポインタを仲介する Reverse P/Invoke thunk というサンクというものが存在し、デリゲートインスタンスごとに実行時生成されます。 C から関数ポインタをコールするとき、実際にはこのサンクが呼ばれます。サンクはデリゲートインスタンスと一対一に対応し、デリゲートインスタンスが回収されたときにサンクも破棄されることになっています。 ↩
組み込み変数: http://www.ruby-lang.org/ja/man/?cmd=view;name=%C1%C8%A4%DF%B9%FE%A4%DF%CA%D1%BF%F4 ↩
SDL を使っていますが、 SDL_ttf などはあまり使いたくなかったんです。 ↩