[unity]c# スクリプトから c++ の関数を呼び出す

それほど機会はありませんが、Cocos や Unreal engine、その他諸々で作成された DLL を C# から呼び出したい事があるかもしれません。

ここでは簡単にその方法を実践してみます。

テスト方法

DLL の用意(c++ で DLL を作成する)

  • ダイナミックリンクライブラリ (DLL) を選びます。プロジェクト名は NativeDLL とします。
  • dllmain.cpp を次の内容に書き換えます。
#include "pch.h"

typedef struct MyStruct
{
    int   a;
    short b[8];
} MyStruct;

extern "C"
{
    __declspec(dllexport) int __stdcall Test(int a, int b)
    {
        return a + b;
    }

    __declspec(dllexport) void __stdcall TestString(char* msg)
    {
    }

    __declspec(dllexport) void __stdcall TestByteData(char* array, int length)
    {
        array[0] = 4;
        array[1] = 3;
        array[2] = 2;
        array[3] = 1;
        array[4] = 0;
    }

    __declspec(dllexport) void __stdcall TestStructure(MyStruct* data)
    {
        data->a = 10;
        data->b[0] = 1;
        data->b[1] = 2;
        data->b[2] = 3;
        data->b[3] = 4;
        data->b[4] = 5;
    }
}
  • Debug – x64Release – x64 でビルドしてください。

unity 側の用意

  • unity のプロジェクトを作成、Assets/ 以下に先ほど用意した NativeDLL.dll をコピーします。
  • GameObject と NativePluginSample というスクリプトを作成し、GameObject に関連づけます。
  • NateviPluginSample.cs を次の内容に置き換えます。
    (色々な引数を試しているので、コードは長め)
using System;
using System.Runtime.InteropServices;
using UnityEngine;

public unsafe class NativePluginSample : MonoBehaviour
{
    [DllImport("NativeDLL")]
    private static extern int Test(int a, int b);

    [DllImport("NativeDLL")]
    private static extern void TestString(string msg);

    [DllImport("NativeDLL")]
    private static extern void TestByteData(char* array, int length);

    [DllImport("NativeDLL")]
    private static extern void TestStructure(IntPtr pStruct);

    struct MyStruct
    {
        public int a;
        public fixed short b[8];
    }

    void Start()
    {
        // int を渡す
        int a = 2;
        int b = 3;
        int c = Test(a, b);

        Debug.Log($"{a} + {b} = {c}");

        // string を渡す。c++ 側で string の加工は手続きが面倒なので割愛
        TestString("string argument");

        // byte[] 型のデータを渡す。c++ 側で書き換えて元に戻す
        byte[]    bytes   = { 0, 1, 2, 3, 4,};

        // 1. c++ に渡すためのデータを作る
        int       size    = Marshal.SizeOf(typeof(byte)) * bytes.Length;
        IntPtr    ptr     = Marshal.AllocCoTaskMem(size);
        Marshal.Copy(bytes, 0, ptr, bytes.Length);
        char*     charptr = (char*)(ptr.ToPointer());

        // 2. c++ の関数をコール
        TestByteData(charptr, bytes.Length);

        // 3. c++ に渡したデータの変化を c# に返す
        Marshal.Copy(ptr, bytes, 0, bytes.Length);  // dest と startIndex の位置が変わる…

        Debug.Log($"byte: {bytes[0]}, {bytes[1]}, {bytes[2]}, {bytes[3]}, {bytes[4]}");

        Marshal.FreeCoTaskMem(ptr); // Alloc の解放を忘れずに

        // 構造体のデータを渡す。c++ 側で書き換えて元に戻す
        MyStruct ins = new MyStruct();
        
        // 1. c++ に渡すためのデータを作る
        IntPtr pStructure = Marshal.AllocCoTaskMem(Marshal.SizeOf(ins));
        Marshal.StructureToPtr(ins, pStructure, false);

        // 2. c++ の関数をコール
        TestStructure(pStructure);

        // 3. c++ に渡したデータの変化を c# に返す
        ins = (MyStruct)Marshal.PtrToStructure(pStructure, typeof(MyStruct));

        Debug.Log($"struct: a/{ins.a}, b0/{ins.b[0]}, b1/{ins.b[1]}, b2/{ins.b[2]}");

        Marshal.FreeCoTaskMem(pStructure); // Alloc の解放を忘れずに
    }
}

