SyntaxHighlighter

2013年12月15日日曜日

SevenZipRuby 作成メモ 2 - C++ での Ruby 拡張ライブラリのコーディング

SevenZipRuby 作成メモ 2 - C++ での Ruby 拡張ライブラリのコーディング

7z ファイルを読み書きする Ruby gem ライブラリである seven_zip_ruby も、無事に rubygems で公開しました。
ダウンロード数から察するに、あまり必要とされていないような気がしないでもないですが、以下にリンクをはっておきます。

今回は、 SevenZipRuby の作成メモ 2 として、 C++ での Ruby 拡張ライブラリのコーディングについて書いておきます。

概要

C++ での Ruby 拡張ライブラリのコーディングについては、 tueda の日記 - (Cではなく)C++でRubyの拡張ライブラリを作るには などが非常に分かりやすく書かれているので、とても参考になると思います。
また、 seven_zip_ruby では使いませんでしたが、 C++ で Ruby 拡張ライブラリを作成するための Rice というライブラリも公開されており、 Rice を使用して Ruby の拡張機能を C++ で作成するなどのページが参考になると思います。

今回は、私が個人的に気をつける必要があると感じた、例外まわりの実装などについて書いておこうと思います。

Ruby の例外機構

Ruby では、 raise "hoge"raise SyntaxError, "invalid syntax" のように例外を発生させ、それを外側のメソッドなどで begin, rescue, ensure を使って捕捉することができます。

この仕組みは、 MRI (CRuby) の場合、 setjmplongjmp によって実現されています。

setjmplongjmp

これらは C 言語で大域脱出を実現するための機構であり、例えば以下のようなことができます。

上記の例のように、 setjmp が呼ばれると、将来その関数の先で longjmp が呼ばれたときのために、「戻り先」を示すための CPU 依存の情報 (実行コンテキスト) を引数 gBuf に保存します。
その後、 longjmp が呼ばれると、引数で渡された実行コンテキストから、 setjmp を呼んだときの「戻り先」を求め、その状態に戻ります。

とてもとても大雑把に書くと、ネストされた関数内から外側の関数へ一度に移動できる goto のようなものです。
Java にはラベル付き break がありますが、「関数をまたいで使えるラベル付き break 」のようなイメージです。

より詳細な (そして正確な) 説明としては longjmpと例外などがとても分かりやすいです。

しかし、 goto などと異なり、 setjmp, longjmp は C++ で使う際には「デストラクタとの併用が困難」という大きな注意点があります。

C++ と setjmp, longjmp

例えば以下のようなコードを考えます。

この場合、 Constructor, Destructor, Error! と表示され、デストラクタ ~Test() が呼ばれていることが分かります。

一方で、以下のようなコードの場合、デストラクタは呼ばれません。

このように、 longjmp は強制的に setjmp を呼んだときのスタック状態に戻すので、 setjmp, longjmp の間で呼ばれるべきデストラクタは一切呼ばれません。
ここはとても重要なポイントです。

例外を考慮した C++ による Ruby の拡張ライブラリの作成

既に書いたように、 Ruby の例外機構は setjmp, longjmp を使って実現されています。
大雑把に書くと、 Ruby コード中の begin の部分で setjmp され、 raise Hoge の部分で longjmp されています。

そのため、 C++ で Ruby の拡張ライブラリを書く場合、前述したようなデストラクタの呼び出しがされない場合があることに注意が必要です。
例えば、 std::vector なんかを使っていると、簡単にメモリーリークが発生します。

C++ で書く場合は、こういったことを考慮して書く必要があります。
例えば、以下のように書く必要があります。

上記のように書けば、 Ruby の例外による longjmp の範囲を rb_protect で呼び出した sub_function の内部に閉じ込めることができます。

でも、それぞれの Ruby メソッドの呼び出しについて関数を分けるのは面倒です。

ラムダ関数の活用

なので、 C++ のラムダ関数を使ってみることにします。

例えば、以下のような関数を作っておいて、 rb_protect に渡すようにします。

こんな感じのテンプレート関数を作っておくと、あとはラムダ関数を使うことで、割と楽に Ruby の関数を呼び出すことができます。

C 言語と C++ の関数呼び出し規約におけるコンパイラ依存性

厳密には、上記の方法はコンパイラに依存しており、うまく動かないこともあります。

run_protect 関数の中で、 C 言語の関数である rb_protect を呼んでおり、その第一引数として run_functor_for_protect<T> のように C++ の関数ポインタを渡しています。

この部分がコンパイラに依存しており、うまく動かないかもしれない部分です。たぶん。

本来であれば、 C 言語の関数に関数ポインタを渡す場合は、 extern "C" 宣言された関数でなければなりません。

上の例のコードがうまく動くのは、「C 言語と C++ の関数の呼び出し規約が一致しているとき」です。
例えば「C 言語の関数の呼び出し規約は cdecl なのに、 C++ の関数の呼び出し規約は stdcall」みたいなコンパイラがあると、当然うまく動作しません。ただし、そんなコンパイラがあるのかは知りません。

C++ の仕様としては、こういう場合は extern "C" を付けることで、呼び出し規約 (と関数の名前修飾) が C 言語と同じになることが保証されていますが、テンプレート関数には extern "C" を付けることはできません。
名前修飾はともかくとして、呼び出し規約だけは C 言語と同じになるような書き方をしたいのですが、標準の書き方ではなさそうな感じです。

function クラスの利用

そのため、ラムダ関数とともに C++11 で追加された function クラスを使うことにします。

今回の場合では以下のようにすると、コンパイラ依存なコードにならないでしょう。

以上のように、 C++ で Ruby の拡張ライブラリを書くのは、割と面倒です。
seven_zip_ruby のように、呼び出し先の DLL が生の C++ を必要とするような環境でもない限り、 C で書くか、 Rice などのライブラリを使うのが楽でしょう。

2013/12/23 function に関する記述を追加

0 件のコメント:

コメントを投稿