内部コードを 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 を駆使することなく)スマートフォン環境に組み込めるのではないかな、と思ったり。このあたりは、もう少し見ていかないといけませんが。