C++ライブラリ(DLL)をUnity(C#)向けに作成して利用するシンプルな方法
なにが学べるの?
自分自身でC++の実装を行うことはあまりないと思いますが、例えば、Unity向けのDLLが提供されていないなどの場合、そのラッパーをC++で記述してDLLとの橋渡しを自分で実装するというケースはあるでしょう。この記事では、そうしたC++を作成しそれをUnity上で扱うためのTipsを紹介します。この記事を通して、どうやってDLLを作成するのか、どうやってそれを利用するのかの知識が得られます。そうした際の参考になれば幸いです。
環境
- Windows 10 Pro (ver. 21H2)
- Visual Studio 2019 (ver. 16.11.13)
- Unity 2020.3.25f1
- Architecture - x64
C++プロジェクトを作成する
まずはVisual StudioでC++プロジェクトを作成しましょう。Visual Studio 2019を開き、以下のメニューから Create a new project を選択します。
選択すると次の画面で、どういうタイプのプロジェクトを作成するのかのテンプレート選択画面が表示されるので Dynamic-Link Library (DLL) を選択します。
Project nameに任意の名前を入力して Create します。(ここでは dll-for-unity としました)
C++側の実装を行う
Visual Studioの Solution Explorer の Header Files を右クリックし、 Add > New Item ...
を選択し、ヘッダーファイルを追加します。(名前は export.h としました)
export.h という名前で生成した
ヘッダーファイルに必要な項目を宣言
まずは作成したヘッダーファイルに必要な項目を以下のように宣言します。
#pragma once
#ifdef DLLFORUNITY_EXPORTS
# define EXPORT __declspec(dllexport)
#else
# define EXPORT __declspec(dllimport)
#endif
class ExportTest
{
public:
int TestFunc(int a);
};
extern "C" EXPORT ExportTest* createExportTest();
extern "C" EXPORT void freeExportTest(ExportTest* instance);
extern "C" EXPORT int getResult(ExportTest* instance, int a);
上から順に見ていきましょう。
Export / Importの宣言を分ける
最初に目に入るのは dllexport
と dllimport
を場合分けしている部分です。
#ifdef DLLFORUNITY_EXPORTS
はプロジェクト名を元に自動で宣言されるPreprocessor definitionです。(今回は dll-for-unity
という名前にしたのでアンダーバーが取り残されたものが採用されている)
この場合分けの意味は、DLL作成プロジェクト(本プロジェクト)では宣言された関数がエクスポートされ、利用側(今回ではUnity側)ではインポートとして扱われるようにするための宣言です。
extern "C" でマングリングを防ぐ
クラスについてはいったんスキップし、後半に登場する extern "C"
の説明を先にします。これはC++プロジェクトで起こる マングリング を防ぐためのものです。
宣言を実装する
ヘッダーファイルで宣言した内容を実際に実装していきます。まずはコード全文を見てみましょう。
#include "pch.h"
#include "export.h"
int ExportTest::TestFunc(int a)
{
return a + 5;
}
EXPORT ExportTest* createExportTest()
{
return new ExportTest();
}
EXPORT void freeExportTest(ExportTest* instance)
{
delete instance;
}
EXPORT int getResult(ExportTest* instance, int a)
{
return instance->TestFunc(a);
}
処理を概観すると、C++クラスのインスタンスを生成する処理とそれを利用する処理を実装します。
理由はC#側からはC++クラスの情報が見えないため明確な型名で保持することができません。そのため、C#側ではC++クラスのインスタンスのポインタのみを保持し、実際の処理(メソッドの実行など)はインスタンスを渡してC++側で行ってもらう必要があるためです。言い換えると、C#側から exportTestInstance.TestFunc(5);
みたいに呼び出すことができないのです。
C++プロジェクトをビルドする
記述できたらこれをDLLとしてビルドします。デフォルトだとプラットフォームのターゲットが x86
になっているので x64
に変更します。
DLLをUnityにコピー
ビルドが成功すると、C++プロジェクトの x64\Debug
フォルダに dll
ファイルが生成されているのでそれをUnity側にコピーします。
Assets/Plugins
以下にコピーする
C#側でDLL側の実装を呼び出す
DLLのインポートが終わったら今度はそれを呼び出すC#側の実装を行います。まずはざっと実装を見てみましょう。
using System;
using System.Runtime.InteropServices;
using UnityEngine;
public class UseDLL : MonoBehaviour
{
[DllImport("dll-for-unity.dll")]
private static extern IntPtr createExportTest();
[DllImport("dll-for-unity.dll")]
private static extern void freeExportTest(IntPtr instance);
[DllImport("dll-for-unity.dll")]
private static extern int getResult(IntPtr instance, int a);
private void Start()
{
IntPtr test = createExportTest();
Debug.Log(test);
int result = getResult(test, 10);
Debug.Log(result);
freeExportTest(test);
}
}
利用はだいぶシンプルですね。重要なポイントは以下です。
-
System.Runtime.InteropServices
名前空間に定義されているDllImportAttribute
を利用する。 - (1)のAttributeを、DLLの関数と紐づけたいメソッドに対して適用する。(※)
- (2)のメソッドには
extern
を付け、定義は記述しない。 - C++側で、C++クラスのインスタンスの引数は
IntPtr
を指定する。
該当メソッドは外部(DLL側)で実装されているために extern
を付けるわけですね。
C++クラスのインスタンスはポインタを経由して利用する
C++側のクラスの詳細はC#側で知ることができないので、C#側ではポインタの保持だけを行い、インターフェースとなる関数を介して処理をC++側に投げる、というのが大まかな流れになります。
具体的には以下の部分です。
IntPtr test = createExportTest();
createExportTest()
メソッドを介してC++クラスのインスタンスを生成し、それを IntPtr
で受け取ります。
int result = getResult(test, 10);
Debug.Log(result);
取得したインスタンスのポインタ( IntPtr
)を引数に渡して処理を実行します。
C++側のクラスを利用するにはこうしたポインタを取り回す必要があるのが手間ですが、それ以外は普通の関数呼び出しとほぼ同義なので、利用はとてもシンプルに行うことができますね。
C++側からC#側の関数(delegate)を呼び出す
C#側からC++を呼び出す方法を見てきました。最後に、C++側からC#側の関数を呼び出す方法について見ていきましょう。
まずはC#との橋渡しとなる型とそれを利用する関数の宣言です。
typedef void(WINAPI *CallFp)(std::int32_t a, std::int32_t b);
extern "C" EXPORT void WINAPI CallFunction(void* ptr, std::int32_t in1, std::int32_t in2);
実装は以下のようになります。
EXPORT void WINAPI CallFunction(void* ptr, std::int32_t in1, std::int32_t in2)
{
CallFp cp = static_cast<CallFp>(ptr);
in1 += 22;
in2 += 33;
(*cp)(in1, in2);
}
まず、typedef
によってC#側から渡してもらうデリゲートの型を宣言します。今回は呼び出しとともにすぐコールバックするようなイメージで実装しています。
デリゲート本体は void*
で受け取り、利用する際にキャストして使っています。
CallFp cp = static_cast<CallFp>(ptr);
今回は引数に渡された値をC++側で加工してコールバックする、というイメージで実装しました。最後に渡されたデリゲートを呼び出すことでC#側のデリゲートを呼び出すことができます。
これを利用するC#側の実装は以下のようになります。
using System.Runtime.InteropServices;
[DllImport("path/to/dll")]
private static extern void CallFunction(IntPtr callback, System.Int32 in1, System.Int32 in2);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate void TypeOfCallback(Int32 a, Int32 b);
private TypeOfCallback _delegateObject;
private GCHandle _gcHandle;
private void Callback(Int32 a, Int32 b)
{
Debug.Log("This is invoked from C++ side.");
}
これまで見てきたように、C++側の関数呼び出すのための extern
メソッドを宣言します。また、橋渡しとなるデリゲートの型をC++側と合わせた形で宣言します。
加えて、適切に呼び出すための変数も合わせて宣言しています。
実際に呼び出すには以下のようにします。
_delegateObject = Callback;
IntPtr pointer = Marshal.GetFunctionPointerForDelegate(_delegateObject);
_gcHandle = GCHandle.Alloc(_delegateObject);
CallFunction(pointer, 10, 15);
_gcHandle.Free();
delegate型の変数に自身のメソッドを登録し、 GCHandle
によってマネージドオブジェクトが収集されないように固定しておきます。そして最後にC++側の関数を呼び出します。
最後に
Unity対応されていないものを使う、というケースはあまりないかもしれません。ただ、新しい機能をいち早く試そうと思うとこうした部分を自分で用意できることで、Unity対応を待たなくてもいいのはメリットでしょう。これがそうしたことへの参考になれば幸いです。
エンジニア絶賛募集中!
MESONではUnityエンジニアを絶賛募集中です! XRのプロジェクトに関わってみたい! 開発したい! という方はぜひご応募ください!
MESONのメンバーページからご応募いただくか、TwitterのDMなどでご連絡ください。
書いた人
比留間 和也(あだな:えど)
カヤック時代にWEBエンジニアとしてリーダーを務め、その後VRに出会いコロプラに転職。 コロプラでは仮想現実チームにてXRコンテンツ開発に携わる。 DAYDREAM向けゲーム「NYORO THE SNAKE & SEVEN ISLANDS」をリリース。その後、ARに惹かれてMESONに入社。 MESONではARエンジニアとして活躍中。
またプライベートでもAR/VRの開発をしており、インディー部門でTGSに出展など公私関わらずAR/VRコンテンツ制作に精を出す。プライベートな時間でも開発しているように、新しいことを学ぶことが趣味で、最近は英語を学んでいる。
MESON Works
MESONの制作実績一覧もあります。ご興味ある方はぜひ見てみてください。
Discussion