1. サイトトップ
  2. ブログ
  3. Unity
  4. 【Unity】Unityでマーシャリングを利用してDLLの関数を呼び出してみよう!

【Unity】Unityでマーシャリングを利用してDLLの関数を呼び出してみよう!

はじめに

こんにちは、情熱開発部プログラム2課の安東です。

入社してから今月で丁度一年、業務では経験を積む機会がたくさんありましたが、その中で初めてマーシャリングというものを扱うことになりました。
しかし、Web上には入門向けの記事が多くなく、学ぶのに少し苦労しました。

そういう経緯もありましたので、今回はUnityでマーシャリングをしてC#とネイティブプラグイン間でやりとりできるよう入門向けに解説していきたいと思います。

前提

今回はUnityにおけるマーシャリングを扱います。
その中でも、C言語リンケージのBlittable型とそのポインタ・構造体のマーシャリングが行えるよう入門向けに解説していきます。

.NET Frameworkも同様にC#ですが動作環境が異なりますので、それらの差異に関しては別のサイトをご参考ください。

マーシャリングとは

そもそも、マーシャリングという言葉を聞いたことがありますでしょうか。

マーシャリングとは、マネージド コードネイティブ コードの間でやり取りする必要がある場合に型を変換するプロセスです。

https://learn.microsoft.com/ja-jp/dotnet/standard/native-interop/type-marshalling

とのことです。
馴染みのない方にはあまりピンと来ませんね。

Unityにおけるマネージドコードネイティブコード(アンマネージドコード)に関してざっくりと説明しますと、おおまかには以下の認識で問題ないと思います。

  • マネージドコード:C#で書かれたもの。
  • ネイティブコード:C#以外で書かれたもの。

つまるところマーシャリングとはC#以外で書かれたコードを使用するために行う処理といった感じです。

マーシャリングの利用機会

そんなマーシャリングの利用機会ですが、Unityを使用していて必要になる場面はそうそうないと思います。
私もこれまで6年ほどプログラマとして仕事をしてきてマーシャリングのマの字も知りませんでしたが、直近のプロジェクトでプラットフォーム独自の機能を利用する際にマーシャリングが必要になり、そこで初めて知りました。

一般的にはマーシャリングが必要になる状況は以下のようです。

  • Unityが公開していない何らかの機能を使いたいとき
  • サードパーティ製のモジュールやライブラリを使いたいがUnity向けに提供されていないとき
  • 処理速度を求めて機能をネイティブ実装することになったとき
  • Unityのガベージコレクションの対象から逃れたいとき

いずれにしても特殊な状況下で使うことになるようです。

社内ではアプリケーション側からスマホの温度を取得する際にマーシャリングを使用した方が居たとのことで、ゲーム制作関連ではやはりプラットフォームの機能を利用する際にマーシャリングすることが多いように思います。

ネイティブプラグインの導入

さて、ここからはマーシャリングを行うまでの流れを説明します。

ネイティブコードをマーシャリングするときはC言語等で書かれたコードをそのまま使えるわけではなく、ネイティブプラグインと呼ばれる(.DLL / .so / .dylib)等のライブラリファイルとして出力する必要があります。※JavaScriptやObjective-C等、一部は例外としてそのままプロジェクトに含められるものもあるようです。

今回はC言語リンケージ(プロトタイプ宣言がC言語で書かれたもの)で導入する想定ですので、以下のステップが必要になります。

  1. ネイティブプラグインを出力するためのプロジェクトを作成する
  2. C言語リンケージでネイティブプラグイン用に処理を実装する
  3. ネイティブプラグイン(DLL)を出力する
  4. Unityのプロジェクトに配置する
  5. マーシャリングをしてDLLの処理を呼び出す

既存のDLLをマーシャリングする場合は1~3は不要となります。

1. ネイティブプラグインを出力するためのプロジェクトを作成する

Visual Studioでネイティブプラグインを出力するためのプロジェクトを作成します。
ダイナミックリンクライブラリ(DLL)のプロジェクトとして新規作成するだけで問題ありません。

