読者です 読者をやめる 読者になる 読者になる

introduction to mono

mono の API を使って C++ から直接 Unity の C# のオブジェクトを触る、という実験を最近やっていたので、その成果を書き残しておきます。
mono を使うことで pinning や marshalling などの余計な処理を介さず C# と C++ を連携させよう、という趣旨です。Unity のプラグインから使う前提で書いていますが、Unreal Engine の場合でもあてはまることは多いと思われます。

mono の組み込みは公式に入門ドキュメントが用意されており、これがいいとっかかりになってくれました。
http://www.mono-project.com/docs/advanced/embedding/
また、突っ込んだことをやろうとするとソースを読む根性が必要になると思われます。Unity の mono はカスタムが入ったものになっています。
https://github.com/Unity-Technologies/mono (本家: https://github.com/mono/mono )

C# から C++ の関数を呼ぶ

C++ の関数を C# に登録するのは何通りか方法があるようですが、ここでは P/Invoke と mono_add_internal_call() を使う 2 通りの方法について触れます。
P/Invoke は、dllexport な関数をそのまま C# 側に取り込むものです。mono の API を使う必要すらなく、一番お手軽です。
P/Invoke では C# -> C++ への暗黙のデータの変換 (marshalling) が行われます。例えば C# の string は内部的に char* に変換して C++ 関数に渡されます。この挙動は MarshalAs attribute を指定することで変更できます。(詳細)

// C++ side
extern "C" __declspec(dllexport) void StaticMemberFunction() { ... }
extern "C" __declspec(dllexport) void MemberFunction(MonoObject *this_cs) { ... }

// C# side
class MyClass
{
	[DllImport("CppDll")]
	public static extern void StaticMemberFunction();
	[DllImport("CppDll")]
	public extern void MemberFunction();

	[DllImport("msvcrt.dll")]
	public static extern int puts(string m);
}

mono_add_internal_call() は、dll やホストプログラム側から明示的に関数を登録する方法です。こちらは暗黙の marshalling は行われません。例えば C# の string はそのまま内部表現である MonoString* として C++ 関数に渡されます。mono の機能に直接アクセスしたい場合はこちらを用います。
以後この記事ではこちら方法で関数を登録している前提で説明します。

// C++ side
void StaticMemberFunction() { ... }
void MemberFunction(MonoObject *this_cs) { ... }
...
mono_add_internal_call("MyClass::StaticMemberFunction", &StaticMemberFunction);
mono_add_internal_call("MyClass::MemberFunction", &MemberFunction);

// C# side
class MyClass
{
    [MethodImplAttribute(MethodImplOptions.InternalCall)]
    public static extern void StaticMemberFunction();
    [MethodImplAttribute(MethodImplOptions.InternalCall)]
    public extern void MemberFunction();
}
class の情報を取得

アセンブリ名、namespace、class 名から型情報 (MonoClass*) を取得できます。これと mono_class_get_* 系 API を用いることでその class のさまざまな情報を取れます。メンバ関数、property、field (C++ でいうメンバ変数) あたりはよく使うことになると予想されます。
ちなみに mono の初期化やアセンブリの読み込みは Unity 本体側が既にやっているので自力でやる必要はありません。

// Transform のメンバ関数一覧を表示
MonoImage *image = mono_assembly_get_image(mono_domain_assembly_open(mono_domain_get(), "UnityEngine")); // UnityEngine.dll アセンブリを get
MonoClass *mclass = mono_class_from_name(image, "UnityEngine", "Transform"); // UnityEngine.Transform の型情報を取得
MonoMethod *method = nullptr;
gpointer iter = nullptr;
while ((method = mono_class_get_methods(mclass, &iter))) {
    puts(mono_method_get_name(method)); // 全 method を巡回して名前を表示
}
C++ から C# の関数を呼ぶ

MonoClass* と mono_class_get_method_from_name() で関数 (MonoMethod*) を得られます。
MonoMethod*、this となるオブジェクト、および引数群を指定して mono_runtime_invoke() を呼ぶことで C# の関数を呼び出せます。

// Transform.position の get/set を呼ぶ例
MonoObject *trans; // Transform コンポーネントであると仮定
...
MonoClass *mclass = mono_object_get_class(trans);
MonoMethod *setter = mono_class_get_method_from_name(mclass, "set_position", 1); // position プロパティの setter。最後の 1 は引数の数で、-1 だと don't care
Vector3 pos = {1.0f, 2.0f, 3.0f};
void *args[] = {&pos};
mono_runtime_invoke(setter, trans, args, nullptr); // trans.position = new Vector3(1.0f, 2.0f, 3.0f)

MonoMethod *getter = mono_class_get_method_from_name(mclass, "get_position", 0); // position プロパティの getter
MonoObject *ret = mono_runtime_invoke(getter, trans, nullptr, nullptr); // ret = trans.position

