mpg123はコンソールで走るMPEGオーディオデコーダですが、その構成要素にlibmpg123というのがあります。今回はこいつを.NETからプラットフォーム呼び出しして使えるように、Win32 DLLを作ってみます。と言うよりは、自分で作ってみた時に困ったり躓いた点のメモです。
実は既に用意されている
ダウンロードページにlibmpg123 Win32 DLLっていうのが親切にも置いてある。が、MinGWでの使用を想定して作られたものなので、今回はこれでは不十分。面倒だけど自分で作る。
コンパイラ
結論から言うと、VC++では無理っぽい。やろうとしても、環境特有のヘッダとか定数とか型とか設定とか嫌と言うほど出てきて、やる気にならない、と言うか、無理。お勧め通りMinGWのgccを使う事にする。
MinGWのインストール
MinGWとMSYSをインストールする。MinGW + MSYS による Win32 な OS での開発が参考になる。gccが動くかどうかテストする時、Vistaだとcc1.exeが見つからないって言われるけど、適当にパス通せば良い。例えば、/etc/fstabに
C:/MinGW/libexec/gcc/mingw32/3.4.5 /mingwlib
を追加しておいて、/etc/profileに
export PATH="$PATH:/mingwlib"
を書いておくとか。ここまでしたら、MSYSのシェルでgccがちゃんと動くようになる。
libmpg123のダウンロード
SourceForgeから最新版を落としてくる。ここではそれを解凍して、できたディレクトリの内容を~/mpg123の中にコピーしたとして話を進める。
呼び出し規約の変更
なんだかやっぱりデフォルトの呼び出し規約はstdcallじゃないっぽい(cdeclでもないらしい? 未確認)ので、プラットフォーム呼び出しする関数と、少なくともアンマネージドコードから呼び出されるコールバック関数については__stdcallを付けて回る。具体的には、~/mpg123/src/libmpg123/mpg123.h.inのプロトタイプ宣言と、同ディレクトリのmpg123.cの関数本体の定義を探し回って__stdcall
を書き加えていく。mpg123_replace_readerを使う場合は、それの引数(勿論r_readとr_lseekだけ)に加えて、reader.hのreader_data構造体のメンバ、r_read、r_lseek、read、lseek、それとreaders.cのposix_readとposix_lseekもstdcallを使うようにしておく。
隠れた曲者CCALIGN
詳細は後述するが、(MinGW上のgccでのみDLLを利用するのでない限り)CCALIGNを1にしてはいけない。~/mpg123/src/config.hはconfigureさんが作ってしまうので、彼自身を編集する。~/mpg123/configureの、14767行目辺りのccalign="yes"
をccalign="no"
に変更する。その下のresult: yesもresult: noにしておいた方が気持ちいいかも。修正後はその近辺は次のようになる。
ccalign="unknown" if test $ccalign = "unknown"; then { echo "$as_me:$LINENO: checking __attribute__((aligned(16)))" >&5 echo $ECHO_N "checking __attribute__((aligned(16)))... $ECHO_C" >&6; } ccalign="no" echo '__attribute__((aligned(16))) float var;' > conftest.c if $CC -c -o conftest.o conftest.c; then ccalign="no" { echo "$as_me:$LINENO: result: no" >&5 echo "${ECHO_T}yes" >&6; } else { echo "$as_me:$LINENO: result: no" >&5 echo "${ECHO_T}no" >&6; } fi rm -f conftest.o conftest.c fi if test $ccalign = "yes"; then cat >>confdefs.h <<\_ACEOF #define CCALIGN 1 _ACEOF fi
これでconfig.hに#define CCALIGN 1
は現れなくなる。
make libmpg123.dll
ここまで来たら、あとはmakeするだけ。~/mpg123/makedll.shを実行すれば、時間はかなり(冗談じゃない程)かかるものの一発でDLLを作成してくれる。デバッグ版を作りたい場合は--enablge-debug=yesをオプションとして付けるといい。makeが終わったら、Hintsにあるようにstrip --strip-unneeded libmpg123-0.dll
しておくと軽量化できる。そういう訳で、DLLは~/mpg123/libmpg123-0.dllとして得られるのだけれど……0って何だ。
おまけ : デバッグ版
gccでデバッグ版DLLを作っても、VC++やVisual Studioじゃデバッグできない、それじゃ--enable-debug=yesしても意味無いじゃん。それはそうなんだけど、DEBUGが定義されていると、libmpg123は標準エラー出力に色々と進捗状況を吐き出してくれる。何も無いよりは随分ましだろう。尤もプラットフォーム呼び出しをする場合標準エラー出力はどっかに飛んでいってしまう(のか、どうだか?)ので、そのままではこれも意味が無い。更に、.NETのSystem.Console.SetErrorでリダイレクトっぽい事をしようとしてみても、してくれない。
それじゃあlibmpg123側でリダイレクトしてしまえ、と言う事で、libmpg123.cのmpg123_initの頭でこうやっておく。
#ifdef DEBUG freopen( "./stderr.log", "w", stderr ); #endif
律儀にfreopenの戻り値をグローバル変数か何かに取っておいて、mpg123_exitでfcloseしてもいい。ちなみに、--enable-debug=yesオプション付きでmakeしたDLLは、stripするとかなり痩せる。どうせgdbは使えないんだし、痩せさせてしまえ。
そのおまけ : stderrオートフラッシュ
stderrはバッファリングされなくても、再割り当てしたファイルストリームはバッファリングされてしまって、AccessViolationExceptionとかで正常にfcloseされなかった時にstderr.logに何も入ってなかった、じゃ悲しいので、面倒だけど毎回バッファをフラッシュしてやる事にする。幸いlibmpg123のエラー出力は~/mpg123/src/libmpg123/debug.hにまとまっているので、そのマクロ集を次のように変えてやればいい。
#define debug(s) do{ fprintf(stderr, "[" __FILE__ ":%i] debug: " s "\n", __LINE__);\ fflush( stderr ); }while(0) #define debug1(s, a) do{ fprintf(stderr, "[" __FILE__ ":%i] debug: " s "\n", __LINE__, a);\ fflush( stderr ); }while(0) /* ...... */
fflushを付けてブロックにしただけ。do-whileもくっついてるけど、これはereturnとの付き合いの結果。
CCALIGN?
CCALIGNを1にしておくと、適宜アライメントをしてくれる。gccはアライメントを指定する修飾子に__attribute__((aligned(x)))
というのがあるようで、これを使えば変数をxバイト境界に乗っけてくれるようだ。libmpg123のmakeでは、CCALIGNが1と定義されていれば、この修飾子を使ってアライメントが必要な変数の位置を調整する。
そんなものが使われているという事は使わないといけないようなものがあるからで、例えばSSE命令がメモリにアクセスする場合、普通はメモリ上のデータは16バイト境界に乗っていないといけない。らしい。libmpg123も最適化デコーダの一つにSSEデコーダを持っていて、mpg123_newのdecoderに"sse"を渡せば使ってくれるし、NULLを渡した場合でも、利用可能な範囲内でSSEデコーダが一番効率がいいと思えば暗黙裏に使ってくれる。
そんな訳で、gccは__attribute__((aligned(x)))
が使えるので、放っておけば良さそうなものだけれど、実は問題がある。どうも、gccで作ったDLLをgccから利用した場合はアライメントは保たれたままのようなのだが、gccで作ったDLLのアライメント情報は、他の環境では理解されないようなのだ。VC++然り、CLR然り。この辺って処理系依存なのか。VC++にも似たような__declspec(align(x))
という修飾子があるけど、Microsoft Specificだ。で、運が悪いと、__attribute__((aligned(16)))
で修飾されたデータのアドレスが16バイト境界からずれて(実際、AccessViolationExceptionが投げられていた時には4バイトずれていた)、movapsが駄々をこねる……。
いや、そう、実は駄々をこねるのはmovapsだけであって、libmpg123では、他のSSE命令はメモリに直接アクセスしないように書かれている。CCALIGNがオプショナルである事が何よりの証拠。CCALIGNが定義されない場合は、アライメントを行う代わりに、16バイト境界に乗っていなくても文句を言わないmovupsを使ってくれる。パフォーマンスは多少落ちるかも知れないけれど、_alligned_mallocを使って動的に領域をlibmpg123の外から与えたりするのはちょっと暴挙に過ぎるし、何より一番簡単だろう。