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

マーシャリング

2つのシステムがそれぞれ異なるデータ形式を持つ場合、一方から他方へデータを伝えるには、相手側に分かるデータ形式に変換しなければなりません。異なるシステムにデータを伝えるため、データ形式を変換する操作をマーシャリングと言います。一般的に異なるシステム間で関数呼び出しを行う場合、引数のマーシャリングが必要になります。

マネージ環境とアンマネージ環境

本書が想定しているマーシャリングはマネージ環境とアンマネージ環境との間で発生します。

マネージ環境とはCLR(Common Language Runtime)によって管理された環境という意味です。CLRとは.NET環境でガベージコレクションやセキュリティ機能など、.NETフレームワークの基本的機能を提供するプログラム実行エンジンです。マネージ環境ではメモリー管理はCLRが行います。配列や文字列の長さはシステムが管理します。使用済みメモリーはガベージコレクション機能で自動的に回収・再利用されます。

一方アンマネージ環境は、文字通りマネージ環境ではない環境を言います。従来のC/C++はアンマネージ環境で動作します。メモリーの管理方法はマネージ環境とアンマネージ環境で根本的に異なります。C/C++では、文字列や配列そして構造体などは、メモリーに割り当てられた一領域でしかありません。バッファがオーバーフローしないように管理するのはプログラマーの仕事です。ガベージコレクションの機能がありませんので、使い終わったメモリーはプログラマーの責任で解放しなければなりません。

マネージ環境とアンマネージ環境では上記のようにメモリー管理の方法が全く異なるため、両環境を横断する関数呼び出しには引数のマーシャリングが必要となります。

アプリケーションをC#で書くとこでマネージ環境の恩恵を受けることができるわけですが、残念なことに多くのマルチメディア関連APIは、マネージ環境を想定した作りにはなっていません。アンマネージ環境のAPIで代表的なものにWin32APIのようなプラットフォーム呼び出しや、DirectShowなどで用いるCOMオブジェクトがあります。マーシャリングの手間はC#を選んだことのトレードオフということになります。

方向属性

マーシャリングには、こちらの情報を相手側へ伝える方向と、相手側の情報をこちらへ伝える2つの方向があります。関数を呼び出す時、こちらの形式を相手側の形式にマーシャリングすることを[InAttribute]属性と呼び、値が戻る時、相手側の形式をこちらの形式にマーシャリングすることを[OutAttribute]属性と呼びます。

[InAttribute]を[In]、[OutAttribute]を[Out]と省略することが可能です。ソースコードでは省略形を用いることが一般的です。

一般的な引数は、[In]属性を持ち、関数の戻り値は[Out]属性を持ちます。ポインターを渡して、相手に値をセットしてもらうケースは[Out]属性となります。こちらで設定した値を相手側で参照できると共に、相手が変更した値をこちらでも参照できるようにするには、[In]と[Out]を同時に持つ[In,Out]属性を設定します。

マーシャラー

マーシャリングを担当するのがマーシャラーと呼ばれるプログラムです。マーシャラーは関数を定義したテンプレートを元に、型の変換コードをプログラムに埋め込みます。引数が正しくマーシャリングされるために、マーシャラーへの指示を適切に行う必要があります。マーシャリングが必要な変数は変数定義の前に [ ] カッコを付け、カッコ中に方向属性および変数の型を指定します。マーシャラーへの指示については後のセクションで詳しく取り上げます。

マーシャリングが必要な場面

マーシャリングが必要になる場面は以下の2つです。

プラットフォーム呼び出し

マネージ環境からネイティブDLL内のAPIを呼び出す機能をプラットフォーム呼び出しといいます。ネイティブDLLはCLRの管理下で動作しない旧来からあるプログラムコードでアンマネージ環境で動作します。マルチメディア関連の多くの機能がネイティブDLLとして実装されているため、プラットフォーム呼び出しは特に重要です。

ネイティブDLL内にどのようなAPIがあるのかC#コンパイラーは知りません。そこでプラットフォーム呼び出しを行うためにAPIの仕様を公開する必要があります。当該APIがどのDLLに含まれているか、関数の引数や戻り値がどのように定義されているか、などをテンプレートの形でコンパイラーに示すことを仕様を公開すると言います。テンプレートにマーシャリングの必要な型が含まれる場合は、マーシャラーへの指示を行わなければなりません。

プラットフォーム呼び出しでは、コールバックを除いて、アンマネージ環境からマネージ環境側への呼び出しはありません。

COMとの相互運用

COM(Component Object Model)とはプログラムを部品化し、プログラムの作成や再利用を容易にする技術です。COMはアンマネージ環境で動作するため、マネージ環境からCOMを扱うには、マーシャリングを含めCOMとの相互運用についての知識が必要です。

