1. はじめに
ご存知の通り、Unityはマルチプラットフォームアプリケーションを実現するための優れた方法の一つです。特に、Android / iOS向けのモバイルマルチプラットフォーム開発において高い効率を発揮することが知られています。しかし、高いパフォーマンスの求められる処理やバイナリデータの操作、あるいは歴史的経緯といった理由からC/C++製のネイティブプラグインが必要となる場面も少なくないのではないでしょうか。
そこで、本記事ではUnity層とC/C++層のソースコードを共通化しつつ、マルチプラットフォームでのビルドを実現するための導入方法について紹介していこうと思います。基本的に、筆者自身が実現するにあたって調べたことや実験したことなどのまとめですので、経験則に基づいた内容も多くなっております。突っ込みどころがありましたらぜひコメントいただけますと幸いです。
1.1. 本記事で説明すること
- 対象プラットフォームは下記
- Android / iOS / Unityエディタおよびスタンドアロン(Windows / MacOSX)
- C/C++ソースコードをUnity用ネイティブプラグインとしてビルドする際の注意点
- C#とC/C++をつなぐインターフェース部分の実装例
1.2. 本記事で説明しないこと
- ネイティブプラグイン自体の実装に対するノウハウ
- C/C++コードを各プラットフォーム用にコンパイルする際の詳細な手順
1.3. 環境
- Unity 2018.2
2. ネイティブプラグインのビルド
まずC/C++製のソースコードを各プラットフォーム向けにビルドします。詳しい手順については割愛しますが、AndroidではAndroid NDKを用いてsoとして、iOSではXcodeを用いてframeworkとして、WindowsではVisualStudioを用いてdllとして、MacOSXではXcodeを用いてbundleとしてビルドすることでUnityにネイティブプラグインとして読み込ませる事ができます。
C/C++製ソースコードをUnity向けのネイティブプラグインとしてビルドする場合、Unityから呼び出す関数の定義は以下の3点について注意する必要があります。
- 名前マングリングを回避する
- 他のネイティブプラグインと衝突しない名前を定義する
- Windows向けにビルドする場合、動的リンクライブラリとして作る
2.1. 名前マングリングを回避する
ソースコードがC++で作られている場合、UnityはC言語に基づいたコールインターフェースによってプラグイン関数を呼び出すため、名前マングリングの問題を解決する必要があります。名前マングリングとは、オーバーロード関数や、異なるスコープ内での同様の関数定義を可能にするための仕組みで、コンパイルの際にコンパイラが名前空間や関数の引数型などを含めた関数名を生成する方法です。一方で、この名前マングリングが行われるとコンパイラによって動的に生成された関数名となるため、Unity層からプラグイン関数名を特定できなくなってしまいます。そこで、 extern "C"
宣言を関数定義につけてあげることで、名前マングリングを回避してUnity層から呼び出すことが可能になります。 extern "C"
は言語リンケージと呼ばれる指定子で、この指定を行うことでC言語から呼び出される関数であることをコンパイラが理解し、名前マングリングを行わずにコンパイルするようになります。なお、関数に対する言語リンケージの指定は戻り値や引数の型、関数ポインタを持つ全てのパラメータにも適用されるため、それらにC言語としての仕様の範囲で制限を受ける点には注意が必要です。
extern "C" {
void hogeFunction();
int fugaFunction(int);
}
2.2. 他のネイティブプラグインと衝突しない名前を定義する
前項で述べたとおり、名前マングリングを回避した事により同名の関数を定義することはできなくなりました。また、iOSではネイティブプラグインは静的ライブラリとしてリンクされるため、他のプラグインと名前の重複があった場合に想定外の動作を起こす可能性があります。そのため、関数名は可能な限りグローバルに重複しない命名にしておくことが望ましいでしょう。
一例として、弊社ではJNIの命名則にならった形で ドメイン名_クラス名_メソッド名
と定義しています。これはUnityや.NET界隈での慣例や一般的な命名とは異なるかもしれませんのでご注意ください。(ご存知の方おりましたら、ぜひコメントいただけると助かります!)
extern "C" {
void jp_co_navitime_NativeSample_hogeFunction();
int jp_co_navitime_NativeSample_fugaFunction(int);
}
2.3. Windows向け動的リンクライブラリとして作る
UnityはiOSを除きネイティブプラグインを動的リンクライブラリとして読み込みます。そのため、Windows向けネイティブプラグインをビルドする際には、DLLとして公開する関数定義に対して __declspec(dllexport)
宣言を付加する必要があります。ただし、この宣言はWindows以外のプラットフォーム向けでビルドする際にも付いているとエラーとなってしまうため、宣言をマクロ定義としWindows環境以外では空となるようにします。ここでは、プリプロセッサシンボルとしてWindows環境下では UNITY_WINDOWS
と設定するようにして回避しています。また、ネイティブプラグインが複数ソースファイルに及ぶ場合は共通ヘッダに定義するようにすると良いでしょう。
#ifdef UNITY_WINDOWS // Windows環境でのみ定義
#define NATIVE_API __declspec(dllexport)
#else
#define NATIVE_API
#endif
#include NativeCommon.h
extern "C" {
NATIVE_API void jp_co_navitime_NativeSample_hogeFunction();
NATIVE_API int jp_co_navitime_NativeSample_fugaFunction(int);
}
3. Unityへの取り込み
ここまでで、各プラットフォーム用のネイティブプラグインが用意できました。Unityから呼び出せるようにするにはアセットとして取り込む必要がありますので、各プラグインをUnityプロジェクトへ追加していきます。Unityでは一部のアセットのためのディレクトリ命名が予約されており、プラグインもその一つで Assets/Plugins/
がプラグインアセット用のディレクトリとなります。Plugins配下にさらに各プラットフォームごとのディレクトリを作り、それぞれ適切な場所へ配置を行います。
- Android
- Android用プラグイン(.so)を配置。AndroidではさらにCPUアーキテクチャごとにディレクトリが別れている。
- iOS
- iOS用プラグイン(.framework)を配置。
- x86
- 32bitのUnityエディタおよびスタンドアロン用プラグイン。32bit Windows(.dll)のものを配置。
- x86_64
- 64bitのUnityエディタおよびスタンドアロン用プラグイン。64bit Windows(.dll)とMacOSX(.bundle)のものを配置。
3.1. インスペクタ設定
ネイティブプラグインの配置が完了したら、各プラグインのインスペクタ設定を行います。各プラグインに対して、適切なプラットフォームとCPUアーキテクチャを選択し、Applyボタンを押して決定します。この際、Unityエディタにそのプラットフォームのビルドモジュールが追加されていない場合、選択可能な一覧にそのプラットフォームが表示されないため注意が必要です。
4. ネイティブプラグインの呼び出し
これでUnityプロジェクトへのネイティブプラグインの取り込みが完了しました。最後に、実際にUnityのC#コードからネイティブプラグインを呼び出す方法について解説します。ネイティブプラグインの関数は、C#コード上で [DllImport]
属性を付けたexternメソッドとして宣言することで呼び出すことが出来ます。DllImportでは通常は読み込むライブラリの名前を指定しますが、iOSでは静的ライブラリとしてリンクしているためライブラリ名として "__Internal" を指定しなければなりません。そこで、プラットフォーム依存コンパイルの仕組みを使ってプラットフォーム別にライブラリ名を定数定義しておくことで、実際にDllImportする場所での記述を共通化することができます。このとき、 static readonly
ではなく const
で定義する必要があることに注意してください。また、デフォルトではexternメソッド名とネイティブ関数名は一致してる必要がありますが、EntryPointとして関数名を指定すればメソッド名は自由に定義することができます。これで、C#からネイティブプラグインの関数を呼び出すことが出来るようになりました。
public class NativeSample
{
// Unityエディタ上でもビルドターゲットをiOSにしているとUNITY_IOSがtrueとなるため
// iOS実機ビルド時のみ __internal 読み込みとなるよう指定
#if UNITY_IOS && !UNITY_EDITOR_OSX
public const string LIB_NAME = "__Internal";
#else
public const string LIB_NAME = "NativeSample";
#endif
[DllImport(NativeSample.LIB_NAME, EntryPoint = "jp_co_navitime_NativeSample_hogeFunction")]
private static extern void hogeFunction();
[DllImport(NativeSample.LIB_NAME, EntryPoint = "jp_co_navitime_NativeSample_fugaFunction")]
private static extern int fugaFunction(int arg);
public static void HogeFunction()
{
hogeFunction();
}
public static int FugaFunction(int arg)
{
return fugaFunction(arg);
}
}
5. まとめ
以上、Unityでマルチプラットフォームなネイティブプラグインを導入する方法について紹介しました。Unityネイティブプラグイン周りはまだまだまとまったナレッジが少ないと感じております。私自身がそうであったように、ネイティブプラグインの導入に苦労している方の一助となれることを願っています。
6. 参考
ネイティブプラグイン - Unityマニュアル
名前マングリング(C++のみ) - IBM Knowledge Center
dllexport, dllimport - Microsoft Docs