内部コードを std::string/char * にするか std::wstring/wchar_t * にするかで、SWIG の挙動が変わるような感じなので、その前段階のテスト
戻り値をstring型にする SWIG の技
C#側からみればC++のDLLから文字列を取得したい場合、GetName( const char *, int ) としてバッファを渡すよりも、string GetName() な形で、string 型を返して欲しいわけです。そうなると、dllimport の部分も
[DllImport("hellodll", EntryPoint = "GetNameStr")]
static extern string GetNameStr();
な形にしておきたいところですよね。しかし、これはできません。戻り値は string に自動変換してくれないんですね。
不思議なことに、SWIG の場合は戻り値に string を使えて、まあこんな風に string 型を渡したり受け取ったりできます。
var hello = new Hello();
hello.SetName("Tomoaki Masuda");
var name = hello.GetName();
Console.WriteLine($"name: {name}");
当然のことながら、これは Hello クラス内でラップしている訳で、
public string GetName() {
string ret = helloswigPINVOKE.Hello_GetName(swigCPtr);
return ret;
}
となっていますが、dllimport を見ると、下記のようになっていて、string 型を返しています。
[global::System.Runtime.InteropServices.DllImport("helloswig", EntryPoint="CSharp_Hello_GetName")]
public static extern string Hello_GetName(global::System.Runtime.InteropServices.HandleRef jarg1);
dllimport で string 型を返しているので、どういうトリックになっているのかとコードを追ってみると、
protected class SWIGStringHelper {
public delegate string SWIGStringDelegate(string message);
static SWIGStringDelegate stringDelegate = new SWIGStringDelegate(CreateString);
[global::System.Runtime.InteropServices.DllImport("helloswig", EntryPoint="SWIGRegisterStringCallback_helloswig")]
public static extern void SWIGRegisterStringCallback_helloswig(SWIGStringDelegate stringDelegate);
static string CreateString(string cString) {
return cString;
}
static SWIGStringHelper() {
SWIGRegisterStringCallback_helloswig(stringDelegate);
}
}
文字列専用のヘルパークラスがあって、string 型を戻すときは
- C++ 側でここで設定されているデリゲート関数を呼び出す。
- C# 側であらかじめ string なデータを作る。
- C++ 側から、string なデータのポインタを返す。
- これを C# 側で string 型で受けて string として利用する。
というなかなか興味深いことをやっています。Python とか Java のコードは追っていないのですが、これは冗長ではあるけど、C#(.NETの世界)のマネージなメモリ空間でデータを扱えるようにする良い手段ですよね。
って、ことまでは分かったのですが、じゃあここの「冗長」な部分は、本来の C# ならばどうするのか?ってのが次です。
C#側でMarshal.PtrToStringAnsiを使う
サンプルコードは、
moonmile/hellodll: C++ の DLL から const char *, wchar_t * を読み込むテスト
https://github.com/moonmile/hellodll
にあります。
C++ の内部的には、std::string あるいは std::wstring を使っている状態で、C# へのインターフェースに、char * あるいは wchar_t * を使うという想定ですね。
– hellodll : C++ の共有ライブラリ
– hello : C# から hellodll を呼び出し
– helloCore: .NET Core から hellodll の呼び出し
– helloStd, helloStdCore : .NET Standard で hellodll を呼び出し、それを .NET Core で利用するパターン
C++ 側ではこんな(乱暴な)感じで、インターフェースを作っておきます。単純な文字列の受け渡しなので、_str や _wstr の中身は C# 側では弄らず、C++ 側でのみ弄るという想定です。C# で弄ったときは、あらためて SetNameStr(char *) のように文字列を渡して貰えばいいのです。
#include <string>
static std::string _str = "";
static std::wstring _wstr = L"";
extern "C" {
__declspec(dllexport) int __stdcall Add(int x, int y) {
return x + y;
}
__declspec(dllexport) void __stdcall SetNameStr(char *t) {
_str = std::string(t);
}
__declspec(dllexport) char * __stdcall GetNameStr() {
return (char*)_str.c_str();
}
__declspec(dllexport) void __stdcall SetNameWStr(const wchar_t *t) {
_wstr = std::wstring(t);
}
__declspec(dllexport) const wchar_t * __stdcall GetNameWStr() {
return _wstr.c_str();
}
}
これを C# 側で受けると、こんな感じになります。C# から渡すときは string が使えるけど、C++ から貰うときは IntPtr で受けます。
[DllImport("hellodll", EntryPoint = "SetNameStr")]
static extern void SetNameStr(string t);
[DllImport("hellodll", EntryPoint = "GetNameStr")]
static extern IntPtr GetNameStr();
[DllImport("hellodll", EntryPoint = "SetNameWStr", CharSet = CharSet.Unicode)]
static extern void SetNameWStr(string t);
[DllImport("hellodll", EntryPoint = "GetNameWStr", CharSet = CharSet.Unicode)]
static extern IntPtr GetNameWStr();
IntPtr で受けた値を、Marshal.PtrToStringAnsi あるいは、Marshal.PtrToStringUni で変換します。char * と wchar_t * に対応するわけです。
string s = "こんにちは C++ の世界"; SetNameStr(s); var s1 = Marshal.PtrToStringAnsi(GetNameStr());
でもって、いちいち文字列を貰うたびに Marshal.PtrToStringAnsi を使うのは面倒なおで、次のようにヘルパークラスを作っておいて、
public class HelloDll
{
public static void SetNameStr(string s) { _SetNameStr(s); }
public static string GetNameStr() { return Marshal.PtrToStringAnsi(_GetNameStr()); }
public static void SetNameWStr(string s) { _SetNameWStr(s); }
public static string GetNameWStr() { return Marshal.PtrToStringUni(_GetNameWStr()); }
[DllImport("hellodll", EntryPoint = "SetNameStr")]
static extern void _SetNameStr(string t);
[DllImport("hellodll", EntryPoint = "GetNameStr")]
static extern IntPtr _GetNameStr();
[DllImport("hellodll", EntryPoint = "SetNameWStr", CharSet = CharSet.Unicode)]
static extern void _SetNameWStr(string t);
[DllImport("hellodll", EntryPoint = "GetNameWStr", CharSet = CharSet.Unicode)]
static extern IntPtr _GetNameWStr();
}
対応する static メソッドを呼び出せばよいのです。
string s = "こんにちは C++ の世界"; HelloDll.SetNameStr(s); var s1 = Marshal.PtrToStringAnsi(GetNameStr()); Console.WriteLine(s1);
一見、ヘルパークラスは冗長な感じがしますが、インテリセンスが効くことと、名前空間などで区切ることができる、C++の短い名前をC#の長い名前に直すことができる、というメリットがあります。
少し高速さには掛けますが、所詮 C++/C# 変換しているところで遅くなっているので、そこは気にしないことにします。
C++と.NET Standard/Core の関係
試しに、.NET Standard を経由してみたのが、helloStd, helloStdCore のプロジェクトです。
一度、.NET Standard のクラスライブラリを作成しておいて、
public class HelloDll
{
public static void SetNameStr(string s) { _SetNameStr(s); }
public static string GetNameStr() { return Marshal.PtrToStringAnsi(_GetNameStr()); }
public static void SetNameWStr(string s) { _SetNameWStr(s); }
public static string GetNameWStr() { return Marshal.PtrToStringUni(_GetNameWStr()); }
[DllImport("hellodll", EntryPoint = "SetNameStr")]
static extern void _SetNameStr(string t);
[DllImport("hellodll", EntryPoint = "GetNameStr")]
static extern IntPtr _GetNameStr();
[DllImport("hellodll", EntryPoint = "SetNameWStr", CharSet = CharSet.Unicode)]
static extern void _SetNameWStr(string t);
[DllImport("hellodll", EntryPoint = "GetNameWStr", CharSet = CharSet.Unicode)]
static extern IntPtr _GetNameWStr();
}
.NET Core から .NET Standard のクラスライブラリを参照設定させて実行というスタイルです。
static void Main(string[] args)
{
Console.WriteLine("Test helloCoreStd");
string s = "こんにちは C++ の世界";
HelloDll.SetNameStr(s);
var s1 = HelloDll.GetNameStr();
Console.WriteLine(s1);
HelloDll.SetNameWStr(s + " in Unicode");
var s2 = HelloDll.GetNameWStr();
Console.WriteLine(s2);
Console.WriteLine("end");
}
先の、.NET Core から C++ 直接呼出しと何が違うのかというと、いったん .NET Standard で包むことによって、.NET Core 以外の環境で動作ができるという話です。C++ のライブラリは環境ごとにビルドして配布(Windows/Ubuntu/Raspberry Piのように)する必要はありますが、.NET Standard のクラスライブラリはそのまま各種の .NET 開発環境で利用できるという訳ですね。所謂、インターフェースクラスの代わりになります。
そうなると、このクラス(アセンブリ)を利用して、Xamarin.Android/iOS で利用することも可能という話です。そうすると、自前の小さな C++ ライブラリをいちいち C# に書き替えることなく(あるいは Marshal.* や unsafe を駆使することなく)スマートフォン環境に組み込めるのではないかな、と思ったり。このあたりは、もう少し見ていかないといけませんが。
