ネイティブ資産の利用 の編集

Last-modified: 2012-03-05 (月) 14:54:43

基本方針

基本的には、C/C++ネイティブのライブラリに、C++/CLIの薄いラッパーDLLをかぶせ、C#でGUIを書く。
C++/CLIで、GUIを書いてもいいが、C#の方がより簡単にGUIを記述できるので、GUIはC#に任せたほうが良い。

ネイティブ資産への変更

上記のC++/CLIラッパーをかぶせる場合、ネイティブライブラリには次のような変更を加える。

  • [半必須] Cランタイムライブラリを「マルチスレッドDLL」系に変更する。
  • [推奨] 文字セットをUNICODEにする。
  • [推奨] レジストリを使わない。

[半必須] Cランタイムライブラリを「マルチスレッドDLL」系に変更する。

ネイティブライブラリがCソースであるなら関係はないが、ネイティブライブラリが
C++で書かれていて、C++/CLIラッパーからインクルードするヘッダに

#include <string>
#include <vector>

があり、実際にvectorを使用していると次のようなエラーになる可能性がある。

libcpmtd.lib(xlock.obj) : error LNK2005: "public: __thiscall std::_Lockit::_Lockit(int)"
(??0_Lockit@std@@QAE@H@Z) は既に msvcprtd.lib(MSVCP80D.dll) で定義されています。

この場合、上記のインクルードをヘッダで使用しないようにするか、(←あまり現実的ではない)
Cランタイムライブラリを「マルチスレッドDLL」あるいは「マルチスレッドDLL デバッグ」にする。
この設定は、プロジェクトのプロパティの「C++」-「コード生成」にある。

また、大量のネイティブライブラリが存在する場合、すべてを「マルチスレッドDLL」に変更するのは
大変なので、vspropsとvcbuildを利用してバッチ処理する。

[推奨] 文字セットをUNICODEにする。

UNICODEを使用しない場合、C++/CLIの文字列がUNICODEであることから、
C++/CLI→ネイティブライブラリの過程で文字が劣化する恐れがある。

この設定は、プロジェクトのプロパティの「全般」にある。
(「C++」や「リンカ」、「ライブラリアン」下の「全般」ではない。)

[推奨] レジストリを使わない。

Windows Vistaでの推奨。

チップス

文字列変換

C++/CLI文字列→C/C++文字列(char*)

System::String^ clistring = L"彼岸花";
std::string cppstring;
array<Byte>^ byteArray;
byteArray = Text::Encoding::Convert(
  Text::Encoding::Unicode,
  Text::Encoding::GetEncoding(0),
  Text::Encoding::Unicode->GetBytes(clistring));
  pin_ptr<Byte> pin = &byteArray[0];
cppstring.assign(reinterpret_cast<const char*>(pin), byteArray->Length);

C++/CLI文字列→C/C++文字列(wchar_t*)

String^ clistring = L"彼岸花";
std::wstring cppstring;
pin_ptr<wchar_t> pin = &clistring->ToCharArray()[0];
cppstring.assign(pin, clistring->Length);

コールバック関数

ネイティブ(C/C++)で書かれたスタティックライブラリのコールバック関数を受けるにはどうしたらいいのか。

条件

ネイティブのライブラリ側に以下のようなコールバック登録用関数があるものとする。

void SetCallback(void (*func)(int value, void * obj), void * obj);

ステップ1

C++/CLIでは、以下のように、グローバルスコープにコールバック受け取り関数を作る。
この関数は、C++/CLI中のクラスには属さない。

void MyGlobalCallback(int value, void * obj)
{
}

ステップ2

次にネイティブライブラリ側にコールバック関数を登録する、

SetCallback(MyGlobalCallback);

一応、これだけでコールバックを受け取ることはできる。

ステップ3

SetCallback()の第二引数は、「void * obj」となっている。この引数はコールバックを受け取る関数の第二引数に渡される任意のオブジェクトになっている。つまり、ネイティブ同士であれば普通に、任意のオブジェクトやインスタンスへのポインタを渡して、コールバックを受け取る関数内で任意の処理が書ける。だが、C++/CLIのクラスのインスタンスは、そのままでは、void*へキャストすることは難しい。
そこでC++/CLIのクラスのインスタンスには、インスタンスごとにユニークなIDを持たせ、インスタンスのリストをinternalないし、publicで参照可能なようにしておく。

public ref class Hoge
{
internal:
  List<Hoge^>^ instanceList;
  Int32 instanceId;
};

コールバック関数をセットする際には、以下のようにインスタンスIDを任意のオブジェクトとして、SetCallback()に与える(もちろん任意のオブジェクトのインスタンスIDを渡しても良い)。

SetCallback(MyGlobalCallback, reinterpret_cast<void*>(this->instanceId));

これで、コールバックを受ける関数内で、インスタンスのリストをなめることで、ネイティブ環境と同じように処理を行える。


参考文献

http://www.atmarkit.co.jp/fdotnet/special/vcppinvista01/vcppinvista01_01.html