凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Windows における Unity のネイティブプラグイン作成についてまとめてみた

はじめに

本記事は Unity Advent Calendar 2017 21 日目の記事になります。

ここ 1 年はいくつか Windows 向けのネイティブプラグインの作成とメンテを行いました。本エントリでは、Windows に特化して、そういったネイティブプラグインの作り方、およびそこから得られた知見などを紹介したいと思います。なお、UWP のプラグインについては本エントリでは解説しません。

環境

本記事のプロジェクト

サンプルとして出て来るプロジェクトは以下にアップロードしてあります。

github.com

最近つくったもの

大体 C++ も含めてコードを MIT で公開しているので参考にしてみて下さい。

uDesktopDuplication

Desktop のキャプチャを Desktop Duplication API という API を利用して高速にコピー、Unity で利用できるようにしたものです。マルチモニタにも対応して、色々便利なスクリプトやシェーダも含まれています。

tips.hecomi.com

uWintab

Wacomタブレットを使えるよう Wintab API を Unity 向けに使えるようにしたものです。

tips.hecomi.com

uTouchInjection

Windows のマルチタッチ API を Unity からエミュレート出来るようにしたものです(解説書き忘れてたので今度書きます)。

github.com

uWindowCapture

まだ未公開です...、デスクトップ上のウィンドウを子ウィンドウも含めてテクスチャ、位置や状態を Unity から(なるべく)高速に参照できるようにしたものです。

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

docs.unity3d.com

Unity には 2 種類のプラグインが存在します。

一つは Managed プラグインで、これは別途 C# 等で書かれたプロジェクトをビルドしてできた DLL で .NETアセンブリです。複数のスクリプトのファイルを一つにまとめたり、ソースコードを隠蔽したりといった用途で使用されます。出来ることは .NET の機能までなので特別なことができるわけではありません。

もう一つが Unmanaged プラグインで、こちらは各プラットフォーム向けにビルドされたネイティブの DLL です。C / C++ 等で記述し、各プラットフォームのネイティブの機能に基本的には何でもアクセスすることが出来ます。

今回は後者の話になります。

どういう時に作る?

Windows ではネイティブプラグインを作らなくても済むケースが沢山あります。例えばデモ展示などで C++ でしか提供されていない SDK があった時の場合、1 フレーム以上の遅延が許されるケースでは、別途 SDK を利用して得られた結果を送信するサーバプログラムを立てた方が便利です。例えば以下のプロジェクトでは外部の認識プログラムから Unity に対して認識他オブジェクトの座標や姿勢といった情報を送っています。こうした構成にすると、パフォーマンスが足りなくなった時に認識プログラムを動かすパソコンをもう一台用意したりといったことに柔軟に対応できるようになります。

tips.hecomi.com

一方でネイティブプラグインを作ると、DLL として Unity のプロジェクトにそのまま組み込めることで構成がシンプルになり、エディタ拡張と併せてプラグインの利用者が簡単に自プロジェクトに採用することが出来たり、遅延を最小に出来たりする点などが上げられます。各プラットフォーム向けの DLL を作っておけば、スクリプト側からは特に意識することなくマルチプラットフォーム対応もできます。また、C# では重い計算を C / C++ 側に任せると言った用途でも用いられます。

作ろうとしているものの目的やユースケースを考えながら、どういった構成で作成するのか考え、ネイティブプラグインが最適だとなった場合に採用する、という手順が良いと思います。

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

ではWindows でのプラグインの作り方を見てみましょう。

docs.unity3d.com

docs.unity3d.com

Unity のプロジェクトの用意

まずは Unity のプロジェクトを作成し、Plugins ディレクトリを作成し、その下に x86 および x86_64 というディレクトリを作成します。プロジェクトを x86 ビルドした時は前者の下にある DLL が、x86_64 ビルドした時は後者の DLL が使われます。なお、Plugins ディレクトリはルートディレクトリでなくても構いません。例えば Hogehoge というプロジェクトだったら Assets/Hogehoge/Plugins 以下に格納すれば問題ありません。

Visual Studio でプロジェクトの作成

