TechCon2017 - Unityネイティブプラグインマニアクス(大竹悠人&山内沙瑛)

2017年2月10日に開催された「TechCon2017」のなかで、ゲーム事業本部開発基盤部の大竹悠人と山内沙瑛によるセッション「Unityネイティブプラグインマニアクス」が行われました。Unityの機能拡張を実現するネイティブプラグインの概要から作り方と注意点まで技術的なトピックを中心にお話しました。

トピックス一覧

1. 自己紹介
2. ネイティブプラグインとは
3. ネイティブプラグインの作り方
4. プラグイン作成時の注意点と対応策
5. まとめ

自己紹介

Unityネイティブプラグインマニアクス

大竹:はじめまして。
このセッションではJapanリージョンゲーム事業本部開発基盤部の大竹悠人と山内沙瑛がUnityネイティブプラグインマニアクスという題で発表させていただきます。
この会場のセッションのうち、僕らのだけ空気読んでないタイトルになっているんですけど、気にせずにやってこうと思います(笑)。

Unityネイティブプラグインマニアクス

まず自己紹介させてください。
僕なんですけど、大竹悠人といいます。開発基盤部の第2グループという部署に所属していて、この会社には中途入社で2013年に入っています。前職ではいくつかのWebサービスや家電やゲーム機向けのプロダクトというちょっと変わったものを担当していて、2013年からDeNAに入りました。
最初のうちWebソーシャルゲームの運用などをした後に、新規ネイティブゲーム開発などをやっていって、その間に基盤整備みたいなことを結構やっていたのでいつの間にかそっちが本業になってました。
現在はUnityに関連したタイトルの技術サポートや、Unity製のさまざまな内製ライブラリの製作や保守をやっていますという感じです。

Unityネイティブプラグインマニアクス

山内:私は山内沙瑛といいます。
開発基盤部の第3グループに所属していて、2012年に新卒で入社しました。これまでは主に共通基盤の開発に携わっていて、UIテストの自動化やゲーム用のSDKのメンテナンス、あとはUnity共通モジュールの開発・メンテナンスおよび調査等を行ってきました。直近では、ゲーム用BaaSのSDKおよびサーバーサイドの開発・運用を行っています。

Unityネイティブプラグインマニアクス

本発表のアジェンダです。
まずネイティブプラグインとは、という点について、なぜ必要なのかというところを含めてご紹介していきます。
次にネイティブプラグインの作り方について、ざっくりとした説明になるのですが、どういうものでどういう実装が必要なのかというところを紹介していきます。
そして最後にプラグイン作成時の注意点と対応策を紹介します。本発表のメインのコンテンツはこちらになります。
マルチプラットフォーム対応ですとか安定性、実機上での実行効率についてお伝えできればと思っています。この資料は後日公開する予定です。

ネイティブプラグインとは

Unityネイティブプラグインマニアクス

それではネイティブプラグインとは、という点について説明していきます。
ネイティブプラグインとは、Unityの機能を拡張するためのネイティブ実装ライブラリのことです。ネイティブ実装とインターフェースを用意することで相互に呼び出すことができるようになります。本発表ではUnityのネイティブプラグインのことをネイティブプラグインと呼んでいきます。
右側のほうに図で例を書いているんですが、ゲームコードとネイティブコード、例えばOSSのライブラリであるとかを相互に呼び出すことができるような仕組みです。

なぜ必要なのか

Unityネイティブプラグインマニアクス

それでは、なぜ必要なのかという点について、説明していきます。
大きく分けると以下の理由があります。
一つ目はUnityが提供していないようなプラットフォームのAPIを使用するためです。
二つ目はネイティブで実装された機能をUnityでも利用するためになります。

Unityネイティブプラグインマニアクス

まず、この一つ目のUnityが提供していないようなプラットフォームAPIを使用するためについて具体的に説明していきます。
例えば、iOSのPush通知や動画再生等はゲームコードから直接呼び出せることができるんですね。ただ、下のほうに書いてある例のように、こちらのWebViewの表示であるとかAndroidのPush通知設定であるとか、あとは空きディスクの容量を取得したいというような、プラットフォームのAPIを使いたいとき、Unityがそもそも対応してないという場合は直接呼び出すことができません。そのため、ネイティブプラグインとして作成して直接呼び出すことが必要になってきます。

Unityネイティブプラグインマニアクス

そして二つ目の理由は、技術資産を流用するためです。
ネイティブ実装されたミドルウェアや機能をUnity上で使用することで、Unityというプラットフォームによらず作成したようなミドルウェアをUnity上でも利用できます。
具体的な例として、DeNA社内のモジュールであればゲームに特化したBaaSであるとか、サウンドエンジンであるとか、あとはリアルタイム通信サーバーやアセット転送の仕組み等があります。
OSSの場合はWebPのローダであるとか、ファイルの暗号化・複号化等をネイティブプラグインとして用意することでUnity上でも使用することができるようになります。

ネイティブプラグインの作り方

ネイティブプラグインとしての実装

それではネイティブプラグインの作り方について、ネイティブプラグインとしてどういう実装が必要なのか、まずは全体像をざっくりお伝えしたいと思います。