COMで作成された部品をCOMコンポーネントと呼びます。COMコンポーネントはインターフェースと呼ばれるAPIの定義セットを公開します。外部プログラムはインターフェースを通してCOMコンポーネントの機能を利用することができます。マーシャリングに関連して言えば、インターフェースの公開が作業の中心となります。

COMとの相互運用では、マネージ環境からアンマネージ環境、そしてアンマネージ環境からマネージ環境への呼び出しの可能性があります。

マルチメディア関連の機能で重要なものにDirectShowがあります。本書ではDirectShowを用いたアプリケーション作成について詳しく解説しています。本書では重要と思われるインターフェースについて公開用コードを用意しましたが、それでもすべてに対応しているわけではありません。本書にない機能を利用したいと思われる読者は、ここで紹介した知識を元にインターフェースを公開するコードを作成して下さい。

DirectShowは元々DirectXの一部でした。DirectXのいくつかの機能、例えばDirectSoundやDirectDrawなどはマネージ環境へ移植されているため、マネージ環境からそれらの機能を使うことができます。しかしDirectShowのマネージ環境への移植は中止され、マネージ環境から用いるには相互運用の手法を用いるしかありません。現在ではDirectShow自体が保守フェーズに入っており、新たな開発は非推奨となっています。それでもなお、マルチメディアファイルを扱うAPIとして魅力を失っていないため、本技術文書では、.NET C#を用いてDirectShowを利用する方法について解説しています。

マーシャリングが必要な変数

マーシャリングが必要な変数が登場する場面は以下の2つです。

関数の引数および戻り値の定義

プラットフォーム呼び出しおよびCOMのインターフェースで、関数のテンプレートを作成する場合、引数および戻り値の型にマーシャリングが必要な場合があります。

構造体、およびクラスのメンバーの定義

プラットフォーム呼び出しおよびCOMのインターフェースでは、しばしばパラメータを格納するための構造体を必要とします。構造体のメンバーにマーシャリングが必要な型が含まれる場合があります。

変数の型

運用される変数の型を紹介します。

ここでは型について簡単な説明に留めます。詳しい説明は後のセクションを参照して下さい。

ブリッタブル型

2つのシステムで表現が同じため変換する必要がない型をブリッタブル型といいます。ブリッタブル型はマーシャリングの必要がありません。当然マーシャラーへの指示も必要ありません。代表的なものにintやdoubleなどがあります。

Boolean型

Boolean型は2値しか取らない型ですが、C/C++とC#で実装と運用方法が異なるためマーシャラーへの指示が必要です。

文字列型

C#では文字列はUnicodeで表現されますが、C/C++ではAnsiであったりUnicodeであったり様々です。またC/C++では文字列を表現する方法が複数あるため、個々の変数に対応したマーシャラーへの指示が必要です。

配列型

C/C++の配列は単にインスタンスが数珠繋ぎになったものですが、C#の配列はシステム(CLR)が管理しているもので、構造に互換性がありません。配列の使用にはマーシャラーへの指示が必要です。

構造体型

マネージ環境とアンマネージ環境の構造体には互換性がありません。構造体を相互に運用するために、マネージ環境では特にC/C++互換の構造体を定義します。構造体のメンバーにマーシャリングが必要な型が含まれる場合、マーシャラーへの指示が必要です。構造体は値型の変数ですが、参照型として扱うために代替クラスとして実装する方法もあります。詳しくは構造体のセクションで扱います。

デリゲート型

デリゲートとはインターフェースや関数へのポインターのC#流の呼び名です。COMとの相互運用のセクションで詳しく扱います。

マーシャラーへの指示

注目している変数の型がマーシャリングを必要とする場合、変数定義の前段にマーシャラーへの指示を記述します。記述方法は [ ] カッコを付け、その中に方向属性、そして変数の型を記述します。

以下にC/C++関数をC#に公開するサンプルを示します。次の関数はBoolean型の引数を1つを取り、intを戻します。

int MyFunc(BOOL bFlag);
int MyFunc([In, MarshalAs(UnmanagedType.Bool)] bool flag);

[ ] カッコの中を注目して下さい。左側から方向属性を示す[In]を、カンマで区切りながら、引数がBoolean型であることを示す[MarshalAs(UnmanagedType.Bool)]を指定しています。

キーワード[In]は変数を呼び出し元から相手側に伝えるときのみマーシャリングするという指示です。キーワード[MarshalAs(変数の型)]でマーシャリングする変数の型を指定します。この例ではマネージ環境C#のbool型が、アンマネージ環境C/C++のBOOL型に変換されます。

戻り値のintはマーシャリングする必要がないのでそのまま記述します。

以下にアンマネージメント型を示すUnmanagedType列挙体をいくつか紹介します。ここに示した例は一部です。また正しく運用するには追加情報を必要とする場合があります。詳しい使い方は後のセクションで解説します。

UnmanagedType C/C++側で対応する型
Bool Boolean型
LPStr Ansi文字列
LPWStr Unicode文字列
LPStruct 構造体へのポインター
ByValArray 固定長の配列
ByValTStr 固定長の文字列
Struct VARIANT型
IUnknown COMのIUnknownへのポインター

