UnityのNative Pluginを作ってC++のコードやライブラリを使う

概要

UnityといえばC#で開発するのが基本ですが、Native Pluginという機能を使えば、他の言語 (C++など) で書かれたコードを呼び出すことができます。

今回はじめてNative Pluginを作ってみたので、調べたことなどをメモしておきたいと思います。


f:id:yuki-koyama:20151118191833p:plain
C++のライブラリ (Eigenlibigl) を用いてUnity上でメッシュのガウス曲率を可視化した例

背景と動機

普段はC++でコンピュータグラフィクス関連の研究をしています(プロダクトの開発は行なっていません)。私がC++を使う主な理由は

  • 過去に自分で書いたコードを使いまわしたいから
  • 優秀な数学演算のライブラリや高度なジオメトリ処理のライブラリを使えるから
  • 高速化がしやすいから

などです。一方でUnityを使うこともあります。私がUnityを使う際の主な理由は

  • 優秀なリアルタイムレンダリングが予め用意されているから
  • Unity Asset Storeから優秀なモデルデータを入手できるから
  • インタラクションの実装が (C++に比べて) 容易だから

などです。

つまり、技術自体を作り込むにはC++が便利だけど、技術デモを作り込むにはUnityが便利なわけです。このトレードオフを解決する手段として、C++で開発した技術をUnityのNative Pluginとして呼び出すという方法を試してみることにしました。

環境

今回は複数アーキテクチャへのデプロイについては考えません。

C言語でインタフェースを作る

C#からC++で作ったNative Plugin (動的ライブラリ) の機能を呼ぶには、C言語の関数でやりとりするようです。例えばC++側では以下のようなC言語の関数を定義しておきます:

extern "C" {
    void* InstantiateMesh(const int* triangles, const double* vertices, int numberOfTriangles, int numberOfVertices);
    void DeleteMesh(void* mesh);
}

なおextern "C"という宣言はC++Name Manglingを回避するために必須なのだそうです。上記の関数は、C#側で以下のように宣言することで使用可能になるようです:

[DllImport ("PluginName")]
private static extern IntPtr InstantiateMesh(IntPtr triangles, IntPtr vertices, int numberOfTriangles, int numberOfVertices);

[DllImport ("PluginName")]
private static extern void DeleteMesh(IntPtr mesh);

intやdoubleは両言語共通で使えるようです。IntPtrとはポインタのことだそうです。

Xcodeでバンドルをビルドする (Mac OS X)

上記のサイトに従ってバンドル (.bundle) を作成し、UnityプロジェクトのAsset以下の適切な場所に配置します。

この際の注意点は、target architecturesの設定です。Unity 4のエディタは32-bitで動いているようなので、32-bit用のバンドルを作成する必要があります。その場合はi386を指定します。また、Unity 5は64-bitにも対応しているそうです。その場合はx86_64を指定します。これは意外に面倒な問題です。

ここで一番良いと思われる方法は、公式マニュアルでも言及されている通り、Mac OS Xについてはいつでもユニバーサルバイナリを作ることです。ユニバーサルバイナリとは複数のアーキテクチャで使えるバイナリです。Xcodeでもそのような選択肢が用意されています。


f:id:yuki-koyama:20151118192017p:plain
Xcode上でユニバーサルバイナリを指定

もう一つ注意点としては、既存のビルド済みスタティックライブラリを用いてユニバーサルバイナリをビルドするには、既存のライブラリもまたユニバーサルバイナリとしてビルドされている必要があります。例えばHomebrewで

$ brew install boost

などのコマンドでライブラリ (ここではboost) をいれている場合は、普通ユニバーサルバイナリとしてビルドしてくれていません。この問題を解決するには

$ brew reinstall boost --universal

とすることでユニバーサルバイナリとしてビルドしなおしてくれるようです。

なお

$ lipo -info (ライブラリへのパス)

などとすると既存のライブラリがどのアーキテクチャ向けにビルドされているか確認することができます。

ただしHomebrewのオプションを指定する方法は(なぜか)いつでもうまくいくわけではないようで、いくつかのライブラリについてはユニバーサルバイナリを作ってくれないようです。自分の場合はCGALというジオメトリ処理用のライブラリをHomebrewでいれようとしても、うまくユニバーサルバイナリを作ってくれませんでした。そこでHomebrewを諦めてCMakeでCGALをビルドすることにしました。CMakeでは

CMAKE_OSX_ARCHITECTURES=x86_64;i386

と指定するとユニバーサルバイナリを作ってくれます。

C#からC++へ配列を渡す

配列のやりとりは基本的にはポインタのやりとりになりますが、C#にはmanagedとかunmanagedとかいう概念があるそうで(C#に疎いので知りませんでした)、ちょっと工夫が必要です。例えばint型の配列をC#からC++に渡すには以下のようにするようです:

int[] triangles = mesh.triangles;
IntPtr unmanagedTriangles = Marshal.AllocHGlobal(triangles.Length * sizeof(int));
Marshal.Copy(triangles, 0, unmanagedTriangles, triangles.Length);
// ここでunmanagedTrianglesを使ってC++の関数を呼ぶ
Marshal.FreeHGlobal(unmanagedTriangles);

このコードは手元では動きましたが、ググると色々な人が色々な方法を提示しているので、これが正解ではないかもしれません。

C++からC#へ配列を渡す

これもmanagedとかunmanagedとかいう概念を考慮する必要があるらしく、例えばdouble型の配列を受け取るには以下のようにすれば良いようです:

IntPtr tempPtr = IntPtr.Zero;
int arrayLength = 0;
SomeFunctionInNativePlugin(ref tempPtr, ref arrayLength);
double[] array = new double[arrayLength];
Marshal.Copy(tempPtr, array, 0, arrayLength);
// ここで何らかの手段でtempPtrが指すメモリ領域を解放する

ちなみにC++側で配列の実体をメモリ上に確保しているはずなので、あとで必ずC++側でメモリを解放する必要があります。

C++のクラスをC#で使う

上記のようなラッパーを書けばC#側からC++のクラスをインスタンス化したりメソッドを呼んだりできそうです。ただしこれはかなり面倒そうです。誰かラッパーを自動生成するスクリプト書いてください。

C++C#で構造体をやりとりする

かなり面倒くさそうです。

余談:コンピュータグラフィクスの研究にどう使うか

コンピュータグラフィクスの要素技術研究に伴う開発では、もちろん研究内容によりますが、頻繁に仕様変更がおきたり、様々な実装の可能性を試行錯誤していく必要が生じます。そういった研究の初期段階では、C++とUnityを組み合わせた開発スタイルではラッパークラスなど本質的でないコードの変更が大量に発生するため、かなり効率が悪そうだと感じました。やはりC++でテスト用のレンダラを実装したりテストシーンを構築したりする方が良さそうです。技術の内容が完全に確定し、論文を書いたり人に見せるためのデモ用シーンを作ったりする段階になって初めてUnityと組み合わせるというのが現実的な選択肢だと思いました。

最後に

間違い・意見などありましたらご連絡いただけると幸いです。

*1:TwitterでUnity 4 Proを使っていると呟いたところ、複数人からUnity 5 (non Pro) に移行した方が良いと勧められました。Unity 4を使っている人は特別な理由がない限りいますぐUnity 5に移行した方が良さそうです。