Unityネイティブプラグインマニアクス

まず、ネイティブ実装とそれを呼び出すためのインターフェースを用意します。
これで相互に呼び出すことができるようになります。右側の図にも書いてあるとおり、まず一つ目、上のほうのマネージドコードについてなんですが、これはゲームコードC#に当たります。本発表では、ゲームコード、マネージドコードは全てC#の実装前提ということになります。
そしてこのゲームコード、Unityのビルドの仕組みで、monoでビルドした場合は、monoの仮想マシン上で実行されるコードになります。これをIL2CPPビルドという、これもUnityのビルドの仕組みで、中間コードをC++のコードとして変換しビルドすることになるのですが、この場合は最終的にネイティブコードになります。
それに対して下の緑色のほうに書いてあるネイティブコード、アンマネージドコードとも書いてあるんですけど、これはCやC++、Java等で記述されたコードのことです。ここでJava等もネイティブコードに含めているんですが、これはプラットフォームに対してネイティブなコードということで含めています。
ネイティブプラグインはそもそもゲームのコード外からプラットフォームのAPI等、ライブラリ等を直接呼び出すもので、今回の発表ではネイティブコードとしてまとめて取り扱っていきます。

Unityネイティブプラグインマニアクス

それでは全体の実装がそもそもどういうものなのかというところ、まずはお見せしていきます。
これはAndroidの例になるんですが、下のlibnative.soという緑色の所で、一番下にaddという関数を定義してあります。
この関数に対してどういうふうにC#(マネージドコード)から呼び出すかといいますと、真ん中の図の青色の上のほう、P/Invoke宣言と書いているこちらに下のaddという関数と同じ関数シグネチャを持ったもの(メソッドシグネチャ)を持ったものを定義します。
そしてそこにexternという指定とあとはライブラリのlibnative.soというライブラリを用意しているので、これに対してnativeという名前、DllImportでライブラリ名を指定します。
この宣言を追加することで一番上のようにゲームコード上からaddという関数を直接呼び出せるようになります。
ちなみにこのDllImportという指定をしているのはAndroidなのでこのような指定をしています。iOSの場合は__Internalという指定になったりもします。これは後ほど詳しく説明します。

必要な実装

Unityネイティブプラグインマニアクス

今ざっくり全体像をお伝えしたんですが、それぞれ何が必要なのかもう少し詳しく説明していきます。
大きく分けるとこのA、B、C、D、の四つになっています。
まず一つ目は、A.ネイティブ実装で拡張機能のコア実装になります。
二つ目は、こちらもネイティブ実装なんですけれどB.マネージドコードから呼び出すためのインターフェースです。
そして三つ目、マネージドコード側ではC.ネイティブコードとの連携実装、最後に四つ目がマネージドコード側でD.利用者向けの呼び出しフロントエンド実装となります。
先ほどお見せした全体像のところでは、D.利用者向けのフロントエンドの部分は省略させていただいていますが、後に詳しく説明していきます。

Unityネイティブプラグインマニアクス

それではこのネイティブ側で必要な実装、AとBはどういうものなのかというところを具体的に説明していきます。

Unityネイティブプラグインマニアクス

まずネイティブコード、これは拡張機能のコア実装で、つまりライブラリ本体ですね。ゲームエンジンをまたいで利用可能な技術資産にできます。Unityに関係なく作っているものだからですね。
それに対してインターフェースを用意する必要があります。これはマネージドコードから呼び出すためのインターフェースです。これを呼び出すためにはC Linkageの関数として実装する必要があります。
例としてtest.cppという形で書いているんですが、これは例えばhogeという関数を定義した場合、extern “C” 宣言を付けてあげないといけません。これはもちろんその呼び出し部分、インターフェースはC Linkageの関数として実装する必要があるだけなので、実装自体はもちろんC++で問題ありません。
マネージドコード実装については、大竹のほうから説明していきます。

Unityネイティブプラグインマニアクス

大竹:それではまずはマネージドコード側での、ネイティブコードとの連携実装についてです。
これは先ほどのネイティブコードでのマネージドコード向けのインターフェースと対応する部分ですね。連携の方法として、以下のようなものがあります。

Unityネイティブプラグインマニアクス

マネージドコード上で書くものとして、先ほどから例に出てたようなコード、externとかDllImportを指定したコードなんですけど、これがP/Invokeと呼ばれる仕組みになっています。先ほどのようにネイティブコードをC#のメソッドのように呼び出すことが可能になる機構です。
またもう一つ、Unityによって用意されている仕組みとしてAndroidJavaObjectというのがあります。
これは当然Androidでしか使えないんですが、JNIを経由してAndroidのJavaのコード、JARだったりSDKのコードとかを呼び出すための機構です。
またちょっとずれはするんですが、ネイティブコード上からマネージドコード側へのデータ手段の一つとしてUnitySendMessageというのもあります。
これはネイティブコードからマネージドコードに文字列を特定のGameObjectに送り付けるみたいなメソッドになっています。

Unityネイティブプラグインマニアクス

