カクタスソフトウェア
カクタスソフトウェア
サウンド MIDI マルチメディア アプリケーション

プラットフォーム呼び出しとマーシャリングに関する話題

このセクションでは、waveOutWrite関数とMessage.GetLParam関数が引き起こす、マーシャリング関連の話題をお届けします。wave関数は、Win32APIの一つで、C#.NET環境から用いるには、マーシャリングが必要になります。他のセクションでも解説している通り、マーシャラーへの指示は、DllImportアトリビュートを用いた宣言で行うことができるのですが、マーシャリングの仕組みを十分理解していないと、思わぬ落とし穴に遭遇する可能性があります。

wave関数

wave関数と言えば、ウェーブデータを再生する伝統的なWin32APIです。そして代表的な使い方にコールバックウィンドウ方式があります。以下にプロセスを示します。

これは、どこにでもありそうなアルゴリズムです。この処理では、デバイスとのやり取りに2つのオブジェクトを用います。WaveHdr構造体とウェーブバッファです。

WaveHdr構造体は、デバイスが作業用として用い、ウェーブデータやループ再生、WaveHdr保存のための情報などが収められます。ウェーブバッファには、ウェーブデータ本体が収められます。

ウェーブバッファ

ウェーブデータを再生デバイスに書き込むため、バッファにウェーブデータをコピーし、そのアドレスをデバイスに知らせます。ウェーブバッファは、再生が終わるまで削除したり移動させたりしてはいけません。

再生デバイスで用いるバッファをマネージ環境で準備するには、ちょっとした工夫が必要です。

マネージ環境でアロケートしたバッファは、再生デバイスに書き込むことができません。これは、マネージ環境では、オブジェクトがハンドルによって管理され、バッファへの参照がデバイスが必要とするバッファアドレスを示しているわけではないからです。

またガベージコレクタは、メモリーの効率的な運用のため、バッファの位置を移動させることがあります。そのため、仮にバッファアドレスを得たとしても、安全に運用することができません。

そこでGCHandleクラスを用いて、ガベージコレクタに移動禁止を指示した領域を作成し、AddrOfPinnedObject関数で得たアドレスをデバイスに書き込みます。このアドレスは、Win32APIに渡すことが可能で、かつガベージコレクタによって移動させられることがありません。

    byte[] waveBuff = new byte[WaveBuffSize];
    GCHandle gch = GCHandle.Alloc(waveBuff, GCHandleType.Pinned);
    waveHdr.lpData = gch.AddrOfPinnedObject();

ただしこのエリアは、ガベージコレクタによって回収されないので、利用が終わったらFree関数で必ず開放しなければなりません。

    // バッファの利用
    gch.Free();

WaveHdr構造体

構造体をWin32APIに渡すためには、マーシャラーへの指示を含めたアンマネージ互換タイプの構造体を定義しなければなりません。

引数として構造体を指定する場合、ほとんどのケースで構造体への参照を渡します。waveOutWrite関数の場合も同様です。ここで指定した構造体はデバイスで利用され、ウェーブデータの取得、フラグの設定など重要な役割を果たします。

構造体を参照渡しした場合、マーシャラーは以下の動作を行います。

ブリッタブルタイプの構造体

構造体のすべてのメンバーがブリッタブルである場合、オリジナルの構造体への参照をWin32APIに渡します。そのためデバイスがフラグを設定するなどした場合、その変更はオリジナルの構造体へ反映されることになります。

waveOutWrite関数で言えば、バッファがキューに登録されたInQueueフラグ、再生が終了したDoneフラグが設定されます。

ブリッタブルでないメンバーを含む構造体

ブリッタブルでないメンバーが構造体に含まれる場合、オリジナルの構造体をWin32APIに渡すことができないため、マーシャラーは、アンマネージ互換の構造体の複製を作成し、それをWin32APIに渡します。

ここでデバイスが受け取るのは、オリジナルの構造体ではないという点に注意してください。そのためデバイスがフラグの設定などを行っても、その変更はオリジナルの構造体へは反映されません。

WaveHdr構造体の場合

幸いWaveHdr構造体は、すべてのメンバーがブリッタブルであるため、アプリケーションは、デバイスが設定したフラグの変更を受け取ることができます。具体的に言うと、バッファがキューに登録されたInQueueフラグ、再生が終了したDoneフラグが設定されます。

これはたまたま、WaveHdr構造体がブリッタブルであるという性質と、マーシャラーがブリッタブルな構造体はコピーを作らない、という仕様が重なった結果であるということです。

WaveHdrクラス

構造体を参照渡しにするためには、「ref」キーワードを付ける必要があります。しかし構造体をクラスとして実装した場合、クラスは常に参照渡しされるため「ref」キーワードは必要ありません。参照渡ししかしないのであれば、クラスとして実装した方が便利です。

しかし「ref」を付けた構造体の参照と、クラスの参照では、マーシャリングの挙動に違いがあることに注意しておく必要があります。「ref」は、マーシャリングの方向がデフォルトで[In,Out]となっているのに対し、クラスの参照は、デフォルトで[In]だけとなっています。

