何故か無性にもっともっとインクレディブルマシーン(The Even More Incredible Machineの日本語版、参考:for all incrayers)がやりたくなったので、ここ暫く猿のようにやっていました(ぶっ続けでやっていた訳ではありませんが)。驚いたことに、Windows xpでもまともに動かせるようにしたパッチが作られていたんですね。慣れないWindows Application Compatibility Toolkitを弄くり回しながら、苦難の末折角最適な互換性設定を見つけたというのに……。TIM風に言うなら歯車の再発明というところでしょうか。
それはさておき、今回は前回作ったOggVorbis.dllを.NET環境から呼べるようにします。.NET環境とは言え相変わらずC#だけですが、他の言語でも似たような手法で出来るはずです。
まず、OggVorbis.dllをマネージドアプリケーションから見えるところにコピーしましょう。ユーザが作成したDLLの場合、実行可能ファイルと同じディレクトリに入れておくのが一般的です。クラスライブラリを作ってそこから呼び出す場合でも、クラスライブラリの出力ディレクトリではなく、そのクラスライブラリを使うアプリケーションの実行可能ファイルと同じ階層でなければ駄目です。
では、まず手始めにエラー処理の為に追加したSetError関数を可視化してみましょう。この関数を呼ぶには、次のようなコードが必要になります:
using System.Runtime.InteropServices;
namespace OggVorbis {
public class Functions {
[DllImport( "OggVorbis.dll", EntryPoint = "SetError" )]
public static extern void SetError( int errorOccurred );
}
}
まず、外部の関数はstatic externで修飾しなければなりません。この関数を持つためのラッパクラスを用意して、OggVorbis.Functionsと名付けました。関数のプロトタイプには、普通のマネージド関数と同じように、戻り値と引数の型を指定してやります。今回は戻り値は無く(void)、int型の引数を持っていたので、上のようになります。VC++ 2005では、__w64をつけない限り常にintは4バイトですので、C#でもint(Int32のエイリアス、4バイト)を使います。
プロトタイプには、DllImportAttributeを付けます。必須の引数としてDLL名、他にオプションとしてエントリポイント名、呼び出し規約などを指定できます。エントリポイント名は、上記のようにマネージド関数の名前と完全に一致する場合には付ける必要はありません。
さて、SetErrorの場合は簡単でしたが、ここからは少し骨が折れます。他のエクスポート関数では、include\vorbis\vorbisfile.hのOggVorbis_File構造体が使われているからです。普通にvorbisfile APIを使う分にはそれ程律儀にやらなくてもいいのですが、ここでは敢えて律儀にやります。
手始めに、SetErrorの次に簡単なov_clear関数を可視化します。
namespace OggVorbis {
public class OggVorbisFile {
// ……
}
public class Functions {
[DllImport( "OggVorbis.dll", EntryPoint = "ov_clear" )]
public static extern int Clear(
[In, Out] OggVorbisFile oggVorbisFile );
// ……
}
}
ov_clearを呼び出す為の関数として、OggVorbis.Functions.Clearを作りました。ov_clearのプロトタイプはextern int ov_clear( OggVorbis_File* vf );
となっているので、OggVorbis_File構造体に相当するものとしてOggVorbisFileクラスを定義します。…が、少しこのプロトタイプは奇妙です。CではOggVorbis_Fileへのポインタでしたが、C#ではクラスの前にInAttributeとOutAttributeがついています。Inは、このパラメータの値(OggvorbisFileへの参照)が外部関数に渡されるべきであることを示し、Outは外部関数からこのパラメータに書き込まれた値をC#側で読み取れるようにすることを指示します。つまり、[In, Out]とやるとC#のrefキーワードに相当しますね。そして、OggVorbisFileへの参照は、ov_clearの呼び出し時にアドレスへと変換されます。マネージドオブジェクトはメモリの中で常に位置がロックされているという保証はありませんが、プラットフォーム呼び出しの間はずっと固定されていることが保証されます。……ちなみに、ov_clear関数がパラメータの値そのものを書き換えることは無いでしょうが、ov_clear( OggVorbis_File* const vf );
となっていないので、念の為OutAttributeもつけておきました。
さて、ov_clearを呼び出すClear関数の方は準備できましたが、肝心のOggVorbisFileクラスの方は準備が出来ていません。vorbisfileのOggVorbis_File構造体を受け渡しするのですから、当然それに対応したクラスである必要があります。と思ってvorbisfile.hを見てみると:
typedef struct OggVorbis_File {
void* datasource;
int seekable;
ogg_int64_t offset;
ogg_int64_t end;
ogg_sync_state oy;
int links;
ogg_int64_t* offsets;
ogg_int64_t* dataoffsets;
long* serialnos;
ogg_int64_t* pcmlengths;
vorbis_info* vi;
vorbis_comment* vc;
ogg_int64_t pcm_offset;
int ready_state;
long current_serialno;
int current_link;
double bittrack;
double samptrack;
ogg_stream_state os;
vorbis_dsp_state vd;
vorbis_block vb;
ov_callbacks callbacks;
} OggVorbis_File;
……とあります。結構量が多いです。しかも、ogg_sync_stateとか、vorbis_dsp_stateとか、構造体がメンバに含まれています。細かいことを捨てて楽をすることも出来るのですが、飽くまでも律儀に模倣することにします。
[StructLayout( LayoutKind.Sequential )]
public class OggVorbisFile {
IntPtr datasource;
Int32 seekable;
Int64 offset;
Int64 end;
OggSyncState oy;
Int32 links;
IntPtr offsets;
IntPtr dataoffsets;
IntPtr serialnos;
IntPtr pcmlengths;
IntPtr vi;
IntPtr vc;
Int64 pcm_offset;
Int32 ready_state;
Int32 current_serialno;
Int32 current_link;
Double bittrack;
Double samptrack;
OggStreamState os;
VorbisDSPState vd;
VorbisBlock vb;
OggVorbisCallbacks callbacks;
}
まず、ポインタ型は全てIntPtrで置き換えます。64ビットマシンではIntPtrは8バイトになるので、4バイト固定のC#のintなどで置き換えないように。次に、ogg_int64_tは文字通り64ビット符号付き整数なので、Int64を使います。また、VC++のintやlongは4バイトです──C#のlongは8バイトなので、間違えないでください。うっかり勘違いしないように、敢えてエイリアスを使わずビット数が判るようにしてみました。その他、OggSyncStateやOggStreamStateはこの後で定義する構造体です。クラスでは参照にしかならないので、領域をきちんと確保できません。構造体ならC/C++と同じように場所を取ってくれます。
そして、頭のStructLayoutAttribute。このクラスのメンバが書いた通りに並ぶように、LayoutKind.Sequentialを指定しています。これを書かないと、アライメント調整によって生まれるパディングを極力減らす為に、メンバの並び替えを勝手にやってくれる(やってしまう?)ようです。C/C++の配置と全く同じようにする為に、きちんと書いておきます。
ちなみに、普通C/C++コンパイラでは、CPUがデータに効率よくアクセスできるように、2バイトのメンバは2バイト境界に、4バイトなら4バイト境界に、8バイト以上は8バイト境界に配置します。C#のコンパイラも同様のことをします。この為、構造体メンバのアライメント調整をどうするかの設定がずれていると正しくメンバにアクセスできず、パディングを掴まされてしまいます。ただ、今回の場合、OggVorbis.dllをビルドする時に構造体メンバのアライメント(/Zp)を既定値のままにしましたし、.NETでもその既定値は同じ8バイトですので、メンバを書き順通りに並べること以外に気にする必要はありません。詳しく知りたい人は、/Zp (構造体メンバの配置) (C++)やalign (C++)辺りを読んでみて下さい。
ま、今回の場合アライメントは「ちなみに」でいいです。アンマネージド関数と構造体をやりとりする時にはとりあえずLayoutKind.Sequential、と、それだけ。次行きましょう。
[StructLayout( LayoutKind.Sequential )]
private struct OggSyncState {
IntPtr data;
Int32 storage;
Int32 fill;
Int32 returned;
Int32 unsynced;
Int32 headerbytes;
Int32 bodybytes;
}
ogg_sync_stateに相当するOggSyncStateです。ちゃんとOggVorbisFileの中に実体を持たなければならないので、structで宣言します。これは場所稼ぎ(領域確保)の為のものでしかないので、構造体ごとprivateでいいでしょう。次はちょっと特殊です。
[StructLayout( LayoutKind.Sequential )]
private struct OggStreamState {
IntPtr body_data;
Int32 body_storage;
Int32 body_fill;
Int32 body_returned;
IntPtr lacing_vals;
IntPtr granule_vals;
Int32 lacing_storage;
Int32 lacing_fill;
Int32 lacing_packet;
Int32 lacing_returned;
OggStreamStateHeader header;
Int32 header_fill;
Int32 e_o_s;
Int32 b_o_s;
Int32 serialno;
Int32 pageno;
Int64 packetno;
Int64 granulepos;
}
[StructLayout( LayoutKind.Sequential, Size = 282 )]
private struct OggStreamStateHeader {
IntPtr header;
}
ogg_stream_state用のOggStreamStateですが、OggStreamStateHeaderという構造体がおまけで付いています。これを使っているOggStreamStateのメンバheaderは、include\ogg\ogg.hで次のように定義されています:
// ……
unsigned char header[282];
// ……
見ての通りです。C#の配列は、配列コンテナが参照型である為、そのままでは282バイトOggStreamStateの中に埋め込むことが出来ません。そこで、282バイトの大きさを持つOggStreamStateHeader構造体を作り、この構造体をメンバに持つことで282バイトの領域を確保します(地道にlong header1; long header2;……なんてやってられませんしね)。ちなみに、このようにSizeフィールドを使えば一々コピーして行かなくても手っ取り早く領域を確保できますが、個々のメンバにアクセスするのは面倒になりますし、サイズも予めC/C++でsizeof演算子を使って調べておく必要があります。ここまで見てきてもう面倒くさいって人はOggVorbisFileをこの手法で確保してしまえばよいですが、ここに挙げるクラスや構造体をそのままコピー&ペーストするのと手間は大して違わないでしょうか。まぁ、次です。
[StructLayout( LayoutKind.Sequential )]
private struct VorbisDSPState {
Int32 analysisp;
IntPtr vi;
IntPtr pcm;
IntPtr pcmret;
Int32 pcm_storage;
Int32 pcm_current;
Int32 pcm_returned;
Int32 preextrapolate;
Int32 eofflag;
Int32 lW;
Int32 W;
Int32 nW;
Int32 centerW;
Int64 granulepos;
Int64 sequence;
Int64 glue_bits;
Int64 time_bits;
Int64 floor_bits;
Int64 res_bits;
IntPtr backend_state;
}
ogg_dsp_state用です。大したことは無いですね。次。
[StructLayout( LayoutKind.Sequential )]
private struct VorbisBlock {
IntPtr pcm;
OggPackBuffer opb;
Int32 lW;
Int32 W;
Int32 nW;
Int32 pcmend;
Int32 mode;
Int32 eofflag;
Int64 granulepos;
Int64 sequence;
IntPtr vd;
IntPtr localstore;
Int32 localtop;
Int32 localalloc;
Int32 totaluse;
IntPtr reap;
Int32 glue_bits;
Int32 time_bits;
Int32 floor_bits;
Int32 res_bits;
IntPtr internal_p;
}
[StructLayout( LayoutKind.Sequential )]
private struct OggPackBuffer {
Int32 endbyte;
Int32 endbit;
IntPtr buffer;
IntPtr ptr;
Int32 storage;
}
vorbis_blockです。oggpack_buffer用のOggPackBufferがおまけで付いてきますが、それくらいなもの。これで、構造体の定義は大体終わりました。