順番に説明していきます。
まずP/Invokeについてです。これは正式名称としてはPlatform Invokeと呼ばれている機能で、C#のCLIの共通言語基盤の機能のうちの一つです。
これを使うことで、ネイティブコードをマネージドコードのように呼び出すことができます。コード例として出ているのは、先ほどから例示しているものと同じような内容です。
実行する際のこととしてここで説明しておきたいのは、引数に渡す値だとか返り値として戻ってくる値には、必要に応じてですがマーシャリングという処理が挟まります。
マーシャリングというのは例えば、intとかだったらそのまま渡せばいいんですが、文字列を渡したい場合、C#内での表現というのはSystem.Stringという型になります。
それをネイティブ側に文字列として取り扱いたくて——例えばcharのポインタで受け取りたいです、というコードだったとします。
そのときC#中のStringの表現は、そのままNULL終端のポインタになっているというわけでもなく、あまり見えないものになってます。なのでそのまま受け渡すことができないので、例えばStringの文字列をマネージドからネイティブに受け渡す場合であれば、マーシャラというものがマーシャリングと呼ばれる処理を行い、このSystem.Stringの内部の文字列表現をネイティブコード側に適合した形にします。
もしここでcharのNULL終端ポインタとして受け渡したいのなら、そのように指定しておくことでデータコピーを行ってネイティブコード側から呼び出せるようにしてくれます。
これは、引数の変数の変換だけじゃなくてdelegateとかを受け取ってコールバックとしてネイティブ側から呼び出すということも一応可能です。

Unityネイティブプラグインマニアクス

次にAndroidJavaObjectについて説明します。
これはAndroidのAPI変数、SDKのAPIや自分で作ったJARなどに含まれるJavaのコードを呼び出すこともできるUnityのAPIです。マネージドコード中からまず文字列でClassを指定してリフレクションみたいな形で呼び出すことができます。
参考資料としてUnityのマニュアルに載っている呼び出し例を載せてみました。これは何をするコードかというとjava.util.LocaleのインスタンスメソッドであるgetDisplayLanguageをDefaultのインスタンスから取ってStringでLogに吐き出したいというコードです。
ここでAndroidJavaClassというのが出ているんですが、これに文字列でClassのネームスペース込みの名前を渡してあげることでClassへの参照を取ることができます。
それに対しCallStaticというメソッドで、getDefaultというのを呼ぶことで、これの返り値が結局java.util.Localeのインスタンスになっているので、AndroidJavaObjectという型でまず受け取っています。そしてgetDisplayLanguage をString指定でコールすることでStringで結果を取っています。
このコードがusing で囲ってあるのはJNIで使う都合上、Java内でのGCへのリファレンスというのを持たなければいけないので、それを解放するためにusingを挟んでいるという形になります。
これを呼ばないと、確保したやつがいつまでも残り続けてしまうという可能性があるので、忘れず呼ぶようにしなければいけません。

Unityネイティブプラグインマニアクス

次です。UnitySendMessageです。
これはちょっと前に挙げた二つとは経路が違うんですが、ネイティブコード側からマネージドコード側に今までとは逆の方向に情報を渡すための仕組みです。
例にあるようなUnitySendMessageという関数としてUnity側が用意してるヘッダファイルに記述されています。
GameObjectName,MethodName,Message to sendの三つを投げてるんですけど、第1引数にGameObjectの名前を指定して第2引数にMethod名を指定します。
そうするとそのGameObjectに付いているいずれかのコンポーネントのMethodNameの名前を持ったMethodに対して第3引数のメッセージが文字列として送られます。
送れる情報がStringだけだとか制約があるので結構使い勝手は悪いのと、あと問題点として、これ実は非同期に呼ばれるというか、すぐに結果がUnity側に返ることが保証されていなくて、例えばネイティブコードをネイティブプラグイン経由で呼び出してその結果をUnitySendMessageで返したとしても、その結果をUnity側から認識できるのは次のフレーム以降ということになります。結構その辺の使い勝手が悪いので、使う場合でもネイティブで一定処理を行った後に違うタイミングで結果を返したいような場合に限って使用するってことが多いです。

Unityネイティブプラグインマニアクス

最後に利用者向けの呼び出しフロントエンドの実装についてです。
今まで紹介してきたようにP/Invokeで書く場合なんですけど、C Linkageのインターフェースで書かなきゃいけないのと、P/Invoke宣言というextern指定をした上で、またStaticMethodでないといけないという制約があります。

利用者向けフロントエンド実装

Unityネイティブプラグインマニアクス

また扱う内容によっては、この例でHogeというモジュールを考えてあるんですけど、createというメソッドでなんか値を渡して内部的にリソースを何かしらヒープに加工してそれを使ってExecという処理ができるようになっていて、最後に片付けるときにremoveを呼んでくれという制約のあるモジュールだと考えます。こういうときにintポインタとかポインタを示す型とかが表出してる場合があったりとか、そもそもそのリソースの管理の都合上、これをデベロッパー側に結構対応付けてちゃんと呼んでくれというのも結構酷なものがあるかと思います。
なので、実際に提供する場合には、この左のやつを右の図のように一層ラッパーみたいなものをC#側で挟んで実装するのが望ましいです。
このパターンだと最初にリソースを確保して最後にちゃんと解放できることというのをC#的な文脈で保証しやすくするためにIDisposableインターフェースを実装した形で表現しています。コンストラクタでcreateメソッドを呼んでintポインタでリソースへのポインタをメンバーとして持っておいてDisposeで最後に破棄できるようにする。使ってる間にはそれを使ってExecを実行できるようにしておくという形です。こういうようなインターフェースを用意してあげることで利用側が違和感なく使えるようになります。
ここからはネイティブプラグインの作成時の注意点と対応策について山内から順に説明してもらいます。

