SWIG の文字列(string)の扱いが少しややこしいので、Marshal.* を使ってみるテスト

内部コードを 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 型を戻すときは

  1. C++ 側でここで設定されているデリゲート関数を呼び出す。
  2. C# 側であらかじめ string なデータを作る。
  3. C++ 側から、string なデータのポインタを返す。
  4. これを 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 を駆使することなく)スマートフォン環境に組み込めるのではないかな、と思ったり。このあたりは、もう少し見ていかないといけませんが。

カテゴリー: 開発, C#, C++ パーマリンク