引数の処理

引数を伴った関数やメソッドを呼び出す場合、引数はスタックに設定された後、呼び出しが実行されます。引数にマーシャリングが必要な場合、マーシャラーは以下の処理を行います。

引数には値のコピーをスタックに設定する値型と、引数を納めた領域への参照(領域の先頭アドレス)をスタックに設定する参照型の2つがあります。

[In]属性を持つ引数の場合

引数が値型の場合は、マーシャリングされたデータが直接スタックに収められます。

参照型の場合は、マーシャリングされたデータがいったんマーシャラーが確保した別の場所に納められ、そこへの参照がスタックに収められます。スタックが示す変数はオリジナルではなく、マーシャラーが複製したコピーであることに注意して下さい。つまり、呼び出し先の関数が引数の内容を変更したとしても、オリジナルの変数は変更されないということです。

ただし引数がブリッタブル変数の場合、オリジナルの変数が変更されてしまう場合があります。ブリッタブル変数のセクションを参照して下さい。

[Out]属性を持つ引数の場合

関数の戻り値はマーシャリングが行われた後、変数に代入されます。

[Out]属性の付いた変数の場合、まずマーシャラーは相手関数が利用する作業領域を確保します。領域への参照はスタックを通して相手側に伝えられます。呼び出し先の関数が作業領域にデータをセットします。関数から戻ったら、マーシャラーは戻されたデータをマーシャリングしオリジナル変数へ代入します。最後に確保した作業領域を開放します。マーシャラーは自身のために確保したメモリーは必ず開放するので、作業領域についてプログラマーは意識する必要はありません。

[In,Out]属性を持つ引数の場合、以下の処理が行われます。

呼び出す前に[In]属性の処理が行われ、関数から戻ると[Out]属性の処理が行われます。

C#のoutとref

C#の文法には変数の参照を表すキーワードとしてoutとrefがあります。どちらも変数への参照を相手側へ伝えますが、運用法が異なります。refは呼び出し元と相手側双方で変数を共有するときに用い、outは変数の入れ物を相手側に伝え値を戻すときに使います。

outとrefはC#コンパイラーへの指示ですが、変数の扱いについてマーシャリングとも関連しています。以下の例は、intへのポインターを相手側へ伝える例です。C/C++ではC#で言うoutとrefの区別がありませんが、この関数をC#へ公開する場合には、変数の方向属性を意識する必要があります。

C/C++側のテンプレート

int MyFunc(int* pHoge);

C#側のテンプレート(値を戻す運用)

int MyFunc([Out] out int hoge);

C#側のテンプレート(値を共有する運用)

int MyFunc([In,Out] ref int hoge);

C/C++のヘッダーで方向属性を示すためにコメントが挿入されている場合があります。これらのコメントはC#へ移植するときの参考になります。以下に例を示します。

virtual HRESULT STDMETHODCALLTYPE QueryInternalConnections(
    /* [out] */ IPin **apPin,
    /* [out][in] */ ULONG *nPin) = 0;

方向属性の省略

変数にoutやrefが付けられている場合、マーシャラーはデフォルトで以下の解釈を行います。

  書式 マーシャラーの解釈
無印
int MyFunc(int hoge);
int MyFunc([In] int hoge);
out
int MyFunc(out int hoge);
int MyFunc([Out] out int hoge);
ref
int MyFunc(ref int hoge);
int MyFunc([In,Out] ref int hoge);

もしデフォルトの解釈と期待するマーシャラーの動作が異なる場合には、必ず方向属性を指定して下さい。以下に方向属性が省略できないケースを紹介します。

クラス型

クラス型は常に参照として扱われるため、変数への参照を必要とする場合でもoutやrefを付ける必要がありません。しかし素の状態ではマーシャラーは[In]の解釈しかしないため、値を戻す必要があるときは[Out]属性を追加しなければなりません。ここでデフォルトの動作を期待してクラス型にoutやrefを付けると、参照への参照という全く異なった指定になるので注意して下さい。

構造体型

構造体は値型なので引数として参照が必要な場合にはoutかrefを付けなければなりません。しかし値を相手側に伝え、かつ戻り値を必要としない場合にはoutは不適切です。またrefを使ったとしても今度は[Out]属性が不要です。こういう場合にはrefを用いかつ[In]属性を指定します。

この場合、refを用いて方向属性を省略することも可能です。しかし[Out]属性が生きているため、戻り値に対して不要なマーシャリングが起こります。プログラムは問題なく動作しますが、パフォーマンスを落とす原因になることがあります。

本書ではデフォルトの解釈を期待して方向属性を省略している場合があります。しかしマーシャラーに正しく方向属性を伝えるため、[In,Out]はできるだけ書くことを推奨します。

ドキュメントの先頭へ

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