引数は primitive 型や struct 型の場合、上記例のようにそのポインタを渡します。それ以外、オブジェクトの場合 MonoObject* をそのまま渡します。
mono_runtime_invoke() は内部的に複雑な処理をやってるようなのであまり多用したくないところです。冒頭の公式ドキュメントでは mono_method_get_unmanaged_thunk() という API が紹介されています。C++ から直接呼べる関数ポインタを返してくれる代物らしく、是非使いたいところですが、残念ながら dllexport されていないようで使用は困難です。

Object

C# のオブジェクトは C++ では MonoObject* 型です。
primitive 型や struct 型の場合、MonoObject の後ろにデータがくっつく形になっています。なので、mono_runtime_invoke() で呼んだ C# の関数は MonoObject* で戻り値を返しますが、以下のようにすることでデータを受け取れます。(実際には Mono から提供されている mono_object_unbox() を使った方がいいです。この API も (これの執筆時点では) 内部的に全く同じことをやっています)

// 上の Transform.position 例の続き
MonoObject *ret = mono_runtime_invoke(getter, trans, nullptr, nullptr); 
Vector3 pos = *(Vector3*)(ret+1);

field (メンバ変数) にアクセスしたい場合、mono_class_get_field_from_name() で MonoClassField* を得て mono_field_get_value(), mono_field_set_value() を使います。
その field が primitive 型や struct 型であれば、直にメモリにアクセスすることもできます。MonoClassField* から mono_field_get_offset() で offset 値を取得し、MonoObject* のアドレスから offset 分先のアドレスに値があります。

// C# side
class TestObject
{
public Vector3 test_field;
}

// C++ side
MonoObject *obj; // TestObject と仮定
...
MonoClass *mclass = mono_object_get_class(obj);
MonoClassField *field = mono_class_get_field_from_name(mclass, "test_field")
guint32 offset = mono_field_get_offset(field);
Vector3 value = *(Vector3*)((char*)obj + offset);

C++ 側で C# オブジェクトを new する場合、mono_object_new() で生成した後、自力でコンストラクタを呼びます。

// new Texture2D(128, 128) 相当処理
MonoImage *image = mono_assembly_get_image(mono_domain_assembly_open(mono_domain_get(), "UnityEngine"));
MonoClass *ctex = mono_class_from_name(image, "UnityEngine", "Texture2D");
MonoObject *tex = mono_object_new(mono_domain_get(), ctex);
int width = 128, height = 128;
void *args[] = {&width, &height};
MonoMethod *ctor = mono_class_get_method_from_name(ctex, ".ctor", 2); // コンストラクタ
mono_runtime_invoke(ctor, tex, args, nullptr);

生成したオブジェクトはそのままだと適当なタイミングで GC されてしまいます。これを抑止するには mono_gchandle_new() で GC のハンドルを生成します。メモリの固定化 (pinning) もこの API で行います。 開放してよくなったら mono_gchandle_free() でハンドルを開放します。

// 上のコードの続き
guint32 gchandle = mono_gchandle_new(tex, TRUE); // 第二引数 TRUE で pinning
// ...
mono_gchandle_free(gchandle);
String

C# の string は C++ では MonoString* に相当します。
これは mono_string_to_utf8(), mono_string_to_utf16() で C++ の文字列に変換できます。

// C# side
[MethodImplAttribute(MethodImplOptions.InternalCall)]
public static extern void CppFunction(string str);
...
CppFunction("hogehoge");

// C++ side
void CppFunction(MonoString *str)
{
	char *mbs = mono_string_to_utf8(str);
	puts(mbs);
}

C++ から C# に文字列を渡す場合、mono_string_new() や mono_string_new_utf16() を用います。

// C++ から MonoBehaviour.name = "new_name" する例
MonoObject *this_cs; // MonoBehaviour を継承した何かと仮定
...
MonoClass *mclass = mono_object_get_class(this_cs);
MonoMethod *set_name = mono_class_get_method_from_name(mclass, "set_name");
MonoString *new_name = mono_string_new(mono_domain_get(), "new_name");
void *args[] = { new_name };
mono_runtime_invoke(set_name, this_cs, args, nullptr); // this_cs.name = "new_name"

mono のワイド文字は uint16 に固定されており、たぶんコンパイラによっては wchar_t としては扱えない点に注意が必要です。
また、Mono の文字列は内部的にワイド文字で保持されるため、mono_string_new() や mono_string_to_utf8() よりも mono_string_new_utf16(), mono_string_to_utf16() の方がオーバーヘッドが少ない点も気に留めておいたほうがいいかもしれません。(悩ましい…)

Array

C# の配列は C++ では MonoArray* に相当します。
MonoArray::max_length がその配列の要素数、MonoArray::vector が先頭要素になっており、max_length 分要素が連続しています。よって、以下のようなコードで C# の配列を C++ で巡回できます。

