内部コードを std::string/char * にするか std::wstring/wchar_t * にするかで、SWIG の挙動が変わるような感じなので、その前段階のテスト
戻り値をstring型にする SWIG の技
C#側からみればC++のDLLから文字列を取得したい場合、GetName( const char *, int ) としてバッファを渡すよりも、string GetName() な形で、string 型を返して欲しいわけです。そうなると、dllimport の部分も
1 2 | [DllImport( "hellodll" , EntryPoint = "GetNameStr" )] static extern string GetNameStr(); |
な形にしておきたいところですよね。しかし、これはできません。戻り値は string に自動変換してくれないんですね。
不思議なことに、SWIG の場合は戻り値に string を使えて、まあこんな風に string 型を渡したり受け取ったりできます。
1 2 3 4 5 | var hello = new Hello(); hello.SetName( "Tomoaki Masuda" ); var name = hello.GetName(); Console.WriteLine($ "name: {name}" ); |
当然のことながら、これは Hello クラス内でラップしている訳で、
1 2 3 4 | public string GetName() { string ret = helloswigPINVOKE.Hello_GetName(swigCPtr); return ret; } |
となっていますが、dllimport を見ると、下記のようになっていて、string 型を返しています。
1 2 | [global::System.Runtime.InteropServices.DllImport( "helloswig" , EntryPoint= "CSharp_Hello_GetName" )] public static extern string Hello_GetName(global::System.Runtime.InteropServices.HandleRef jarg1); |
dllimport で string 型を返しているので、どういうトリックになっているのかとコードを追ってみると、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 *) のように文字列を渡して貰えばいいのです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #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 で受けます。
1 2 3 4 5 6 7 8 | [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 * に対応するわけです。
1 2 3 | string s = "こんにちは C++ の世界" ; SetNameStr(s); var s1 = Marshal.PtrToStringAnsi(GetNameStr()); |
でもって、いちいち文字列を貰うたびに Marshal.PtrToStringAnsi を使うのは面倒なおで、次のようにヘルパークラスを作っておいて、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 メソッドを呼び出せばよいのです。
1 2 3 4 | 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 のクラスライブラリを作成しておいて、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 のクラスライブラリを参照設定させて実行というスタイルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 を駆使することなく)スマートフォン環境に組み込めるのではないかな、と思ったり。このあたりは、もう少し見ていかないといけませんが。