ま、そんなところで。

ニッチな技術系メモとか、車輪を再発明してみたりとか.

Unmanagedの C++ から Managedのクラスを使う

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;

参考記事