プラグイン作成時の注意点と対応策

Unityネイティブプラグインマニアクス

山内:それではネイティブプラグイン作成時の注意点と対応策についてです。
ここではマルチプラットフォーム対応、安定性、実機上の実行効率について大まかに説明していきたいと思っています。ここでの話は、私たちが実際に実装していく中で出会った問題や、それに対する対応策や、たまった知見等を具体的に紹介していきたいと思っています。

まずはマルチプラットフォーム対応について、ここに挙げた四つの項目、各プラットフォーム用のライブラリ対応であるとか、ネイティブプラグイン呼び出し実装であるとか、あとはライフサイクルについてや、最後にネイティブコードからマネージドコードを呼び出す実装の注意点について、私のほうから説明していきます。

マルチプラットフォーム対応

Unityネイティブプラグインマニアクス

まずは各プラットフォーム用のライブラリ対応についてです。
この場合、各プラットフォーム用、例えばiOS、Android、macOS、Windows用に実装したライブラリを用意する必要があります。
ここでmacOSやWindows用のネイティブプラグインを用意するというお話をしているのはUnity Editor上でも動作をさせるためになります。
例えばアプリ上で動作をさせることが必要であれば二つだけではいいんですけど、ここでUnity Editorの対応もしていないと実行するためにいちいちビルドしなければならないとなってしまって、開発効率が下がってしまうのでUnity Editor上での確認もするために、この四つのプラットフォームに対応していきます。
この各プラットフォームでそれぞれ必要なライブラリの形式は異なります。これは表のとおりなんですが、iOSであれば Static Library、AndroidであればShared LibraryまたはJARでJNI経由、macOSであればLoadable Bundle、WindowsであればDynamic Link Libraryの形で用意する必要があります。
ここで一つ注意しておいてほしいのが、iOSだけはStatic Libraryで、他の残り三つのプラットフォームはShared Libraryであるということ。
ここだけこの後出てくる話でコードの例が変わってきたりするので、少し頭の片隅に覚えておいてください。

Unityネイティブプラグインマニアクス

それでは各プラットフォームごとのネイティブコード呼び出し実装についてです。
先ほど例示したように各プラットフォームごとに必要なライブラリが異なってくるので、それ用にP/Invoke宣言の対応も必要になります。そしてその分ライブラリごとのDllImportが異なるのでプリプロセッサディレクティブというもので分岐させる必要があります。この例は後ほど説明します。
加えてIL2CPPビルドを行う場合は以下の対応も必要になってきます。iOSとAndroidのIL2CPPの場合であれば、Delegateを使ってネイティブコードからマネージドコードを呼び出す関数に対してはMonoPInvokeCallback属性というもの指定しなければなりません。そしてAndroidのIL2CPPにおいては、ビルドエラーを回避するためにこのDLLImport("__Internal")という宣言を有効にならないようにする必要があります。これは少し前もお伝えしたとおり、iOSの場合はStatic Libraryなのでこのような宣言が必要なんですね。これをAndroidのIL2CPPビルドのときに無効になるようにしておかないと、ビルド時に静的リンク前提のコードが形成されてしまってビルドエラーになります。

Unityネイティブプラグインマニアクス

プリプロセッサディレクティブの記述例を簡単に示しておきます。
こちら一番下に書いてあるaddという関数を宣言する場合なんですが、各プラットフォームごとにこのように分岐が必要になります。
Unity Editorの場合はnativeForEditor、Unity Androidの場合はnative、Unity iOSの場合は__Internalという指定に分けています。これは先ほどもお伝えしたとおり、iOSはこういう指定が要ります。それに対してEditor用の指定とAndroid用の指定でライブラリを分けていた場合は、このように二つ書く必要が出てきます。もし名前を一緒にすればここiOSとそれ以外という様に二つにすることはできます。

Unityネイティブプラグインマニアクス

次にネイティブプラグインのライフサイクルについて。
ネイティブプラグインのロードタイミングに注意して実装していないと、後々はまることになります。
まずはAndroidの場合から説明していきます。Androidの場合、ロード順序が担保されません。これはネイティブプラグインで複数のライブラリがある場合、そこに依存関係がある場合の話なんですが、AndroidのOSバージョンによってはライブラリの依存関係が解決されません。
そしてJNI_OnLoadが呼び出されない場合があります。JNI_OnLoadというのは、Javaの仕組みで共有ライブラリの読み込み時に呼び出される関数です。このJNI_OnLoadの中で、例えばライブラリをロードするような処理を書いていると、そもそも実行されないってことがあるのでこれは気を付けておかないといけません。
これらのことに対応するためには、まずライブラリはそもそも関数呼び出し時にはロードされるのでこれを利用します。ライブラリをロードするための関数をネイティブプラグイン側で用意しておいて、これをネイティブプラグインの実際の処理が走る前に実行しておいて依存関係が解決されるようにマネージド層からネイティブのプラグインのライブラリの依存関係が解消されるようにしておく必要あります。