空の C++ プロジェクトを作成します。作成場所はどこでも良いのですが、私はいつも Assets と同階層(Unity のプロジェクトのルート)に Plugins ディレクトリを作成し、そこに DLL 用プロジェクト用のディレクトリが来るようにしています。

f:id:hecomi:20171220222756p:plain

作成し終わったらプロジェクトを右クリックしてプロパティページを開きます。「構成」を「すべての構成」に、「プラットフォーム」を「すべてのプラットフォーム」にした後、「構成の種類」を「ダイナミック ライブラリ (.dll)」にします。

f:id:hecomi:20171220223152p:plain

ビルド後に DLL を Unity 側のディレクトリにコピーするよう設定します(直接ビルド先として指定しても構いませんが、ここではコピーすることにします)。「ビルドイベント」の「ビルド後のイベント」の「コマンドライン」で、「プラットフォーム」を「Win32」に変更して次の値を入力します。

xcopy /Yq $(TargetPath) $(SolutionDir)..\..\Assets\Plugins\x86\

次に「プラットフォーム」を「x64」にして次の値を入力します。

xcopy /Yq $(TargetPath) $(SolutionDir)..\..\Assets\Plugins\x86_64\

[f:id:hecomi:20171220224408p:plain]

これでビルド後に DLL が該当のディレクトリ下にコピーされるようになります。

ソースの追加とビルド

まずは適当なソースを追加してみましょう。例えば Main.cpp をプロジェクトに追加し、次のようなコードを入力してみます。

#define UNITY_INTERFACE_API __stdcall
#define UNITY_INTERFACE_EXPORT __declspec(dllexport)

extern "C"
{

UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetNumber()
{
    return 123;
}

}

スクリプト側から利用するには呼び出し規約の __stdcall と DLL に関数をエクスポートするための __declspec(dllexport) を追加する必要があります。

これでビルドをすると、自動的に DLL が Plugins 下に送られます。Win32 と x64 でビルドしてみて下さい。なお、「Ctrl + Shift + R」で「バッチビルド」というのが出来るのですが、ここで Win32 と x64 両方にチェックを入れて「ビルド」を押下すると、同時に 2 つの DLL をビルドすることが出来ます。

f:id:hecomi:20171220224758p:plain

ビルドすると以下のように Unity に DLL が配置されます。ビルドされた DLL をクリックすると Inspector にどのプラットフォーム向けのビルドなのかが出て来るのでチェックしておきましょう。DLL が見つからない、と怒られる時は、ここのチェックが外れている時もあります。

f:id:hecomi:20171220225614p:plain

C# からの利用

ではビルドした DLL をスクリプトから利用してみましょう。DllImport で DLL の名前を指定するとその中に含まれる関数が使えるようになります。namespace でくくった static なクラス等に DllImport した関数をまとめておくと 1 箇所で管理できて見通しが良くなると思います。

Lib.cs
using System.Runtime.InteropServices;

namespace NativePluginExample
{

public static class Lib
{
    [DllImport("NativePluginExample")]
    public static extern int GetNumber();
}

}
CallDllFunction.cs
using UnityEngine;

namespace NativePluginExample
{

public class CallDllFunction : MonoBehaviour
{
    void Start()
    {
        Debug.Log(Lib.GetNumber());
    }
}

}
実行結果

f:id:hecomi:20171220234248p:plain

DLL の更新

さて、続けて作業していきます。次のように適当にコードを書き足して再度ビルドしてみましょう。

...
extern "C"
{

...
UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API DoSomething(int x, int y)
{
    const int x2 = x * 2;
    const int y3 = y * 3;
    return x2 * y3;
}

}

すると以下のように失敗します。

f:id:hecomi:20171221001522p:plain

これは xcopy コマンドで Unity の Plugins ディレクトリ下へ DLL をコピーするのに失敗している、というエラーです。Unity で最初に DLL の関数を呼んだ時に DLL が Unity によって握られ、解放ないので置き換えができなくなります(DLL を使った関数を呼んでいなければ Unity でプロジェクトを開いたあとでも置き換え可能です)。