2. C言語リンケージでネイティブプラグイン用に処理を実装する

任意のCPPファイルを作成したら、まずは実装したい処理を書きましょう。
簡単に以下の関数を実装したい場合とします。

int	IntTest( int x )
{
	x *= 2;
	return x;
}

これをC言語リンケージ用の記述にするためには『extern “C”』を追加します。
また、ネイティブプラグインにコンパイルした際に外部から呼び出しが出来るように『__declspec(dllexport)』を追加します。
※Windows以外の環境で呼び出す場合は『__declspec(dllexport)』があると逆にエラーとなってしまうため場合分けして回避する必要があります。

対応すると以下の通りになります。

#ifdef _WINDOWS
	#define EXPORT __declspec(dllexport)
#else
	#define EXPORT
#endif

extern "C" {
	EXPORT int IntTest( int x )
	{
		x *= 2;
		return x;
	}
}

このように記述することでC#側から呼び出す準備は完了です。

ヘッダーとCPPファイルで分ける場合は以下のように、CPP側にはexternやdllexportの記述は必要なくなるのですっきりします。
後述の処理はヘッダーでexternなどがされている前提で書きます。

#ifdef _WINDOWS
	#define EXPORT __declspec(dllexport)
#else
	#define EXPORT
#endif

extern "C"{
	EXPORT int IntTest1( int x );
	EXPORT int IntTest2( int x );
	EXPORT int IntTest3( int x );
	...
}
#include "まとめて記述する例.h"
int IntTest1( int x )
{
	// 処理
}
	
int IntTest2( int x )
{
	// 処理
}

int IntTest3( int x )
...

C言語リンケージはプロトタイプ宣言がC言語で書かれていれば問題ありませんので、関数内部の処理がC++の要素で構成されていてもオッケーです。
引き数や戻り値にC++の機能(class、std::string等)が含まれている場合はマーシャリングができない訳ではありませんが、対応が難しくなるほか、プラットフォームによっては『C言語リンケージのみ』のような指定がある場合は使用できませんのでご注意ください。

