C#からアンマネージドDLL関数の呼び出しとコールバック関数渡し

まずC++でこんな.dllを作る。仮にgreeting.dllとする。

#include <stdio.h>
extern "C" {
  typedef const char* (*WhatDoYouSay)();
  __declspec(dllexport) void Greeting(WhatDoYouSay callback) {
    printf("Greeting: %s\n",(*callback)());
  }
}

この場合C#からの呼び出しは

using System;
using System.Runtime.Interopservices;
class Program {
  delegate string WhatDoYouSay();
  [DllImport("greeting.dll")]
  static extern void Greeting(WhatDoYouSay callback);
  static string ISay(){
    return "Good Morning!!";
  }
  static void Main(string[] args){
    Greeting(ISay);
  }
}

以上。通常のC関数ならコールバック関数もdelegateでOKということでした。

C#からアンマネージドDLLクラスを呼び出しは?

結論から言うとクラス呼び出しにするにはちょっとめんどくさい。理由は、

  • エントリポイント(関数名)をエンコードした名前で指定しないといけない。下記のEntryPointがエンコードされた名前。
  [DllImport("greeting.dll",EntryPoint="?Greeting@@YAXXZ")]
  static extern void Greeting(WhatDoYouSay callback);
  • クラスのメモリ容量等がわからないので結局.dll側にファクトリ関数を配置することになる。
class Hoge {
  ...
};
__declspec(dllexport) Hoge* Hoge_new(){
  return new Hoge();
}
__declspec(dllexport) void Hoge_delete(Hoge* p){
  delete p;
}

こんな風になるなら実際の実装がC++であっても.dllに公開する場合にはC言語で書いた方がスマートかな。どちらにしてもC++クラスをそのままインポートする方法が無い以上C#クラスの体裁を整えるにはバインディングを書かないとダメ。

class Hoge : IDisposable {
  [DllImport("hoge.dll")]
  static extern IntPtr Hoge_new();
  [DllImport("hoge.dll")]
  static extern void Hoge_delete( IntPtr context );
  [DllImport("hoge.dll")]
  static extern void Hoge_Greeting( IntPtr context, string ISay );
  
  IntPtr context;

  public Hoge(){
    this.context = Hoge_new();
  }
  public void Dispose(){
    Hoge_delete(this.context);
  }
  public void Greeting(){
    Hoge_Greeting(this.context,"Hello!!");
  }
}

ちなみにC++クラスのメソッドをインポートするのであれば、CallingConvention=CallingConvention.ThisCallを使うと良いみたい。

class __declspec(dllexport) Hoge {
public:
  void Greeting(const char* ISay){
    ...
  }
};
[DllImport("hoge.dll",
  CallingConvention=CallingConvention.ThisCall,
  EntryPoint="?Greeting@Hoge@@QAEXXZ")]
static extern void Hoge_Greeting( IntPtr context, string ISay );//Hoge::Greeting

ThisCallを指定するとHoge_Greetingの最初のIntPtrがC++のthisに渡されます。

というかC++/CLIを使えよと

using namespace System;
using namespace System::Runtime::InteropServices;
ref class HogeCS : public IDisposable {
public:
  HogeCS(){
    this->hoge = new Hoge();
  }
  virtual ~HogeCS(){//デストラクタ
    delete this->hoge;
    this->hoge = NULL;
  }
  virtual !HogeCS(){//ファイナライザ
    this->~HogeCS();
  }
  virtual void Dispose(){
    delete this;
  }
  void Greeting( String^ ISay ){
    IntPtr str = Marshal::StringToHGlobalAnsi( str );
    this->hoge->Greeting((const char*)str.ToPointer());
    Marshal::FreeHGlobal( str );
  }
private:
  Hoge* hoge;
};

仕事で書いてるコードはC++/CLIでつなぎを書いています。しかしこれはこれで短所があります。

  • C#の.dllとC++の.dllに加えて、さらにつなぎのためだけのC++/CLIの.dllを作らなくてはいけない。
  • Stringをわざわざchar配列に変換しないといけない。
  • C++/CLIには言語仕様的に無理がある。息切れ感たっぷり。
    • GCで回収されるとデストラクタは呼ばれずにファイナライザだけ呼ばれる。しかも別スレ上等とか。
    • ref classにはプリミティブ型しか持てないとか。
    • ハット'^'ってなんやねん。
    • SIMD命令直接呼ばせろや。

うちの場合C++/CLIC++だからと思って普通にそこに機能追加してしまい、後々上述のようなところで無駄な労力を取られました。極力C++/CLIは避けて通った方が賢明です。