もしクラスがブリッタブルでないメンバーを持つ場合、構造体ではAPIが設定した変更を受け取れるのに対し、クラスでは変更が反映されないことになります。

具体的には、バッファがキューに登録されたInQueueフラグが影響を受けます。

これもまた、WaveHdrクラスがブリッタブル変数のみから構成されるため、フラグの変更を受け取ることができますが、正式には以下のように[In,Out]属性を指定しなければなりません。

    [DllImport("winmm.dll", SetLastError=true, CharSet=CharSet.Auto)]
    public static extern int waveOutWrite(
        IntPtr              hWaveOut,
        [In,Out]WaveHdr     lpWaveHdr,
        int                 uSize);

Message.GetLParam

再生が終了したバッファをアプリケーションへ戻す方法にフォームメッセージがあります。ここではMmWomDoneメッセージがデバイスから送信されます。

    public const int    MmWomOpen  = 0x3BB;     // waveform output
    public const int    MmWomClose = 0x3BC;
    public const int    MmWomDone  = 0x3BD;

    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
        case MmWomOpen:
            break;
        case MmWomClose:
            break;
        case MmWomDone:
            IntPtr hWaveOut = m.WParam;
            WaveHdr waveHdr = (WaveHdr)m.GetLParam(typeof(WaveHdr));
            // 再生終了の処理
            break;
        }
        base.WndProc(ref m);
    }

WParamは、デバイスのハンドルです。そして問題のLParamがWaveHdrへの参照を示しています。

一般的にLParamが何を示しているのか、と言う問題があります。C#では通常WndProcを使う機会はほとんどないと思われます。あるとすれば、今回のようにWin32APIとの連携を行うというシナリオでしょう。この場合、多くのケースで、WParamはIntPtrのハンドル、LParamが作業領域への参照という設定になります。

Win32APIで用いる作業領域は、当然アンマネージ環境の構造体です。それをC#で扱うためには、マネージ環境で使えるオブジェクトに変換する必要があります。

GetLParam関数は、LParamの内容を解釈して指定されたタイプのマネージオブジェクトを作成します。これはLParamが示すオブジェクトをキャストしたものではなく、マネージ側へマーシャリングされた複製です。

別の方法として、MarshalクラスのPtrToStructure関数を使うこともできます。PtrToStructure関数は、IntPtrで示されたアンマネージオブジェクトを、指定されたタイプのマネージオブジェクトへマーシャリングします。以下にコード例を示します。

    WaveHdr waveHdr = (WaveHdr)Marshal.PtrToStructure(m.LParam, typeof(WaveHdr));

GetLParam関数を用いても、PtrToStructure関数を用いても、同様の結果を得ることができます。

マネージ側からアンマネージ側をコールする場合、ブリッタブルな構造体であれば、アンマネージオブジェクトの複製を作る必要はありません。しかし、アンマネージ側からマネージ側へ戻る場合は、構造体がブリッタブルであると分かっていても、マネージオブジェクトを作成しなければなりません。

GetLParam関数で得たWaveHdrは、オリジナルのオブジェクトではないため注意が必要です。例えば、複製されたWaveHdrにwaveOutUnprepareHeader関数で後処理を行っても、オリジナルのPreparedフラグは立ったままとなります。

直接キャストする

今回のケースは、LParamの身元が分かっています。つまり、マネージタイプでかつブリッタブルな構造体がデバイス経由で戻ってきたもの、ということです。そのためLParamが示すオブジェクトは、オリジナルのWaveHdr構造体そのものという推定ができます。

つまりWaveHdrに直接キャストすることができる、と言うことです。

しかし一般的には、IntPtrの構造体へのキャストは危険です。そのため「unsafe」キーワードを付けなければコンパイルが通らないようになっています。今回、GetLParam関数で問題が解決するので、「unsafe」キーワードを付けてまでキャストするメリットはないと思われます。「unsafe」キーワードは、これしか解決策がない場合に限るべきでしょう。安全なマネージコードで代替できるときは、できるだけ「unsafe」キーワードを利用しないことをお勧めします。

WaveHdrの管理

作業に一貫性を持たせるため、GetLParam関数で得たオブジェクトをオリジナルのオブジェクトに置き換えます。オブジェクトの比較は、ユニークであることが分かっている構造体のメンバーを利用します。

オリジナルを放棄し、コピーに置き換える方法も可能です。しかし、再生を強制的に停止させ、バッファを短時間に回収するためには、オリジナルを用いる方が有利です。

アンマネージ環境では、再生デバイスからフォームメッセージ経由でWaveHdrが循環するプロセスが成り立ちます。しかし、マネージ環境では、マーシャリングの壁があるため、常にオリジナルのWaveHdrをアプリケーション側で管理するようにします。

ドキュメントの先頭へ

カクタスソフトウェア 技術協力 資料室 資料室の広場 SourceForge.jp お問い合わせ