Unityのネイティブプラグインをはじめました

昨日に引き続き、今日はネイティブプラグインを扱います。

ここではUnity 2018 1.3f、Visual Studio 2017を使用しています。

おかげさまで絶好調です。

 

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

多分公式マニュアルを見た方が早いでしょうか?
docs.unity3d.com

既存のC,C++で書かれたライブラリを、 C#で扱うための仕組みです。

Visual Studioなど他のコンパイラでdllファイルを作成し、Unityでこのdllの関数を呼び出します。 

 

ネイティブプラグインの作成

dllの作成

まずはVisual Studio 2017でdllを作成するプロジェクトを作成します。

Visual C++ > Windows デスクトップ > ダイナミック リンク ライブラリ(.dll)のプロジェクトを作成します。

f:id:chirotec:20180617201500p:plain

 

初期状態では、以下のようなプロジェクトになっています。

 

f:id:chirotec:20180617201456p:plain

ここでは「NativePluginSample.cpp」のみを編集します。
 さらにdllをincludeするためのヘッダーファイル「NativePluginSample.h」をプロジェクトに追加します。

 

NativePluginSample.h

#pragma once
#ifdef NATIVEPLUGINSAMPLE_EXPORTS
#define SAMPLE_API __declspec(dllexport)
#else
#define SAMPLE_API __declspec(dllimport)
#endif

extern "C" {
	// 値渡し
	SAMPLE_API int SampleAPIInt(int i);
	SAMPLE_API float SampleAPIFloat(float f);
	SAMPLE_API double SampleAPIDouble(double d);

	// 参照渡し
	SAMPLE_API void SampleAPIInt2(int& i);
	SAMPLE_API void SampleAPIFloat2(float& f);
	SAMPLE_API void SampleAPIDouble2(double& d);

	// 配列の参照渡し
	SAMPLE_API void SampleAPIIntArray(int intArray[], int intArraySize);
	SAMPLE_API void SampleAPILongArray(long longArray[], int longArraySize);
	SAMPLE_API void SampleAPIFloatArray(float floatArray[], int floatArraySize);
	SAMPLE_API void SampleAPIDoubleArray(double doubleArray[], int doubleArraySize);

	// 文字列
	SAMPLE_API const char* SampleAPIString1();
	SAMPLE_API const char* SampleAPIString2(const char* str);
}

NativePluginSample.cpp

#include "stdafx.h"
#include "stdlib.h"
#include "NativePluginSample.h"

SAMPLE_API int SampleAPIInt(int i)
{
	return 123 + i;
}

SAMPLE_API float SampleAPIFloat(float f)
{
	return 123.456f + f;
}

SAMPLE_API double SampleAPIDouble(double d)
{
	return 123.456 + d;
}

SAMPLE_API void SampleAPIInt2(int& i)
{
	i = 123 + i;
}

SAMPLE_API void SampleAPIFloat2(float& f)
{
	f = 123.456f + f;
}

SAMPLE_API void SampleAPIDouble2(double& d)
{
	d = 123.456 + d;
}

SAMPLE_API void SampleAPIIntArray(int intArray[], int intArraySize)
{
	for (int i = 0; i < intArraySize; i++)
	{
		intArray[i] = i;
	}
}

SAMPLE_API void SampleAPILongArray(long longArray[], int longArraySize)
{
	for (int i = 0; i < longArraySize; i++)
	{
		longArray[i] = i;
	}
}

SAMPLE_API void SampleAPIFloatArray(float floatArray[], int floatArraySize)
{
	for (int i = 0; i < floatArraySize; i++)
	{
		floatArray[i] = 0.5f + i;
	}
}

SAMPLE_API void SampleAPIDoubleArray(double doubleArray[], int doubleArraySize)
{
	for (int i = 0; i < doubleArraySize; i++)
	{
		doubleArray[i] = 0.5 + i;
	}
}