// C# side
[MethodImplAttribute(MethodImplOptions.InternalCall)]
public static extern void CppFunction(Vector3[] v3a);
...
Vector3[] v3a = new Vector3[] { Vector3.one*1.0f, Vector3.one*2.0f, Vector3.one*3.0f, };
CppFunction(v3a);

// C++ side
void CppFunction(MonoArray *v3a)
{
	int size = v3a->max_length;
	Vector3 *data = (Vector3*)v3a->vector;
	for(int i=0; i<size; ++i) {
		printf("{%.2f, %.2f, %.2f}\n", data[i].x, data[i].y, data[i].z);
	}
}

MonoArray は MonoObject を内包しており、型情報も含んでいるため、必要に応じて型チェックを行うこともできます。
C++ 側で配列を生成して C# に渡すには mono_array_new() 一族を用います。

// new Vector3[16] 相当処理
MonoImage *image = mono_assembly_get_image(mono_domain_assembly_open(mono_domain_get(), "UnityEngine"));
MonoClass *cv3 = mono_class_from_name(image, "UnityEngine", "Vector3");
MonoArray *v3a = mono_array_new(mono_domain_get(), cv3, 16); 
Generic Method & Generic Class

generic 関数 (C++ で言うところの template 関数) は実体化しないと使えません。実体化には mono_class_inflate_generic_method() を用います。

// GetComponent<Transform>() を呼ぶ例
MonoObject *this_cs; // MonoBehaviour を継承したオブジェクトと仮定
...
MonoImage *image = mono_assembly_get_image(mono_domain_assembly_open(mono_domain_get(), "UnityEngine"));
MonoClass *ctransform = mono_class_from_name(image, "UnityEngine", "Transform"); // UnityEngine.Transform の型情報
MonoClass *cthis = mono_object_get_class(this_cs);
MonoMethod *gmethod = mono_class_get_method_from_name(cthis, "GetComponent", 0); // GetComponent<T>

MonoGenericInst *mgi = (MonoGenericInst*)malloc(sizeof(MonoGenericInst));
mgi->id = -1;
mgi->is_open = 0; // must be zero!
mgi->type_argc = 1; // generic のパラメータ数
mgi->type_argv[0] = mono_class_get_type(ctransform); // Transform を generic パラメータに
MonoGenericContext ctx = { nullptr, mgi };
MonoMethod *imethod = mono_class_inflate_generic_method(gmethod, &ctx); // GetComponent<Transform> を実体化

MonoObject *transform = mono_runtime_invoke(imethod, this_cs, nullptr, nullptr); // this_cs.GetComponent<Transform>()

ここらへんは説明がほとんどなくて苦労しました。微妙に使い方間違ってる可能性もあります。
MonoGenericInst は実体化された関数が存在してる間は有効な領域にある必要があります。mono 側でスマートに管理してくれる機構があってもよさそうなもんですが見つけられなかったので、この例では自分でmalloc() した領域に置いています。
generic class も同様に mono_class_inflate_generic_class() を使えば実体化できそうに見えるんですが、なぜかこの API は dllexport されておらず、現状困難だと思われます…。


補足事項として、C# では fixed() などで固定していないオブジェクトは GC が勝手にメモリ上の位置を移動させることがある、という仕様がありますが、Unity ではこれが当てはまりません。
Unity に使われている mono には古い GC が使われており、fragmentation 対策がなされていないためです。これは C++ 側が C# 側オブジェクト (のアドレス) を保持しっぱなしでも問題ないということであり、今回に関しては都合がいい方向に働いてくれます。
Unreal Engine の場合、新しい GC が使われているはずなので保持しっぱなしだとたまにマズいことが起きる可能性が高いです。(未検証)

また、Unity では ゲームを開始する度に Assembly が更新されるようで、mono_domain_assembly_open(mono_domain_get(), "UnityEngine") とかで取得したイメージは保持しっぱなしではマズいです。スクリプト部分に関しては、御存知の通り、編集するたびに更新されるので生存期間はさらに短いです。これらは少なくともエディタから実行してる時は必要に応じて Assembly を取得しなおす必要があります。


現在 C++ を Unity のスクリプトとして使うプラグインを作ってるんですが、これのソースがより具体的な使い方の例になるんじゃないかと思います。このプラグインについては、一通り機能を実装してから後日詳細に紹介する予定です。
https://github.com/i-saint/UnityCppScript
mono を触ってみて、C# はスクリプト言語としてなかなか悪くないかもしれない、と思うようになりました。


[edit: 2014/10/27] P/Invoke についての説明に不足があったので追記 (暗黙の marshaling 関連)
[edit: 2014/12/10] Object の項目に GC を抑制する方法を追記。いくつか細かい補足追加