Unityネイティブプラグインマニアクス

そしてUnity Editorの場合です。つまりmacOSとWindows上での動作となります。
これもいくつかポイントがありまして、三つぐらい挙げるんですけど、詳細に説明していきます。
まず一つ目は、ネイティブプラグインがロードされるのは初回再生の初回の呼び出し時のみです。そのため、ネイティブプラグインを更新した場合はUnityの再起動をしなければなりません。
二つ目、Unity Editorの再生・停止モードには影響されません。再生・停止モードっていってるのはこの下のスクリーンショットで書いてある所、再生ボタンを押した状態とか、実行されてる状態なんですけれど、この場合、再生・停止してもネイティブプラグインはアンロードされません。
そして最後に三つ目、初期化処理が複数回呼ばれうるような実装にしておく必要があります。これはゲームコードがリコンパイルされた後のアセンブリ置き換えで終了処理が呼ばれない場合があります。アプリの場合であればアプリの開始と終了がそのままネイティブプラグインの開始・終了にもなると思うんですけれど、Unity Editorの場合は再生・停止してもネイティブプラグインがアンロードされないというところでアプリとは実際には挙動が異なります。そのため終了処理が呼ばれないままでそのまま初期化処理が呼ばれてしまうということもあるので、何度初期化されてもいいような実装にしておく必要があります。場合によっては初期化するときに終了処理がちゃんと呼ばれてなかったら自分で終了してあげて初期化するというような処理を掛けてあげる必要があります。つまり状態管理はネイティブプラグイン側で行っていなければなりません。

Unityネイティブプラグインマニアクス

そしてネイティブコードからマネージドコードを呼び出す実装の注意点についてです。
ネイティブプラグインからマネージドコードへ結果を呼び出す場合、以下の点に注意しなければなりません。
例えば、ネイティブで作成したスレッドからマネージドコードを呼び出した場合、Unityの内部処理もネイティブプラグインからの呼び出しになってしまうことがあるので、問題が起こることがあります。
具体的にどういうことかといいますと、まずiOS、Androidアプリの場合、Unityのメインスレッド前提の処理の一部で例外が発生します。それでクラッシュしてしまうことがあります。UnityはそもそもUnityのメインスレッドでユーザーコードがほぼ実行される前提になっているところもあるんですけれど、この上のほうに書いたようにネイティブで作成したスレッドからマネージドコードを呼び出してしまうとその前提が崩れてしまうので、意図しない状態にUnity自体がなってしまいます。

Unityネイティブプラグインマニアクス

Unity Editorの場合はこのようなコードを含む状態でデバッガ接続を行ってしまうと Unity Editorが応答不能になってしまいます。これはUnity内部のMonoの実装の問題でこのような挙動になってしまいます。今の問題を回避するためにはネイティブで作成したスレッドからマネージドのコールバック処理、例えばDelegate等は直接呼び出さないようにします。あくまでマネージド層からの処理として完結させるようにUnityのメインスレッドから呼び出すようにしていきます。
対応例としては下に書いたとおりなんですが、マネージド層でポーリングしておいてマネージド層からネイティブ層へ結果を問い合わせる形に、つまりPull型にするという方法があります。

Unityネイティブプラグインマニアクス

コードだと分かりにくいので図に示してみました。左側がクラッシュが起こるような問題のあり実装で、右側が先ほどお伝えしたPull型の実装になります。
まず左側のほうから説明していきます。水色で書いてある所、緑色の所と濃い緑と三つあるんですけれど、まず上からUnityのメインスレッドとネイティブ実装とネイティブで作成したスレッドを表しています。
最初にUnityのメインスレッドからネイティブプラグインを呼び出すようなコードを呼び出します。そこで、Unityのメインスレッドから呼び出されたネイティブ実装で、さらにネイティブ実装から作成したスレッドで何か処理を行いたい場合、例えばデータのパースであるとか別のスレッドでやりたいことってあると思うんですけれど、これを別のスレッドで処理を行います。
ここで行った処理を直接Unityに返した場合、これが先ほどお話ししたようなクラッシュ等の問題が起こるケースになります。これを避けるためには、右側のOKと書いてあるほうの図なんですけれど、呼び出し処理までは一緒で、ネイティブで作成したスレッドから結果はネイティブ実装の所までしか返さないようにします。ポーリングしておき、処理の結果の結果を、Unityのメインスレッド、Unityから都度結果を問い合わせて取得するようにすることで、先ほどお話ししたような問題は解消できるようになります。
それでは安定性と実機上での実行効率については大竹のほうからまた説明していきます。

安定性

Unityネイティブプラグインマニアクス

大竹:安定性については、フェイルセーフ性の担保とプラットフォーム毎のアラインメント問題の考慮という形でお話しさせていただきます。 最後にここ結構大きなトピックになるんですけど、実機上での実行効率を上げるというところでメモリコピーの回避手段について詳しく説明させていただこうと思っています。