プログラムを実行

無事実行された場合、unity のコンソールに次のように表示されます。

ここまでのテストプロジェクト

GitHub にひっそりとあげておきます。

簡単に説明

基本パターン

ミニマムな基本パターンは int 引数、int 戻り値。この場合、C# と C++ で大きな差はありません。

C++ 側で extern “C”__declspec(dllexport) [戻り値] __stdcall というキーワードによって、C# からコールされる事を示し、C# 側で DllImport 宣言を行い、使用可能にします。

[C++]
extern "C"
{
    __declspec(dllexport) int __stdcall Test(int a, int b)
    {
        return a + b;
    }
}

[C#]
public class NativePluginSample : MonoBehaviour
{
    [DllImport("NativeDLL")]
    private static extern int Test(int a, int b);

    void Start()
    {
        Test(1, 2);
    }
}

バイト配列を渡す

C# の配列と C++ の配列は内部構造的な形が異なるため、引数として渡すためにはマーシャリングという手続きで C++ に合わせる必要があります。

        int       size    = Marshal.SizeOf(typeof(byte)) * bytes.Length;
        IntPtr    ptr     = Marshal.AllocCoTaskMem(size);
        Marshal.Copy(bytes, 0, ptr, bytes.Length);
        char*     charptr = (char*)(ptr.ToPointer());

この Alloc したメモリは GC で回収されないメモリなので、使用後は明示的に解放してください。

        Marshal.FreeCoTaskMem(ptr);

また、C++ から返ってきた配列の内容を C# で使いたい場合は逆変換を行ってください。
Marshal.Copy がそれです。C# → C++ の変換と、C++ → C# の場合、引数の順序が違うことに注意。

        // 2. c++ の関数をコール
        TestByteData(charptr, bytes.Length);

        // 3. c++ に渡したデータの変化を c# に返す
        Marshal.Copy(ptr, bytes, 0, bytes.Length);

なお、C++ 側で新たにメモリ確保したバッファを使う…といった事も可能かもしれませんが、ワケがわからなくなるので個人的には控えた方が無難だと思います。

これは string を渡す場合に特に言える事です。C++ と C# では文字列の扱いに天と地の差があるので、C++ から C# に文字を返すような構造にするならば、より深い理解(とコーディング)が必要です。

構造体を渡す

多少大きめな情報を渡したり、返したりしたい場合は構造体が便利だと思います。
C++ の場合、C# のような可変長バッファを気軽に作れないため、予め大きさを決めてやりとりします。
C# と C++ の構造体のサイズが一致するよう、気をつけてください。

確認はしていませんが、固定長バッファは 4 の倍数にしておくと、余計な問題が起こりにくくていいと思います。

[C#]
struct MyStruct
{
    public int a;
    public fixed short b[8];
}

MyStruct ins = new MyStruct();
IntPtr pStructure = Marshal.AllocCoTaskMem(Marshal.SizeOf(ins));
Marshal.StructureToPtr(ins, pStructure, false);

[C++]
typedef struct MyStruct
{
    int   a;
    short b[8];
} MyStruct;

__declspec(dllexport) void __stdcall TestStructure(MyStruct* data)
{
    data->a = 10;
    data->b[0] = 1;
    data->b[1] = 2;
    data->b[2] = 3;
    data->b[3] = 4;
    data->b[4] = 5;
}

C++ で変更した値を C# で受け取るには、バイト配列同様、逆変換が必要です。

ins = (MyStruct)Marshal.PtrToStructure(pStructure, typeof(MyStruct));

返信を残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA