前回の通り、ここ数日Ruby専用DLLである「拡張ライブラリ」なんてものに手を出しているわけですが
割と呆然とする問題に遭遇したので書いてみる。
djpeg.soのコードを弄っていたら突然「Segmentation fault」に遭遇。
コードミスかと思ったがどうにも原因が掴めない為、最小再現コードを書いてみることに。
#include "ruby.h"
#include <stdio.h>
__declspec(dllexport) void Init_djpeg(void) {
FILE *fp = fopen("bug_test.txt","w");
fclose(fp);//->[BUG] Segmentation fault
}
で、上記のコードが最小再現コード。
仮にエラーするとしたらfopenに失敗してfpがNULLで、それをfcloseしようとして~という場合が考えられますがそうではありません。
百聞は一見に如かず、ということで説明する前にこちらもご覧ください。
#include "ruby.h"
#include <stdio.h>
__declspec(dllexport) void Init_djpeg(void) {
FILE *fp = fopen("bug_test.txt","w");
rb_w32_fclose(fp);
}
これは最小再現コードとまったく同じ機械語にコンパイルされます。
つまり、fclose関数が得体の知れぬrb_w32_fcloseなる関数に置き換わり、さらにその中でエラーしている、というわけです。
上記のような気味が悪い現象に陥っているのはruby.hから#includeされているwin32/win32.h中で「#define fclose(f) rb_w32_fclose(f)」されているせい。
自分の常識では標準ライブラリ関数を#defineするなどまともなプログラマーがする事とは思えませんが、今は置いておいて話を進めます。
で、肝心のrb_w32_fcloseの実態ですが、驚くべき事にどこにも見当たりません。
msvcrt-ruby18.libとのリンクを外すとリンクエラーしたのでこの中にあるか、もしくはmsvcrt-ruby18.libと関連づいているdllファイルの中にあるようです。
ですが、通常この実装はきわめて危険です。
なぜなら、標準ライブラリ関数の実装は仕様で定められているわけではなく、いつ変わってもおかしくないからです。
つまり、コンパイラが変われば動作が変わることもありえます。
なので基本的に標準ライブラリ関数をDLL中で使わない、使うにしても内部で完結させて使う形にするのは必須であり、リンク元と連携する事を前提に専用ハンドルを渡しあうなど正気の沙汰ではありません。
最低限、コードに出して一緒にコンパイルするか、FILEやfopenなどといった関連部分全てを置き換えるかするべきでしょう(どちらも危険である事は変わりませんが)
というわけで、libをコンパイルしたコンパイラと手元のコンパイラでfcloseの実装が同じ事を前提として組まれたライブラリによって起きた問題でした。
ruby本体のソースコードまであさったわけじゃないのでrb_w32_fcloseが具体的になにをやっているのかは知りませんが、fopenやfwriteを置き換えていないところを見るに動作とは関係ないもののようなので無視してコードを組むことにします。
ちなみに、「#include “ruby.h”」後に「#define rb_w32_fclose(f) fclose(f)」と書いてやればfcloseを元に戻せます。
不安ならwin32.hから直接該当のコードを削除してしまっても構わないでしょう。
うーん、rubyのこの辺はまっとうなメンテナついてないのかなぁ