また、注意点として以下の問題が起きないように気を付けてください。

  • デフォルト引き数は使用できない
  • 関数のオーバーロード等、他のネイティブプラグインや同DLL内に同名の関数がある場合は上手く行かなくなる
  • Windows以外の環境の場合は『__declspec(dllexport)』は不要

    3. ネイティブプラグインを出力する

    先ほど記述した実装をネイティブプラグインにコンパイルします。
    設定に問題がなければプロジェクトをビルドをすることでDLLが生成されます。
    開発中はDebugビルドで問題ありませんが、本番用のDLLを出力する場合はReleaseビルドにするのを忘れないようにしましょう。

    4. Unityのプロジェクトに配置する

    Unityのプロジェクトに配置します。
    パスの指定などはありませんので、わかりやすい場所に配置してください。

    1~3でDLLを作成した場合は、ビルド後イベントでUnityのプロジェクトにコピーする処理を書いておくと便利です。

    5. マーシャリングをしてDLLの処理を呼び出す

    さて、今回のメイントピックのマーシャリングです。
    先ほどUnityに配置したDLL内に2で作成した以下のコードが含まれてるとして、これをC#上からマーシャリングして呼び出すとします。

    int IntTest( int x )
    {
    	x *= 2;
    	return x;
    }

    C#側の記述は以下の通りになります。

    // 宣言部
    [DllImport("NativePlugin.dll")]
    public static extern int IntTest( int x );
    
    // 呼び出し例
    private void hoge(){
    	int num = IntTest( 1 );
    	UnityEngine.Debug.Log( num );	// 2が出力される。
    }

    思ったよりも簡単ですね。
    今回は一番シンプルな対応例で、ざっくりと説明すると以下の通りです。

    [DllImport(“NativePlugin.dll”)]
    この宣言時のアトリビュート指定がマーシャリングの本質で、ネイティブプラグインを使用する際にはこの宣言が必須です。
    括弧内の文字列 “NativePlugin.dll” は、どのネイティブプラグインから関数を呼び出すかを指定します。
    呼び出し方が特殊になればなるほど、このアトリビュートの記述が増えて複雑になっていきます。

    マーシャリングが上手く行えていれば、呼び出す際は通常の関数と同様に使用できます。

    public static extern int IntTest( int x );
    宣言時は必ずstatic externを付ける必要があります。
    また、今回は必要ありませんでしたが、C言語とC#では変数の名前が異なる場合がありますので記述を合わせる必要があります。

    対応表は以下の通りです。

    C言語C#
    charsbyte
    unsigned charbyte
    shortshort
    unsigned shortushort, char
    intint
    unsigned intuint
    long longlong
    unsigned long longulong
    floatfloat
    doubledouble
    size_t IntPtr

    上記の対応表で示されているものは、マーシャリングの際にただ文字列を置き換えるだけで実行できるようになります。
    これらはC言語のデフォルトで用意されているプリミティブ型であり、マーシャリング的にはBlittable型と呼ばれます。

    それ以外のものは非Blittable型とされ、データ変換が必要になるので対応が少々難しくなります。
    間違いやすいものとして、bool型は非Blittable型になりますのでデータ変換の記述が必要になります。
    ※今回はbool型は解説しませんがint型として実装する形だと簡単に対応できます。

    難しいマーシャリング

    ステップアップとして、非Blittable型の中でも比較的簡単でよく使う『ポインタ・配列』『構造体(Struct)』のマーシャリングをしてみましょう。

    対応表としては以下の通りですが、それぞれいくつか補足や注意点がありますので後述で説明します。

    C言語C#
    各種ポインタ・配列引き数:ref / In / Out [ポインタ・配列の型]
    戻り値:IntPtr
    構造体引き数:ref / In / Out [構造体]
    戻り値:IntPtr
    ※後述

    各種ポインタ・配列

    各種ポインタや配列は、引き数であればref / in / outパラメータで対応できます。
    ただし、inを指定していたとしてもメソッド内で変更されてしまう等予期せぬ不具合が起こりえますので、パラメータの指定と内部処理には注意を払い適宜対応してください。

    戻り値の場合はIntPtrを介して取得ができます。
    戻り値の型が参照型の場合は、ポインタではなく元の型を指定しても取得できますが、安全性のために一貫してIntPtrを使用することをおすすめします。

    配列はポインタと取得方法に変わりはありませんが、サイズも同時に授受する必要があります。

    ※補足ですが、IntPtrという名前からInt型のみのポインタのように見えますが、(恐らく)すべてのポインタ型を扱えますのでご安心ください。

    // 引数にポインタがある場合
    void SetValue( int* iPtr )
    {
    	// 処理
    }
    
    // 戻り値にポインタがある場合
    float* GetPtr()
    {
    	return &fVal;
    }
    
    // 引数に配列がある場合
    void SetArray( int* iPtr, int size )
    {
    	// 処理
    }
    
    // 戻り値に配列がある場合
    float* GetArray( int* pOut_size )
    {
    	// 処理
    
    	*pOut_size = size;	// サイズを返す必要がある
    	return array;
    }
    // 引数にポインタ指定がある場合、refの例。
    [DllImport("NativePlugin.dll")]
    public static extern void SetValue( ref int iPtr );
    
    // 戻り値にポインタがある場合
    [DllImport("NativePlugin.dll")]
    public static extern IntPtr GetPtr();
    
    // 引数に配列がある場合
    [DllImport("NativePlugin.dll")]
    public static extern void SetArray( in int[] iArray, int size );
    
    // 戻り値に配列がある場合
    [DllImport("NativePlugin.dll")]
    public static extern IntPtr GetArray( out int size );
    
    // 使用例
    void hoge()
    {
    	// 引数にポインタ指定がある場合
    	{
    		int val = 0;
    		SetValue( ref val );
    	}
     
     // 戻り値にポインタがある場合
     // 普通はプリミティブ型をポインタで戻り値にしないが一応…
     {
    		IntPtr iptr = GetPtr();
    		int size = 1;
    		float[] fArray = new float[size];
    		Marshal.Copy( iptr, fArray, 0, size );	// fArray[0]に値が入る
     }
    
    	// 引数に配列がある場合
    	{
    		int[] iArray = { 1, 2, 3 };
    		SetArray( in iArray, iArray.Length );
    	}
    
    	// 戻り値に配列がある場合
    	{
    		int size;
    		IntPtr iptr = GetArray( out size );
    		float[] fArray = new float[size];
    		Marshal.Copy( iptr, fArray, 0, size );
    	}
    }

    IntPtrで受け取ったポインタはMarshalクラスの機能を使用して配列や値を取得できます。

    struct

    構造体の場合も配列やポインタと同様、引き数の場合はref / in / outパラメータ、戻り値の場合はIntPtrで対応できますが、アライメントまわりの対処が必要になります。

    プラットフォームによって構造体のバイト境界が異なる可能性があるため、明示的に指定しておかないと予期せぬエラーが起きてしまいます。
    バイト境界、構造体のアライメント・パディングはしっかりと把握しておきましょう。

    バイト境界やアライメント・パディングについてわからない場合は別サイトをご参照ください。

    // 構造体の定義
    #pragma pack( push, 4 )
    struct TEST{
    	int   a;
    	char  b;
    	float c;
    };
    #pragma pack( pop )
    
    // 引数の場合
    void SetStruct( TEST* data )
    {
    	// 処理
    }
    
    // 戻り値の場合
    TEST*	GetStruct( void ){
    	return str;
    }

    #pragma pack( push, 4 )
    #pragma pack( pop )
    これらは構造体のバイト境界を制御するもので、定義の開始と終了に配置します。

    開始の引き数指定の『4』は4バイト境界にすることを示しています。
    好き好きで構いませんが、個人的には馴染み深いので4バイト境界にしています。

    これと同様のことをC#側でも行うことで問題なく取得できるようになります。

    // 構造体の定義
    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public struct TEST{
    	public int   a;
    	public sbyte b;
    	public float c;
    }
    
    // 引数の場合
    [DllImport("NativePlugin.dll")]
    public static extern void SetStruct( in TEST data );
    
    // 戻り値の場合
    [DllImport("NativePlugin.dll")]
    public static extern IntPtr GetStruct( void );
    
    // 使用例
    void hoge()
    {
    	// 引数の場合
    	{
    		var test = new TEST(){ a = 1, b = 2, c = 3.4f };
    		SetStruct( in test )
    	}
    	
    	// 戻り値の場合
    	{
    		IntPtr iptr = GetStruct();
    		TEST str = Marshal.PtrToStructure<TEST>( iptr );
    	}
    }

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    C#で構造体のバイト境界を指定する場合はこの指定を行います。

    Marshal.PtrToStructure<TEST>( iptr );
    戻り値で受け取ったポインタはMarshalクラスを使用して構造体に戻すことができます。

    今回は構造体のメンバをBlittable型で統一したのでシンプルになりましたが、非Blittable型や構造体の入れ子にするとまた少し書き方が変わってくるのでご注意ください。

    さいごに

    いかがだったでしょうか。
    マーシャリングは使う機会が限定的なせいか入門的なサイトがあまり多くない印象だったので取り扱ってみました。
    解りやすく説明できていれば幸いです。

    初心者でも直ぐに実装に取り掛かれるように細かな挙動などは省きましたので、より高度なマーシャリングを勉強する際の足掛かりとして役立てればと思います。

    文字列の扱いは特に難しいので、チャレンジの際はしっかり調べてみてください。
    個人的には参考にリンクを張りました『Unityで使うC#/DLLマーシャリング事典』がとてもわかりやすかったのでオススメです。

    それでは、良きマーシャリングライフを!

    参考


    【免責事項】

    本サイトでの情報を利用することによる損害等に対し、
    株式会社ロジカルビートは一切の責任を負いません。