構造体マーシャリング P/Invoke 割付メソッドを使わない選択肢
マーシャルの概念は、いわゆる ANSI C にはない概念です。これは言語仕様ではなく処理系の技術だからです。今回は、 VC++, .NET 間で DLLを共有するために必要な型変換に関する技術について少し触れます。
P/Invoke
P/Invoke とは Visual C++ , NET間で型を相互変換するための .NET技術のことです。シンプルな組込みデータ型の場合は型変換のためのコードを必要としない場合があります。暗黙の P/Invoke といいます。
マーシャリング
構造体など 組込データ型ではない型の場合は、型変換のため型変換メソッドを用いる必要があります。
.NET コード上で
.NETコード上で Visual C++ のうち export している関数を呼び出す場合は System.Runtime.InteropServices の DllImport 属性を用いますが、型変換は同ランタイムの Marshal クラスのメソッドを用います。
C++/CLI とは
C+/CLI は Visual C++ と .NET を混在記述できる仕組みです。
C++/CLI プロジェクトでマーシャルしておくのは何のため?
.NET コード上で Visual C++ の export 関数を呼び出すのは、やや冗長な記述になります。 .NET のウリは短いコードで多機能を実現することにありますので、 VC++で実装した機能を .NET で使用させたい場合は、.NET のできるだけ最も基本的な文法だけでで呼び出せるやうに隠ぺいしておくのが、親切といへるでせう。また後年、自分自身が使用する場合においてもしかりです。さういふ意味で C++/CLI には大きな意義があります。
そのやりかたはヘッダファイルで公開している関数を C+/CLI プロジェクト上で呼び出し、このなかで変換を記述しておくことです。一般にクラスにしておくほうがよいでしょう。
要するに .NET 側に、InteropService が提供する型変換を「意識させない」やうにあらかじめ隠ぺいしておくために、C++/CLI という仕組みを利用して DLL を作っておく、といふわけです。
もしこれをラッパープロジェクトとして利用する場合は明示マーシャリングをここに記述します。その場合は VC++ の名前空間msclrのinteropクラスを using することで、明示マーシャルのメソッドを使うことができます。
「手動マーシャル」という用語が意図すること
手動マーシャルという用語は MSDN にはありません。人によって何を意図するか変わります。
- 暗黙のP/Invoke ではない、明示的マーシャルのこと
- ユーザ定義型の一つである構造体や共有体をマーシャルする場合に用いられる手法全般
- 上記 2. のうち Interopクラス下にあるメモリ割付・解放に関するメソッドを用いない方法
インターネット上の情報においては、 (組込みデータ型は P/Invoke で相互変換されるため) 2. または 3. のことを指す場合が多いように見受けられます。 2. の場合は具体的な手法を指定するものではないので意味が曖昧になりますし, 3. ではない場合も手動マーシャルと呼んでいる例も見受けられますので、
要するに MSDN がこの用語を用いていないからしても、また情報元によって意味合いが異なることからも、この手動マーシャルという用語はあまり用いないほうがよいかもしれません。
「簡易マーシャル」と呼んでみる
ポインタ変数を含む構造体や構造体配列のマーシャルにおいては、 Interopクラス下にあるメモリ割付・解放に関するメソッド、を用いないほうが直感的にわかりやすい場合があります。すなわち、組込みデータ型に関する型変換を組み合わせて明示的に指定するやりかたです。
私はこれを P/Invoke API である Interop を深く理解しなくても採用できる手法ということで、「簡易マーシャル」と呼ぶことにします。ただしこれを「手動マーシャル」と呼んでいる情報元もあるようですが前述のとおり、意図するところが各々異なるのでその用語は用いないようにします。
具体的に、この簡易的な手法においては、どのような記述になるのでしょうか。以下は C++/CLI で記述した例になります。
array<statTCPROW_NET^>^ getstat_tcp(){
stat_row = (*stat_ins).getstat_tcp();
unsigned int siz = stat_row[0].size;
stat_row_net = gcnew array<statTCPROW_NET^>(siz);
for (unsigned int i = 0; i < siz; i++) { stat_row_net[i] = gcnew statTCPROW_NET(); stat_row_net[i]->LocalAddr = chr2str(stat_row[i].LocalAddr);
stat_row_net[i]->RemoteAddr = chr2str(stat_row[i].RemoteAddr);
stat_row_net[i]->Statestr = conchr2str(stat_row[i].Statestr);
stat_row_net[i]->ProcessName = wchr2str(stat_row[i].ProcessName);
stat_row_net[i]->LocalPort = stat_row[i].LocalPort;
stat_row_net[i]->RemotePort = stat_row[i].RemotePort;
stat_row_net[i]->Statenum = stat_row[i].Statenum;
stat_row_net[i]->PID = stat_row[i].PID;
stat_row_net[i]->size = stat_row[i].size;
}
delete[] stat_row;
delete[] ins;
return stat_row_net;
}
このようにアロケートに関するメソッドを使わずに、構造体メンバの一つ一つを組込みデータ型変換メソッド marshal_as などで変換していくという手法になります。なお上記例の chr2str などは表現短縮の便宜上、定義しておいた関数になります(関数の中身はたんなる組込みデータ型の明示的な型変換です)。
このような手法は MSDN から見れば 正攻法ではないとは思いますが、 ANSI C 範囲の基礎を習得した人であれば手っ取り早く思いつく方法ではあるでしょう。この場合、元のデータがポインタ変数を含む場合には、.NET 側で用いるためのメモリ解放のための関数を実装する必要があるでしょう。
この手法は決して推奨されるものではありません。あくまでも、こういう方法でも可能ではあるという例です。つまりポインタ変数のある構造体など、要するに、任意のデータ型をいろいろと使っている VC++ 関数を、
コード的な品質はともかくとして、とりあえず .NET 側に提供したい、という場合、MSDN や それにも記載されていないやうな InteropService の各種機能 について網羅的に知らなくても、
上記のやうに、構造体のメンバをひとつひとつ変換していく、というやうな愚直な方法でも可能である、といふことですな。
デメリットは見ての通り、構造体のメンバの数に比例してマーシャルのためのコードが長くなるということです。
メリットは見ての通り、Interop メソッドについてわからなくても、構造体メンバに変化があった場合の修正のうえで分かりやすいということです。