Unityネイティブプラグインマニアクス

まずフェイルセーフ性の担保です。
これは結構当たり前のこともかなり含むんですけど、当然のことながら、ゲームを実際にプレイするプレイヤーのユーザー体験をクラッシュしたら大きく損ねてしまうので、そこの安定性が求められます。
ネイティブプラグインの場合、特徴的なのがEditor上でも動作をさせないと開発効率下がってしまうというのはあったんですけど、動作したとしてもそれで頻繁にUnity がクラッシュしてしまうと、かなり開発者の開発効率というのを下げることにつながってしまいます。なので開発チームの開発効率を下げないように安定した形で提供していく必要が強く求められます。
そのために、より気を付けることとして、全てのネイティブコードのネイティブプラグインの呼び出しというのは、常に状況にによらず安全に実行ができて正しくエラーとしてマネージド側に伝えられるという形で実装していく必要があります。
当然マネージド側からネイティブコードを呼び出したときにエラーをハンドリングできるようにするだとか、ネイティブプラグインの内部で呼び出してないけど内部のスレッドでエラーが発生しましたということも多くあるかと思うんですが、その場合とかもUnity Editorのコンソールとかにその内容を出力できるようにしたりとか中のレポート経路を用意しておいてUnity Editor上からそれを使ってる開発者が何が起こったかというのを把握できるようにする必要があります。
でないと、やはり開発中などは開発チーム側から結構こういうバグありましたという報告があったりするんですが、そういうときに何が起こってるかというのを把握しやすくなりますし、こうすることでそういうときの対応を素早く行うことができるようになっていきます。

Unityネイティブプラグインマニアクス

次にアラインメント問題についてです。
これはコンテキストが高い話なんですけど、P/Invoke によってネイティブプラグインをつなげていく際にstructも渡すものとして指定することというのができます。
なんですけど、そういうコードを使用する場合に、状態としてC 上で書いてあるstructの定義というのとそれに対応する形でC#側でそれに対応するメモリレイアウトのstructというのを別に定義してあげる必要があります。
ですので、その二つの間でメモリ上のどのフィールドがどの何ビット目に配置されるかというところに齟齬が生まれてしまうと、きちんと呼び出しができなかったり、最悪なケースでは動くけど実は違うパラメータで動いていたみたいなことが全然あり得ることになってしまいます。ですので、これのために特定のプラットフォームで発生するフィールドのアラインメントというのを意識していく必要が出てきます。
サンプルとして二つ構造体を定義してるんですが、Sample1は32bit、16bit、64bitという順番でフィールドを定義しています。Sample2は32bit、32bit、64bitというふうに定義しています。これだけ見るとそれぞれのサイズって表面上は違うサイズなんじゃないかというふうに見えるんですけど、ARMなどにおいてはこのサイズというのは同一サイズで、両方16byteとして見なされます。
これはなぜかというと、まず sample1のほうがこの横の赤い四角があるほうの所で、Feild1、Feild2で32bit、16bitあって、48bit使っているんですけど、ARMにおいては8byteの64bitのフィールドというのは64bitの倍数の位置にしか格納できません。というか参照値がそういう制約がかかっているので処理系がそのように配置してしまいます。ですので、この場合だと16bit分のパディングが生まれて、その分パディングを埋めた後にFeiled3が始まるようになります。
Sample2のほうはちょうど64bitで埋まってるので、このパディングが発生せずにそのまま配置されます。Feild3の配置場所が同じになった結果、同じサイズになってしまうということです。こういう挙動とかを意識して書いてく必要がある場合も多くあります。

実機上での実行効率

Unityネイティブプラグインマニアクス

次に実機上での実行効率についてです。
まずゲームなのでパフォーマンスというところにも強く気を使っていく必要があります。そのために、メモリコピーというのは重い処理になっていくので、過度に発生しないようにしなければいけません。
そのためには、ネイティブ層とマネージド層というところでヒープというのがどういうものがあるのか、どういうものなのかというのを意識していく必要があります。

Unityネイティブプラグインマニアクス

ここではマネージドヒープとネイティブヒープという言葉で説明していきます。
まずマネージドヒープと呼ばれるものは、いわゆるゲーム側のC#のコード内から取り扱われるヒープです。これは、CLI(mono)ランタイムだったりIL2CPPのランタイムだったりに管理されるGC対象となるフィールドになります。
GCの対象になるので、この領域のヒープに置いてあるメモリというのは誰も参照しなくなったら任意のタイミングで削除される可能性があったりとか、または削除されなくても可能性としてはデフラグ的なことが、フラグメンテーションが起こってそのアドレスが移動してしまう可能性があり、アドレスの一貫性がつまりあまり担保されません。
またネイティブヒープという形で説明しているのは、いわゆるCやC++から取り扱うmallocとかnewとかしたときに確保される世間一般でいうところのヒープ領域のことになります。Unityのネイティブ層で取り扱われる内部的なリソースとかもネイティブヒープとかに置かれることが多いです。

