C++から利用可能なC#によるクラスライブラリの作成

ちょっと詰まったのでメモ。

やりたいことは「ユーザであるC++のコードから、C#のクラスライブラリに任意型のデータを渡す。C#はそれをリストに溜め、適宜要求に応じてC++の人に返したりする」
とりあえずインタフェイスとして以下のようなものを考える。
実際にはもっといろんな操作が必要だろうな

  • AddVariant() : データの追加
  • GetVariantAt() : データの取り出し


これを備えたクラスライブラリを以下のように作る

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Diagnostics;

namespace COMVariant
{
    [ComVisible(true)]
    public interface IVariant
    {
        void AddVariant(Object obj);
        Object GetVariantAt(int num);
    }

    [ClassInterface(ClassInterfaceType.AutoDual)]
    public class Variant:IVariant
    {
        private List<Object> _list;

        public Variant()
        {
            _list = new List<object>();
        }

        public void AddVariant(Object obj)
        {
            Debug.WriteLine("AddVariant "+ obj+": type="+obj.GetType());
            _list.Add(obj);
        }

        public Object GetVariantAt(int num)
        {
            if(_list.Count > num)
            {
                Object obj = _list[num];
                Debug.WriteLine("GetValueAt["+num+"] => " + obj);
                return obj;
            }

            //num is exceeding limit
            Debug.WriteLine("GetValueAt exceeded limit");
            throw new Exception("Exceeded Limit");
        }
    }
}

各所、COMとして使うためにインタフェイス・クラスのそれぞれ適切な属性をつけている

[ComVisible(true)] // インタフェイス

[ClassInterface(ClassInterfaceType.AutoDual)] //クラス

またこれらに加えてプロジェクトのプロパティとして

  • 「アプリケーション」タブ内の「アセンブリ情報」→「アセンブリをCOM参照可能にする」をON
  • 「ビルド」タブ内の「COM相互運用〜」もON

さらにビルド後には

  • 生成されたDLLにたいして

> regasm (生成されたDLL)

として登録しておく。


次に呼び出すC++の方
こちらでは基本的に以下の流れで先に作成したライブラリを呼び出す

  • #importでtlbファイルの指定、タイプライブラリの読み込み
  • COMの初期化
  • インタフェイスポインタの宣言、インスタンス
  • ほげほげ
  • COMの終了処理

まずは以下のように先に作成したtlbを指定する

#import "C:\\full\\path\\COMVariant.tlb" named_guids raw_interfaces_only

これによりプリプロセス時にDebug\bin以下にタイプライブラリヘッダ"callbacklib.tlh"なるファイルができる。
詳しくはここ*1にあるのを参照
それぞれの後続するオプションは

  • named_guids: 後で使うCLSIDなどを自動生成
  • raw_interfaces_only: つけとけって言ってる*2からとりあえずつけとく

他にもクラスライブラリの名前空間を省くno_namespaceなどがある

COMの初期化は基本的に以下の一行

CoInitialize(NULL);

これは後述する終了処理のCoUninitialize()と対になっている。

.NETのクラスをつかむためにインタフェイスポインタとやらを使う。
これに当たってスマートポインタというものがでてくる。
C#側で宣言したインタフェイスは"IVariant"だが、プリプロセッサで処理が終わった時点でインタフェイスポインタとして以下のような新しいものが定義される

スマートポインタ = インタフェイス名 + "Ptr" = IVariantPtr

このIVariantPtrをここでは使う。IVariantを使うと怒られるので注意。
このスマートポインタにたいしてCreateInstance()を呼び出すことでインスタンス化ができる。

IVariantPtr dotNetCOMPtr; // Smart Pointer Name = IVariant + Ptr
HRESULT hRes = dotNetCOMPtr.CreateInstance(COMVariant::CLSID_Variant); // Instancialize

このポインタを通してほげほげした後はCOMの終了処理を行う。
以下の一行挟めば終わり

CoUninitialize();