SAMPLE_API char* MallocString(const char* str)
{
	size_t bufSize = strlen(str) + 1;
	char* buf = (char*)malloc(bufSize);
	strcpy_s(buf, bufSize, str);
	return buf;
}

SAMPLE_API const char* SampleAPIString1()
{
	return MallocString("Hello!");
}

SAMPLE_API const char* SampleAPIString2(const char* str)
{
	// 文字列の後ろに"Hello!"をくっ付けて返す
	const int bufferSize = 256;
	static char buffer[bufferSize];
	const char hello[] = "Hello!";

	strcpy_s(buffer, bufferSize, str);
	size_t n = strnlen(buffer, bufferSize);
	strcpy_s(buffer + n, bufferSize - 7, hello);
	return MallocString(buffer);
}

このプロジェクトを作成したときに、プロジェクトのプロパティ内で、プリプロセッサの定義に NATIVEPLUGINSAMPLE_EXPORTS が定義されています。これを利用して dllexport 属性またはdllimport 属性を使用出来ます。

正しくdllexport属性が使用できていれば、ビルド時にインポートライブラリ(*.libファイル)が*.dllファイルと共に作成されます。

注意するべき点は、const char* 型をC#側でstring型として扱う場合、malloc()でヒープ領域にメモリを確保して返す必要があることです。うっかりスタック領域を返してしまうと、最悪Unity Editorごと落ちます。malloc()で確保したメモリは、stringが解放されるタイミングで開放されます。

ポインタを扱う場合、C#側ではIntPtr型で扱えますが、マーシャライズなどを行う必要があるため今回説明は省略します。

 

dllのテストプログラムの作成

dllが正しく作られたかテストを行うために、先ほどのソリューションに追加する形で、Windows コンソールアプリケーションを作成します。

f:id:chirotec:20180617201452p:plain

 テストプログラムのプロジェクトが追加されたら、テストプログラムのプロジェクト側の「参照」に、先ほどのdllのプロジェクトを追加します。

f:id:chirotec:20180617201449p:plain

 構成はこのようになります。テストプログラムの方をスタートアッププロジェクトに設定しておけば、デバッグ実行を行うことが出来ます。

f:id:chirotec:20180617201443p:plain

 インクルードディレクトリにdllプロジェクトのヘッダファイルが入ったフォルダを追加しておきます。

f:id:chirotec:20180617201603p:plain

以下のようなチェック用プログラムを作成しました。

NativePluginSampleTest.cpp

#include "stdafx.h"
#include "stdlib.h"
#include "NativePluginSample.h"

int main()
{
	printf("int:    %d\n", SampleAPIInt(111));
	printf("float:  %f\n", SampleAPIFloat(111.111f));
	printf("double: %f\n", SampleAPIDouble(111.111));

	int    i = 222;
	float  f = 222.f;
	double d = 222.0;
	SampleAPIInt2(i);
	SampleAPIFloat2(f);
	SampleAPIDouble2(d);
	printf("int:    %d\n", i);
	printf("float:  %f\n", f);
	printf("double: %f\n", d);

	int    is[3] = { 111, 222, 333 };
	long   ls[3] = { 111, 222, 333 };
	float  fs[3] = { 111.f, 222.f, 333.f };
	double ds[3] = { 111.0, 222.0, 333.0 };
	SampleAPIIntArray(is, 3);
	SampleAPILongArray(ls, 3);
	SampleAPIFloatArray(fs, 3);
	SampleAPIDoubleArray(ds, 3);
	printf("int:    %d,%d,%d\n", is[0], is[1], is[2]);
	printf("long:   %d,%d,%d\n", ls[0], ls[1], ls[2]);
	printf("float:  %f,%f,%f\n", fs[0], fs[1], fs[2]);
	printf("double: %f,%f,%f\n", ds[0], ds[1], ds[2]);

	const char st[] = "MyTest ";
	const char* p1 = SampleAPIString1();
	const char* p2 = SampleAPIString2(st);
	printf("string1: %s\n", p1);
	printf("string2: %s\n", p2);
	free((void*)p1);
	free((void*)p2);

	return 0;
}

 このプログラムを実行すると、dllの関数が呼び出されていることが分かります。