ネイティブプラグインという文脈で、これはどういうときに考慮しなきゃいけないかというと、主に最初の頃に説明したマーシャリングという処理が行われる際にメモリコピーが発生します。
どのように発生するかというのをこの図に示しています。
まずマネージドコードから渡したいパラメータに関してマネージドヒープに置かれています。当然その参照はヒープへの参照を内部的には持っています。
それをネイティブコードに引き渡すときにパラメータとして渡すときにマーシャラが作動するようなパターンであると、多くの場合データコピーが発生してしまうことが多いです。この挙動というのが、Stringの例で説明したように、マネージドヒープの中身をマーシャラが読んでそのときにあった変換もした上でネイティブヒープ側の領域にいったんメモリをコピーします。
そのコピーした領域へのポインタをネイティブコードにマーシャラへ引き渡すという形でパラメータのやりとりが行われます。
このときにメモリコピーが発生していくことになります。このやりとりするサイズが大きかったりだとか、または1フレーム内で何十回何百回と呼ばなければいけないという場合、頻度が高いような場合というのには、このマーシャリングというのをできる限り回避することを意識していく必要が出てきます。

Unityネイティブプラグインマニアクス

具体的な回避策について、一つとしてBlittable型という概念を使う方法があります。Blittable型というのはものすごくざっくりした説明なんですけど、マネージドとネイティブの世界でメモリレイアウトが同じになるような固定的、静的に決まる型のことになります。
このBlittableな場合にはP/Invokeでマネージドからネイティブ、ネイティブからマネージドで受け渡す際にマーシャリングを回避して直接マネージドヒープの中身を参照するということができるようになります。
ただそのアドレスが正しいと保証されるのは、そのネイティブコードを呼んでいる間の区間のみということにはなります。それの例がこの図ですね。先ほどコピーしていたのを直接マネージドヒープの中身を見てネイティブコードが走るようになっています。こうすることで参照するだけであれば、引数を渡すだけであればメモリコピーを回避してポインタを引き渡すだけでできるようになっていきます。

Unityネイティブプラグインマニアクス

次にBlittable型の具体的な例がどういうものかというのについて説明していきます。
定義としてこれはMSDNに載っていることなんですが、この三つになります。
まずはboolやcharを除くプリミティブな値型です。これはintとかshortとかfloatとかuintとかも含むんですけど、そういういわゆる値を表すSystem名前空間にある一般的な型たちです。それのうちboolとcharだけは性質上、除外対象になっています。
そして、Blittable型として扱われる表現の1次元の配列、1次元の可変長配列というのはBlittableとして扱えます。
最後に、Blittableな型だけを含むStructLayoutというアトリビュートでメモリ上のレイアウトがこのように配置されますというのを宣言する値型というのは、Blittable型として見なされます。ただしこれはすごく複雑な条件として、その中にBlittable型の可変長配列というのを含んでいる場合は除外対象になってしまいます。

Unityネイティブプラグインマニアクス

具体的な例で、int型の変数とかは普通にBlittable型として扱われます。boolやcharを除くプリミティブな値型です。その型の1次元配列、可変長配列というのもBlittable型として見なせます。StructLayout指定をされた値型ですね。Blittable型だけを含む指定された値型という定義になっています。これ一番特殊な例を指し示していて、普通に配列をフィールド内で宣言してしまうと可変長配列になってしまうんですけど、unsafeコンテキスト下だとfixedというキーワード使うことで固定長の配列を定義することができます。こうやって使うことで、この場合ではBlittable型の型として扱われます。ただこれはかなり利用上も制約が多いので、あんまりこの配列とかは扱わないようにするというのが正しいアプローチにはなっていきます。
そしてLayoutkind.Sequentialというのは出てきたフィールドを上から順番にアサインしていく、普通にCとかで定義したときのアサインと似たような、ほぼ同じような形でメモリレイアウトを決めていくというルールになっています。

Unityネイティブプラグインマニアクス

そして、非Blittable型の例です。
まずString等の参照型の値はプリミティブなString 型などでも対象外です。boolも実は対象外になっています。あとはStructLayoutを指定していない型だと、例えばintだけを含むような型であっても対象外になってしまいます。またはStructLayoutを含んでいても可変長の配列を含んでる場合は何か分からないので対象外という形になってしまいます。

Unityネイティブプラグインマニアクス

これらを使った方法として先ほどは読み込みのときを説明したんですが、今度は書き込むこと、マネージドヒープ側に書き込みたいときの話です。
このときは基本的なポインターが渡ってきてるんで書こうと思えば書けはするんですが、基本的にOut方向属性という属性を宣言として追加するという形になります。これを行うことでマネージドヒープの中身を直接読むだけでなくてネイティブコードのほうからマネージド側で用意したマネージドヒープに対して直接ネイティブコードから書き込みを行うということができるようになります。これを行うことでFixの受け渡しだけじゃなくて結果を返すときにもメモリコピーを介して受け渡すというのが可能になっていきます。

Unityネイティブプラグインマニアクス

