はじめに
C++向けのインターフェースはあるのにC#向けのインターフェースがないSDKを使いたいと思っていたところ、Swigに出会いました。
去年にSwigの使い方が少し分かったので以下記事を書きましたが、あまり出来がよろしくありません。
C++で作成したdllをSwigで作ったWrapper経由でC#から呼び出す方法
というか自分で言うのもなんですが、上記記事を見ても以下3つのことしか出来ないため、実用的な物を作ることが出来ません。
- C++側のAPIの引数がint
- C++側のAPIの戻り値がbool
- C++側のAPIの戻り値がクラス
C++で作ったdllをC#で使うことを考える際、最低限以下はサポートしたいと個人的には思っています。
- C++側のAPIの引数に文字列
- C++側のAPIの引数にvector
- C++側のAPIの引数にクラス
- C++側のAPIの戻り値が文字列
- C++側のAPIの戻り値がvector
- C++側のAPIの戻り値がクラス
- C++からC#へコールバックで通知
- コールバックの引数に文字列
- コールバックの引数にvector
ようやく動かすことが出来ましたので、手順を本記事にまとめます。
ソースコードはここに格納しています。
プロジェクトとしては以下2つです。
- MyDLL
C/C++で作成するdllのプロジェクトです。 - CsharpApp
作成したdllをC#から呼び出すことが出来るのかテスト用のプロジェクトです。
環境
今回の環境は以下です。
- Windows10 Pro(x64)
- Visual Studio 2019 Professional
- Swig 4.0.2
目次
-
C++でdll作成
1-1. .hファイル作成
1-2. .cppファイル作成
1-3. ビルドしてdllを作成 -
SwigでdllにC#向けインターフェースを追加
2-1. Windows対応
2-2. 文字列とvector対応
2-3. コールバック対応
2-4. ビルドしてdllを作成 - C#でdllを呼び出す
1. C++でdll作成
ダイナミック リンク ライブラリ(DLL)のテンプレート選択して、プロジェクト名を入力してください。
今回はプロジェクト名をMyDLLとしています。
1-1. .hファイル作成
Header.hというヘッダファイルに下記2のクラスを定義しています。
-
CallbackBase
C#側がこのクラスを継承して、CppClassのSetCallback()で渡すことを想定しています。 -
CppClass
文字列と配列のやり取りをするクラスになります。
さっそく中身を見てみましょう。
#include <string>
#include <vector>
class CallbackBase
{
public:
virtual void Callback(const std::string &str, const std::vector<float>& fVect) = 0;
};
class CppClass
{
public:
void SetString(const std::string &str);
std::string GetString();
void SetVector(std::vector<float>& fVect);
std::vector<float> GetVector();
void SetCallback(CallbackBase& callbackObject);
void ExeCallback();
private:
std::string string;
std::vector<float> floatVector;
CallbackBase* pCallbackObj = NULL;
void InvokeCallback();
};
typedef CppClass* (*GetInstanceFuncPointer)(void);
extern "C" __declspec(dllexport) CppClass * CreateInstance(void);
特に躓く部分はないと思います。
CppClassのPrivateで定義されている部分は、本来なら見せたくないのでCppClassを抽象クラスとしたいですが今回のテーマはSwigなので省いています。
1-2. .cppファイル作成
CallbackBaseは今回作るdllを使う側が継承して作ることになるので、作るのはCppClassということになります。
今回は下記のように実装しています。
#include <thread>
#include "Header.h"
void CppClass::SetString(const std::string& str)
{
string = str;
}
std::string CppClass::GetString()
{
return string;
}
void CppClass::SetVector(std::vector<float>& fVect)
{
floatVector = fVect;
}
std::vector<float> CppClass::GetVector()
{
return floatVector;
}
void CppClass::SetCallback(CallbackBase &callbackObj)
{
pCallbackObj = &callbackObj;
}
void CppClass::ExeCallback()
{
std::thread *th = new std::thread(&CppClass::InvokeCallback, this);
th->join();
}
void CppClass::InvokeCallback()
{
if (pCallbackObj != NULL)
{
pCallbackObj->Callback(string, floatVector);
}
}
__declspec(dllexport) CppClass* CreateInstance(void)
{
return new CppClass;
}
1-3. ビルドしてdllを作成
.hと.cppを作成で来たのでVisual Studio上でビルドしてください。
特に問題なくビルド出来ると思います。
このタイミングで、作成されたdllが公開しているAPIを見てみることにします。
Visual Studioに付属するDeveloper Command Prompt For VS2019を起動してください。
日本語環境下では名前が開発者プロンプトなどかもです。
プロンプト上で、作成されたdllがある場所までcdコマンドで移動して以下コマンドを打ってください。
bumpbin /exports MyDLL.dll
実行すると下の画像のようにCreateInstanceだけが表示されていると思います。
これはGetProcAddress()で取得できる関数のポインタがCreateInstance()しかないことを示しており、意図通りです。
2. SwigでdllにC#向けインターフェースを追加
Swigを使うためにはインターフェースファイルと呼ばれる.iファイルを作る必要があります。
書き方の詳細は本家のページを見ていただくとして、それ以外に参考になったページを挙げておきます。
2-1. Windows対応
では早速.iファイルを作成していきます。
最も基本的なインターフェイスファイルは以下になります。
%module "MyDLL" //(1)
%{
#include "Header.h" //(2)
%}
%include "Header.h" //(3)
(1)には作成するDLL名を、(2)と(3)にはDLLを使う上で必要な情報が記載されているヘッダを書きます。
具体的に(2)(3)に何を指定するのかは 似非AC雑記 : 任意の型の使用に素晴らしい説明がありますので引用します。
%{、%}で囲まれた部分は生成される*.cppファイルにそのまま出力される。ここでは型の名前解決のためにヘッダファイルを指定している。
%includeでSWIGが解析を行うファイルを指定する。
このようなインターフェイスファイルを作ったら、以下のようにSWIGに食わせるとラッパの.cppと.hとCSファイルが出力される。
swig.exe -csharp -c++ -cppext cpp Swig.i
.iファイルの役割と書き方が分かったのでさっそく上記コマンドを実行して.cppと.hと.csを出力してみましょう。
(なお、swig.exeを実行するbatをexecSwig.batというファイル名でGithub上位置いていますので、それをダブルクリックしても良いです。)
> swig.exe -c++ -csharp -cppext cpp Swig.i
Header.h(31) : Error: Syntax error in input(1).
エラーが発生してしまいました。
どうやらHeader.hの31行目がまずいようです。
色々調べると公式ページに下記説明を見つけました。
A common problem when using SWIG on Windows are the Microsoft function calling conventions which are not in the C++ standard. SWIG parses ISO C/C++ so cannot deal with proprietary conventions such as __declspec(dllimport), __stdcall etc. There is a Windows interface file, windows.i, to deal with these calling conventions though. The file also contains typemaps for handling commonly used Windows specific types such as __int64, BOOL, DWORD etc.
上記に従って、windows.iをincludeするようにしましょう。
%module "MyDLL"
%{
#include "Header.h"
%}
%include <windows.i>
%include "Header.h"
これで再度swig.exeを実行するとエラーは発生しませんでした。
2-2. 文字列とvector対応
先ほどWidnows.iを加えることで無事swig.exeを実行することが出来、以下ファイルが生成されます。
- Swig_wrap.cpp
- CallbackBase.cs
- CppClass.cs
- MyDLL.cs
- MyDLLPINVOKE.cs
- SWIGTYPE_p_std__string.cs
- SWIGTYPE_p_std__vectorT_float_t.cs
CppClass.csを開くとCppClassが公開しているAPI名が見えます。
以下はCppClass.csの一部です。
public void SetString(SWIGTYPE_p_std__string str) {
MyDLLPINVOKE.CppClass_SetString(swigCPtr, SWIGTYPE_p_std__string.getCPtr(str));
if (MyDLLPINVOKE.SWIGPendingException.Pending) throw MyDLLPINVOKE.SWIGPendingException.Retrieve();
}
public SWIGTYPE_p_std__string GetString() {
SWIGTYPE_p_std__string ret = new SWIGTYPE_p_std__string(MyDLLPINVOKE.CppClass_GetString(swigCPtr), true);
return ret;
}
public void SetVector(SWIGTYPE_p_std__vectorT_float_t fVect) {
MyDLLPINVOKE.CppClass_SetVector(swigCPtr, SWIGTYPE_p_std__vectorT_float_t.getCPtr(fVect));
if (MyDLLPINVOKE.SWIGPendingException.Pending) throw MyDLLPINVOKE.SWIGPendingException.Retrieve();
}
public SWIGTYPE_p_std__vectorT_float_t GetVector() {
SWIGTYPE_p_std__vectorT_float_t ret = new SWIGTYPE_p_std__vectorT_float_t(MyDLLPINVOKE.CppClass_GetVector(swigCPtr), true);
return ret;
}
SetString()の引数がSWIGTYPE_p_std__stringという型になっています。
同様に、SetVector()の引数はSWIGTYPE_p_std__vectorT_float_t型になっています。
SetString()はstring型を使いたいし、SetVector()はListやfloat[]を使いたいのに、何その型???
このままでは使いにくいので、.iファイルを以下のようにします。
%module "MyDLL"
%{
#include "Header.h"
%}
%include <windows.i>
%include <std_string.i>
%include <std_vector.i>
/* std::vector<float>をC#側ではFloatVectorというクラスとして扱う */
%template(FloatVector) std::vector<float>;
%include "Header.h"
これで再度swig.exeを実行するとCppClass.csで定義されているSetString()やSetVector()の引数が、stringやFloatVectorになっていることが分かります。
FloatVectorはListみたいなものです。
詳細な定義はFloatVector.csに書いてありますが、初見でも問題なく扱えると思います。
2-3. コールバック対応
現状の.iファイルを使ってswig.exeを実行するとファイルが作られました。
- Swig_wrap.cpp
- CallbackBase.cs
- CppClass.cs
- FloatVector.cs <- New
- MyDLL.cs
- MyDLLPINVOKE.cs
これで.iファイルは完成だ!と思いきや実はコールバックをするためにはまだ細工が必要です。
公式のページに詳細の説明があるのでそちらを参照していただくとして、最終的な.iファイルは下記のようになります。
/* 変更 */
%module (directors="1") MyDLL
%{
#include "Header.h"
%}
%include <windows.i>
%include <std_string.i>
%include <std_vector.i>
/* std::vector<float>をC#側ではFloatVectorというクラスとして扱う */
%template(FloatVector) std::vector<float>;
/* 追加 */
%feature("director") CallbackBase;
%include "Header.h"
上記.iファイルを使用してSwig.exeを実行すると以下のファイルが作られます。
- Swig_wrap.cpp
- Swig_wrap.h <- New
- CallbackBase.cs
- CppClass.cs
- FloatVector.cs
- MyDLL.cs
- MyDLLPINVOKE.cs
2-4. ビルドしてdllを作成
ではdllにC#向けインターフェースを追加してみます。
手順としては、先ほどSwig.exeを実行して作成されたSwig_wrap.cppをプロジェクトのソースファイルに追加して改めてビルドするだけです。
はい、これだけでC#向けのインターフェースが作成されます。
.iファイルさえ作ることが出来ればC#向けのインターフェースを作るのは簡単ですね!
実際にビルドして作られたdllにdumpbinを実行して本当にAPIが増えたのか見てみましょう。
bumpbin /exports MyDLL.dll
実行すると下の画像のようにCreateInstance以外も表示されていると思います。
APIがすごく増えていますね。
C#側はこの増えたAPIを経由して、CppClassにアクセスすることになります。
増えたAPIですが、C++側でGetProcAddress()で取得することが出来るはずです。
やる意味ないですが...
3. C#でdllを呼び出す
C#で作成するコンソールプログラムから作成したdllが呼び出せるか確認します。
プロジェクト名はCSharpAppです。
することは3つだけです。
- Swig.exeが作成した.csファイルをプロジェクトに加える
- CallbackBaseを継承したクラスを作成
- dllが提供するAPIを呼び出す
特に説明も不要だと思いますのでコードを貼り付けます。
using System.Diagnostics;
namespace CsharpApp
{
public class MyCallback : CallbackBase
{
public override void Callback(string str, FloatVector fVect)
{
int i = 0;
Debug.WriteLine($"str = {str}");
foreach (float value in fVect)
{
Debug.WriteLine($"[{i}] :value = {value}");
i += 1;
}
}
}
class Program
{
static void Main(string[] args)
{
MyCallback myCallbackObj = new MyCallback();
CppClass instance = MyDLL.CreateInstance();
string str = "テストHello World!";
instance.SetString(str);
Debug.WriteLine($"str = {instance.GetString()}");
FloatVector v = new FloatVector();
v.Add((float)-0.1);
v.Add((float)-1);
v.Add((float)-10);
int i = 0;
instance.SetVector(v);
FloatVector buf = instance.GetVector();
foreach (float value in buf)
{
Debug.WriteLine($"[{i}] :value = {value}");
i += 1;
}
instance.SetCallback(myCallbackObj);
instance.ExeCallback();
}
}
}
では実際に動作するか見てみましょう!
str = テストHello World!
[0] :value = -0.1
[1] :value = -1
[2] :value = -10
callback : str = テストHello World!
callback : [0] :value = -0.1
callback : [1] :value = -1
callback : [2] :value = -10
意図した通りに動いていますね!
まとめ
この記事ではSwigの使い方と、実際にdllにC#向けのインターフェースが追加されることを確認しました。
期待した通りに追加され、C#側で問題なく使用できることを確認しました。
本当は配列を受け取る、配列を返すというものをやりたかったのですが配列を返す方法が分からなかったので断念しています。
誤字脱字や認識間違い等あればご指摘ください。
参考リンク
- Microsoft extensions and other Windows quirks
- C# Directors
- Irrlicht.NET by SWIG : SWIGの使い方
- 似非AC雑記 : 任意の型の使用
- C++で作成したdllをSwigで作ったWrapper経由でC#から呼び出す方法
ソースコード