f:id:chirotec:20180617201600p:plain

 

Unity側での使用

Unityのプロジェクトを作成します。 

f:id:chirotec:20180617210619p:plain

”Plugins”フォルダを作成し、中にReleaseビルドしたdllファイルを入れます。

(64bit版と32bit版を用意し、Platform settingsで振り分けます。)

f:id:chirotec:20180617211100p:plain

C#の関数にdllの関数を割り当てます。
lib.cs

using System.Runtime.InteropServices;

public class Lib {

    // 値渡し
    [DllImport("NativePluginSample")]
    public static extern int SampleAPIInt(int i);
    [DllImport("NativePluginSample")]
    public static extern float SampleAPIFloat(float f);
    [DllImport("NativePluginSample")]
    public static extern double SampleAPIDouble(double d);

    // 参照渡し
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIInt2(ref int i);
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIFloat2(ref float f);
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIDouble2(ref double d);

    // 配列の参照渡し
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIIntArray(int[] intArray, int intArraySize);
    [DllImport("NativePluginSample")]
    public static extern void SampleAPILongArray(int[] longArray, int longArraySize);
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIFloatArray(float[] floatArray, int floatArraySize);
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIDoubleArray(double[] doubleArray, int doubleArraySize);

    // 文字列
    [DllImport("NativePluginSample")]
    public static extern string SampleAPIString1();
    [DllImport("NativePluginSample")]
    public static extern string SampleAPIString2(string str);
}

テストプログラムを作成し、適当なオブジェクトにアタッチして実行します。

内容は先ほどのC++版のほぼ等価です。

TestObject.cpp

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestObject : MonoBehaviour {

	// Use this for initialization
	void Start () {
        Debug.Log("int:    " + Lib.SampleAPIInt(111));
        Debug.Log("float:  " + Lib.SampleAPIFloat(111.111f));
        Debug.Log("double: " + Lib.SampleAPIDouble(111.111));

        int i = 222;
        float f = 222.0f;
        double d = 222.0;
        Lib.SampleAPIInt2(ref i);
        Lib.SampleAPIFloat2(ref f);
        Lib.SampleAPIDouble2(ref d);
        Debug.Log("int:    " + i);
        Debug.Log("float:  " + f);
        Debug.Log("double: " + d);

        int[]    i_s = new int[] { 111, 222, 333 };
        int[]    l_s = new int[] { 111, 222, 333 };
        float[]  f_s = new float[] { 111.0f, 222.0f, 333.0f };
        double[] d_s = new double[] { 111.0, 222.0, 333.0 };
        Lib.SampleAPIIntArray(i_s, i_s.Length);
        Lib.SampleAPILongArray(l_s, l_s.Length);
        Lib.SampleAPIFloatArray(f_s, f_s.Length);
        Lib.SampleAPIDoubleArray(d_s, d_s.Length);
        Debug.LogFormat("int:    {0},{1},{2}", i_s[0], i_s[1], i_s[2]);
        Debug.LogFormat("long:   {0},{1},{2}", l_s[0], l_s[1], l_s[2]);
        Debug.LogFormat("float:  {0},{1},{2}", f_s[0], f_s[1], f_s[2]);
        Debug.LogFormat("double: {0},{1},{2}", d_s[0], d_s[1], d_s[2]);

        string st = "MyTest ";
        string s1 = Lib.SampleAPIString1();
        string s2 = Lib.SampleAPIString2(st);
        Debug.Log("string1: " + s1);
        Debug.Log("string2: " + s2);
    }
}

 

f:id:chirotec:20180617201553p:plain

 動作が確認できました。