具体的なその例になります。P/Invokeでマーシャリング方向属性を付けた定義です。
まずは下のほうのネイティブコードとしてsample_encodeというメソッドで渡されたポインターを、渡された長さだけ、渡されたポインタに対して何らかのencodeを行って書き戻すというロジックがあったとします。
それに対する効率の良い渡し方としては上のマネージドコード側の定義になっていきます。
このifディレクティブで分けてるのは先ほど説明があった部分なので割愛するんですが、このsample_encodeの宣言に今までなかったInというアトリビュートがsource側の変数引数に付いていてOutというアトリビュートがdestというほうに付いています。
Inは入力として引き渡すときに指定する値ですね。destのほう、書き込まれるほうのバッファに対してOutという指定を付けています。これを使うことでdestというものに渡した領域はネイティブ側から書き込まれますよということがマーシャル側に認識できるので、確実に書き戻すことができるようになっていきます。

Unityネイティブプラグインマニアクス

次に、ちょっと先に行った話です。
ここまでは同期的な呼び出しのほうについて話してきました。
Blittableを使った参照ではアドレスの一貫性が保証されるのは関数呼び出しをしてそれが終わるまで、マーシャラが認識できる範囲のみを対象にしています。ですのでこの場合、同期呼び出しにしか使用できないということになります。1回渡したやつが次に呼んだときというか、そのポインタを利用したときにその一貫性がアドレスが本当に使えるかというのはそのままでは担保されません。
ですのでネイティブ層で非同期に処理を行いたい場合にはGCHandleってものを使用する必要があります。非同期に処理を行いたい場合というのは、例えばxmlとかを渡しておいてxmlパーサに読んでもらいたいだとか、データの複合化とか暗号化をしたい等の場合が含まれます。

GCHandleについて説明していきます。
これはC#のAPIというか、NET Frameworkの中にある機構の一つです。これを使うことで指定したインスタンスのガベージコレクタ上のハンドルを取ることができます。そしてその挙動を一部制御することができます。
マネージドヒープに置かれたインスタンスは先ほどから説明しているようにGCによってアドレスが移動する可能性があるんですが、このAPIを使うことでそのアドレスというのを指定した区間だけ固定することができるようになります。これを利用して固定しているそのインスタンスのポインタをネイティブ層に渡していくことで、その間はそのアドレスの一貫性が保証されるのでネイティブ層からその領域に対して非同期で別のタイミングで読み込みや書き込みが行えるようになります。実際に具体的な例について書いて説明してみようと思います。GCHandleを使ってマネージドヒープに書き込みを行う簡単な例になります。

Unityネイティブプラグインマニアクス

これ自体は意味はないものではあるんですが、例えばlibnative.soとしてset_bufferというメソッドとwriteというメソッドがあったとします。
まずset_bufferの中でマネージドコード側からbufferのポインタとそのサイズをもらっておいて中に持っておきます。writeというのが呼ばれたときに実際にそのポインタに対して書き込みを行うという処理を想定してみます。
これに対して別のタイミングでこれを安全に行うためには、ちょっとイメージコードっぽくはなってるんですけど、以下のようなマネージドコードの必要があります。

Unityネイティブプラグインマニアクス

まずはexternでP/Invokeで宣言を、上の4行、それぞれ宣言しておきます。
set_bufferが呼び出されたときに、これでその上にあるbytesというのをいったんマネージドのフィールドとして持っておいて、それを渡すというふうにしてるんですけど、渡す前にGCHandle.Allocというのを呼んでいます。
これにその固定したいインスタンスと、あとはGCHandleType.Pinnedというenumを指定してあげることでアドレスを固定するモードでGCHandleを取ることができます。
なお、このGCHandleを取れるのは先ほど説明したBlittable型であることというのにpinnedを使って取れるのはBlittable型であることという制約があります。
その上で、set_bufferを呼び出してポインタと長さを実際に渡しています。Handleを持ったまま例えば別のタイミングでOnLoadFinishedってのが呼ばれたとしてそのタイミングでネイティブ呼び出しを行ってやると別のタイミングで呼ばれても安全にこれで呼び出してbytesの中にデータを書いてもらうことができます。
最後にGCHandleを確保してあるので、それが必要になったときにはDispose処理としてGCHandleのFreeメソッドを使ってその参照を解放してあげる必要があります。でないとメモリリークにつながったりアドレスが移動しなくなるのでフラグメンテーションにつながることがあります。
今のところの話の出典は以下になります。主にMSDNの資料からもろもろいただいております。では最後に山内のほうからまとめをしていただきます。

Unityネイティブプラグインマニアクス

まとめ

Unityネイティブプラグインマニアクス

山内:それでは本発表のまとめに入ります。
ネイティブプラグインを作成することでゲーム用の機能や開発効率向上のための機能が拡張できるようになります。
ネイティブプラグインの実装には各プラットフォームへの対応やマネージド層とアンマネージド層を意識した実装が必要になります。
本発表が皆さまの今後ネイティブプラグインを開発するときに何か役立てば幸いです。
ご清聴いただきありがとうございました。

大竹:ありがとうございました。


TechCon トップへ戻る

RECRUIT - 募集職種一覧

「DeNAのイメージを破壊」してくれる、ゲームクリエイターを常に募集しています。

EVENT - イベント

Game Developer's Meeting
PAGE TOP

DeNA for GAME CREATORS