なので DLL を更新したい時は、大変に面倒なのですが、一度 Unity を終了して再度起動してプロジェクトを開かないとなりません。。ツラミ。

ホットリロード

こういった面倒な再起動処理ですが、id:i-saint さんが公開されている PatchLibrary を利用すると起動したままでも DLL の更新ができるようになります。

tips.hecomi.com

ただし、記事にも書きましたが状態を内部で持つ場合には注意が必要なのでご注意下さい。

デバッグの方法

デバッグもプロセスをアタッチすることで、通常の開発と同じようにブレークポイントを貼って変数を調べたりクラッシュを検出したりといったことが可能です。

メニューの「デバッグ」から「プロセスにアタッチ」を選択し、リストから Unity を探します。

f:id:hecomi:20171221102146p:plain

「アタッチ」を押下すると以下のように画面が変わります。

f:id:hecomi:20171221102324p:plain

DLL が読み込まれていない(Unity で該当の DLL の関数を呼んでいない)状態だと、ブレークポイントを貼っても図のように、シンボルが読み込まれていないことを示す表示(白抜き○にワーニングアイコン)になります。この状態で Unity で該当の関数を呼んでみます。

Lib.cs
using System.Runtime.InteropServices;

namespace NativePluginExample
{

public static class Lib
{
    [DllImport("NativePluginExample")]
    public static extern int GetNumber();

    [DllImport("NativePluginExample")]
    public static extern int DoSomething(int x, int y);
}

}
CallDllFunction.cs
using UnityEngine;

namespace NativePluginExample
{

public class CallDllFunction : MonoBehaviour
{
    void Start()
    {
        Debug.Log(Lib.DoSomething(3, 5));
    }
}

}

f:id:hecomi:20171221103605p:plain

ブレークポイントの場所で止まり、変数の確認や書き換えが行えます。実行ボタンを押せば再度 Unity が動き出し、書き換えた内容も反映されています。

f:id:hecomi:20171221103839p:plain

Win32 API の利用

Win32 API のサンプル

Windows 向けの記事、ということで折角なので Win32 API を使った簡単なサンプルを書いてみましょう。試しに Unity からマウスの位置を動かしてみます。

次のコードを Main.cpp に追加します。

...
UNITY_INTERFACE_EXPORT BOOL UNITY_INTERFACE_API NpeSetCursorPos(int x, int y)
{
    return ::SetCursorPos(x, y);
}
...

同名を回避するために適当な接頭辞(Npe)を付けました。次にこれを C# 側で利用します。Lib.cs に追記してください:

...
    [DllImport("NativePluginExample", EntryPoint = "NpeSetCursorPos")]
    public static extern void SetCursorPos(int x, int y);
...

EntryPoint アトリビュートで関数名を指定し、好きな関数名をセットできます。名前のコンフリクトを避けるためにこういった運用もオススメです。これを CallDllFunction.cs から利用してみます。

...
public class CallDllFunction : MonoBehaviour
{
    void Start()
    {
        ...
        Lib.SetCursorPos(100, 100);
    }
}
...

実行してみると、カーソルが (100, 100) の位置に移動します。

C# から使う方法

ただ、この例のように単に API を利用するだけであれば、ネイティブプラグインを作らずに C# のみでも実現できます。例えば上で利用した SetCursorPosuser32.dll に格納されているので、これを DllImport() してあげます。Win32API.cs というファイルを作ってそこに次のように記述してみて下さい:

using System.Runtime.InteropServices;

namespace NativePluginExample
{

static public class Win32API
{
    [DllImport("user32.dll")]
    public static extern int SetCursorPos(int x, int y);
}

}

これを CallDllFunction.cs で次のように利用します。

...
public class CallDllFunction : MonoBehaviour
{
    void Start()
    {
        ...
        // Lib.SetCursorPos(100, 100);
        Win32API.SetCursorPos(100, 100);
    }
}
...

同じ結果が得られると思います。

どっちを使う?

どちらも一長一短あります。C# の場合は、ネイティブプラグインを作らなくても色々と関数を利用できるのが最大の利点です。ウィンドウハンドルを取得して受け渡すといったことや、ウィンドウメッセージを取得してフックするみたいなことも C# だけで可能です:

tarukosu.hatenablog.com

ただ、特定の構造体で結果を受け取ったり、Enum がたくさん定義されている、といったケースでは DLL 側とのやりとりをするために、C# 側で C/C++ 側で宣言されている構造体の構造や Enum の番号を再宣言しないとなりません。1 つ、2 つなら良いのですが大量にあると結構大変です。

その点ネイティブプラグインではこういった再宣言の必要はなく、ヘッダに記述されている構造をそのまま利用すれば問題ありません。時と場合によっては C/C++ で書いたほうがコードが短くなることもあると思います。これも最初の「どういう時に作る?」というところと重複しますが、ユースケース次第でどういう設計にするかを帰るのが良いでしょう。

(おまけ): uWintab の設計

Win32 APIプラグイン作成は色々ツラミもあるところですが、変なことも出来て面白いです。例えば uWintab ではウィンドウメッセージをフックするのでなく、不可視最前面のウィンドウを作成し、Wintab API からのメッセージはそちらに飛ばして受け取る、みたいなことをしています。

uWintab/Main.cpp at master · hecomi/uWintab · GitHub

この程度であれば C# だけでも出来ると思いますが、Wintab API の中で色々な構造体が用意されていたり、いちいち必要な Win32 APIEnumC# に持ってくるのが大変だったので、C++ で作る方針にしました。

レンダリング用のプラグイン

レンダリングプラグインについて

Unity の描画に絡むもの、例えば外部でテクスチャを生成し、それを Unity へ持ってきたい、といった場合にはあるお作法に則って作成しないとなりません。

画像サイズがそんなに大きくなく、CPU だけでやりとりしても問題ない場合は以下のように Texture2D を介してデータのやり取りをする方法が使えます。

tips.hecomi.com

テクスチャが大きく速度が問題になる場合や、直接 GPU 側へ DLL からテクスチャをアップロードしたい場合には Low-Level Native Plugin Interface というものを利用します。

tips.hecomi.com

tips.hecomi.com

枠組みは変わらないのですが、解説当時とは現在は少しお作法が変わっているので最新情報をチェックして下さい。

docs.unity3d.com

高速化

基本的にはメイン・レンダリング両スレッドを邪魔しないよう、別スレッドを立てて処理を行うようにします。GPU にアップロードするのも結構時間がかかるのですが、ここは以下で頂いたプルリクエストのように ID3D11Device を自前で作成してアップロードすることでかなりパフォーマンスが改善しました。

github.com

その他

ログ

配布すると予期しないエラーで落ちることが多々あります。私は、基本的には利用するネイティブ側のライブラリの API コールのエラー値はなるべく補足するようにし、エラーが出たときにはテキストファイルに出力するようにしています。

DLL

自分の環境では動くけれど、他の人の環境では動かない、ということが多々あります。よくある原因の一つとして msvcp や msvcr といったランタイムライブラリの DLL がないことによります。この場合は再頒布可能パッケージをインストールしてもらうか、ビルドする DLL の設定を変えることで対処が可能です。

再頒布可能パッケージはこちら:

ビルドの設定で対処する場合は、プロジェクトの「プロパティ」の「C/C++」の「コード生成」にある「ランタイム ライブラリ」の設定を DLL でなくリリースでは「マルチスレッド(/MT)」、デバッグでは「マルチスレッド デバッグ(/MTd)」にすることです。これにより DLL のサイズは大きくなってしまいますが(~数百 kb)、ランタイムライブラリが不要になります。

f:id:hecomi:20171221124655p:plain

ビルド

デバッグビルド配布しがちなので気をつけましょう…(何度かやらかしました)。

おわりに

開発が中々面倒なのもあるのですが、その分、Unity だけでは出来ないことがユーザ側では簡単に色々出来るようになったり、たくさん利用してくれる人がいてくれたりと、なかなか面白い開発ができたと思っています。

明日は ろっさむさんの「Anima2Dでモーション作成したい」になります。Anima2D 使ったこと無いので楽しみです!