UnityNativePlugin(Windows向け)を作ってみた

はじめに

スマホエンジニアがUnity4/5向けWindows Dllを作ってみたときの覚書的なアレ
環境はVC2015
なおC++の学習レベルは超久しぶりに触ったのでだいぶ低いです。

C++

久しぶりにC++を触ったらすごく言語仕様が拡張されてた。罠がたくさんあった

lambda

Boost.Asioを使う関係で多用したけどキャプチャが面倒だった。
何も考えずに参照でキャプチャしていったりすると変数の寿命が先に尽きてて死ぬ。
Multithreadが絡むと毎回死ぬわけではなく稀に良く死ぬ感じでわけわかんないバグとなりストレスがマッハ

関数ポインタとlambda

キャプチャ付きlamdaを関数ポインタとして扱おうとするとコンパイルエラーとなり生きるのがつらい
std::bindとかstd::functionを使いたいけどdll exportするときはCなので使えなくてつらい

~_sのみなさん

なんかしばらく見ない間にバッファオーバーラン対策?なのか異様に面倒なことになってた。つらい
strcpy_sの使い方とかいまだによくわかんない。std::string dst(src)ってやっちゃった

Dllのデバッグ

割と楽にやれた。単純にVisualStudioの「ローカルWindowsデバッガー」からUnity Editorに接続すると行けた。
ただクラッシュするとVisualStudioを巻き込んだり、Unity Editorが固まったりして割とストレス
通信とかが絡んで一々ブレイクして見てるのが面倒なところは

void debug( const std::string & log)
{
#ifdef _DEBUG
FILE* logfile;

freopen_s(&logfile, "log.txt", "a", stdout);
if ( logfile != nullptr ) {
std::cout << log << std::endl;
fclose( logfile );
}
#endif
}

こんなのでログ書き込みして対応してた。

テスト

Google Testを利用した。いずれ別ページで書く

Unity

Unityとの繋ぎこみでもいろいろと困った

Unity 4/5

Unity4のエディタは32bit専用、Unity5のエディタは32bit/64bit両方あるけど32bit版はもうサポート終了
ってことで32bit/64bit両方作らざるを得ない。つらい

C APIインタフェイス

Unity5だけをターゲットにするのであれば普通に関数ポインタ書けば行けたんだけど、
どうもUnity4は関数ポインタも__stdcallでないとダメらしくしばらくハマった
(もしかしたら[DllImport( …, CallingConvention = CallingConvention.Cdecl)]的な書き方をすると行けるのかも)

// Unity5ならこれでおk。Unity4だとクラッシュ
__declspec(dllexport) void DllFunction(void(*callback)(int, const char*));

// Unity4/5両対応
__declspec(dllexport) void DllFunction(void(__stdcall *callback)(int, const char*));

// callbackに引数がない場合はこっちでもUnity4も大丈夫
__declspec(dllexport) void DllFunction(void(*callback)());

UnitySendMessage

GameObjectに直接コールバック出来る。つまりMainThreadにデータを返せるって点では便利なんだろうけど、
文字列しか返せないのが辛かったので今回は使わなかった。
Unity5.3以降でOKだったり、JSON系外部ライブラリ使えるんだったら良いんじゃないかな。

Marshalling

intやfloatなどのプリミティブ型や、char*とstringの受け渡しはUnityちゃんがいい感じにアレしてくれてるようだけど
それ以外は自力で書かざるを得ない

struct

structの受け渡しはこんなだった。以下はstructの配列を受け渡す例

Dll側関数定義
struct Item
{
char Id[256];
char Name[256];
int Value;
};

__declspec(dllexport) void DllFunction(void(__stdcall *callback)(Item*, int));
C#側
[StructLayout(LayoutKind.Sequential)]
public struct Item
{
[MarshalAs(UnmanagedType.ByValTStr, SizeCount = 256)]
public string Id;

[MarshalAs(UnmanagedType.ByValTStr, SizeCount = 256)]
public string Name;

public int Value;
}

// dllとの接続
[DllImport("nativedll"), EntryPoint = "GetItems"]
private static extern void DllFunction(Action<IntPtr, int> callback);

// C# API
public static void DllFunction(Action callback)
{
var items = new List();
DllFunction( (ptr, size) => {
if ( size == 0 ) {
callback(items);
return;
}

foreach( var i in System.Linq.Enumerable.Range(0, size -1)) {
var item = (Item)Marshal.PtrToStructure(p, typeof(Item));
items.Add(item);

var p = ptr.ToInt64();
p += Marshal.SizeOf(typeof(Item));
ptr = new IntPtr(p);
}

callback(items);
});
}

こんなので受け渡しが出来た。
Item**を渡したほうがきれいになった気がしないでもないけど元がstd::vectorだったためvector.data()で簡単に取れるこっちになった。

またstructの中身の文字列をchar*にした場合は[MarshalAs(UnmanagedType.LPStr)]になる
今回は文字長がそれほど長くなかったし、メモリ管理が面倒だったので固定長にした

文字列

普通の文字列であればstring / const char*で単純に受け渡せるが、配列を渡そうとすると結構面倒だった
以下はASCIIしか来ないことが分かっていたのでこうしたけどマルチバイト文字が絡むとどうなるのかはちょっと不明
(IntelliSenseに出てたからこれ使うんだろーなーとは思ったけど試してない)

Dll側関数定義
__declspec(dllexport) void DllFunction(void(__stdcall *successCallback)(const char** stringArray, size_t count));

C#側
[DllImport("nativedll", EntryPoint = "DllFunction")]
private static extern void DllFunction(Action<IntPtr, int> callback);

static void DllFunction( Action callback )
{
var list = new List();

DllFunction((ptr, size) => {
if (size == 0)
{
callback(list);
return;
}

foreach (var i in System.Linq.Enumerable.Range(0, size - 1))
{
list.Add( Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(ptr, i * IntPtr.Size)) );
}
});
}