Unmanagedの C++ から Managedのクラスを使う
- Pure C++コードから、.NET Frameworkベースのライブラリを使いたい・・・
- UnmanagedクラスにManagedクラスのハンドル型を持たせられる?
- さあ、gcrootを使ってWrapperを作ろう.
- 参考記事
Pure C++コードから、.NET Frameworkベースのライブラリを使いたい・・・
純粋なC++コード(Unmanagedコード)から、C#など.NET Frameworkベースで開発されたライブラリ(Managedクラス)を利用しなければならなくなった・・
それほど頻繁に遭遇するケースではありませんが、こんな課題に出くわすことがあります.
基本的な方法としては、共通言語ランタイムをつかって(/clrオプション)C++CLIにすれば丸く解決・・・のはずですが、これが許されないケースがあります.
例えば、Native部分のコードに標準ライブラリのmutexとかatomicとか使ってる場合.
これは共通言語ランタイムと互換性がないので、C++CLIにしてコードを共存させることができません.
(共存させようとするとコンパイラに叱られますね.)
こうなると、C++CLIにできる部分と出来ない部分はモジュールを分割し、ManagedクラスのインスタンスをラップしたクラスをUnmanaged C++から使ってやるしかないわけですね.
さて、ここで一つ問題が起こります。
error C3265: マネージ 'hoge' をアンマネージ 'fuga' で宣言できません.
おっと、UnmanagedのクラスはManagedクラスのハンドルを持てないのでした・・・
UnmanagedクラスにManagedクラスのハンドル型を持たせられる?
うーんと悩みながらドキュメントを漁ると・・msclr名前空間にとても便利なクラスがありました.
msclr::gcroot<T> を使え!
今回キモとなるのは gcroot テンプレート.
msclr/gcroot.h をインクルードして使用します.
このクラスはC++のテンプレートになっていて、Managedの参照型(ref class)をUnmanagedクラスに保持させることができるように設計されています.
Unmanagedクラスのメソッドでは、gcrootでラッピングしたハンドルからメソッドを呼び出します.
親切なことに operator ->()とoperator T()が準備されてますので、普通のハンドルとそれほど変わらない使い方が可能です.
コレは便利!!
じゃあ、value class の場合はどうするの・・?
当然の疑問ですよね.
サンプルでは全然見かけないのですが、実は value class も同じように保持できます.
これは、Managedの値型に AutoBoxingが働いて value class は自動的にref classのハンドルでWrapする処理が入るため.
下記のコードではvalue classのSystem::DateTimeを保持しています.
// System::DateTime は value class . // ハンドル型へはAutoBoxingされる DateTime^ dth = DateTime.Now; // DateTime^にAutoBoxingされるので, gcrootへも直に格納できる. msclr::gcroot<DateTime^> dtwrap = DateTime.Now; // ハンドル型はそのまま取得( operetor T() ) DateTime^ dth = dtwrap; // unboxは明示キャストが必要 DateTime dtv = static_cast<DateTime>(dth); DateTime dtv2 = static_cast<DateTime>(dtwrap);
unboxは上記のように明示的にキャストすればOKです.
ただし、Unbox処理はそこそこ負荷がかかりますので注意.
なるべくBoxされたハンドルのままで取り扱うのが良さそうです.
さて、これで準備は整いました.
さあ、gcrootを使ってWrapperを作ろう.
Wrapperの実装
見せたくないものを隠すのは モザイク Wrapperが定石ですね.
Pure C++からはManagedクラスが見えてはダメなので基本的に抽象化を使います.
class の内部メンバをNativeC++モジュールから完全に隠すため、ここでは pure virtual クラスを使います.
pure virtualな型のインスタンスをインタフェースオブジェクトとしてモジュールの外のコードに提供します.
ヒープ境界の問題があるので、モジュール外部(Native C++モジュール)のランタイム側のdelete/freeがこのインタフェースに対して使われないように少し細工が必要です.
ここでは デストラクタをprotectedにしてpure virtualなインスタンスをdelete出来ないようにし、deleteの役割は仮想関数の実装側のDestroyで行います.
こうするとDestroyメソッドの実装本体がC++CLI側モジュール内にあるので、必然的にCLIモジュール側のランタイムによるdeleteを強制することができます.
// // interface.h // 公開用ヘッダファイル // // モジュールの外から使うためのインタフェースクラス class CliDateTimeIF { protected: virtual ~CliDateTimeIF() {} // 直delete禁止 public: virtual void Destroy() = 0; // 代理デストラクタ. deleteを行う. virtual int GetYear() = 0; virtual int GetMonth() = 0; virtual int GetDay() = 0; };
そして、インタフェースの実装クラスです.
#include <msclr/gcroot.h> using namespace System; // ------------------------------------------------------ // ManagedクラスをUnmanaged C++でラップする class CliDateTimeImpl : public CliDateTimeIF { protected: virtual ~CliDateTimeImpl() { // 後始末 } public: CliDateTimeImpl() noexcept { } virtual void Destroy() override { delete this; // self-delete } virtual int GetYear() { return dt_->Year; } virtual int GetMonth() { return dt_->Month; } virtual int GetDay() { return dt_->Day; } void SetDateTime(DateTime^ dt) { dt_ = dt; } DateTime^ GetDateTime() { return dt_; // gcroot<T>::operator T() } protected: msclr::gcroot<DateTime^> dt_; // ハンドル型 };
モジュールの外から使えるようにする
最後にモジュール外に公開するファクトリ関数です.
公開ヘッダファイルにファクトリ関数の定義を追加します.
// // interface.h // 公開用ヘッダファイル // // モジュールの外から使うためのインタフェースクラス class CliDateTimeIF { protected: virtual ~CliDateTimeIF() {} // 直delete禁止 public: virtual void Destroy() = 0; // 代理デストラクタ. deleteを行う. virtual int GetYear() = 0; virtual int GetMonth() = 0; virtual int GetDay() = 0; }; extern "C" { // ------------------------------------------------------------------- // 公開ファクトリ関数 __declspec(dllexport) CliDateTimeIF* __cdecl CreateCLIDateTime(); };
Load Time Link(libファイルを使ったリンク)でもDynamic Link(LoadLibraryを使うリンク)でも使いやすいように __cdecl 規約の C関数をExportします.
この関数の戻値に pure virtual のポインタを返します.
extern "C" { // --------------------------------------- // 公開ファクトリ実装 CliDateTimeIF* __cdecl CreateCLIDateTime() { CliDateTimeIF* ptr = nullptr; try { ptr = new CliDateTimeImpl(); ptr->SetDateTime(DateTime.Now); } catch (std::bad_alloc&) { // alloc error!! } return ptr; } };
呼び出し側のコードでは、ファクトリ関数を使ってインスタンスを生成して使うだけです.
CliDateTimeIF* pdt = CreateCLIDateTime(); : int year = pdt->GetYear(); : pdt->Destroy(); // delete pdt = nullptr;