[unity]C# から C / C++ で書かれたライブラリのメソッドを呼び出したい

結論から行くと、そこそこ簡単にアクセスする事ができます。
あくまで、そこそこ、です。初心者であればつまづく点は結構あります。
引数を渡す時などちょっとクセがありますね。

unity としましたが、unity に限らず C# と C / C++ の汎用的なやりとりです。

GitHub

今回テストしたプロジェクトは、以下で公開しています。

C++ のライブラリ(DLL)を作る

VS2019 であればこちら。ダイナミックで検索するとすぐ見つかると思います。
ここではプロジェクト名を NativeDLL とします。
場所はどこでもいいのですが、私は unity project の下に作成しました。

作成したプロジェクトの dll_main.cpp を次のコードで置き換えます。

#include "pch.h"

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

構成マネージャーを Debug - x64 にしてビルド - ソリューションのビルドx64/Debug/ というフォルダに NativeDLL.dll というファイルが生成されます。

ソリューション(sln ファイル)とプロジェクト(vcxproj ファイル)を別々のフォルダにした場合は x64/Debug が2つあるかもしれません(ここらへん、紛らわしい)。
Native.DLL.dll が入っているフォルダを探しましょう。

デバッグコードが不要の場合は、Release - x64 で構いません。最終的には Release にするといいと思います。

DLL を unity に配置する

先ほどの DLL を配置するだけ(Assets/ 下にコピー。ディレクトリは任意に)で構いません。

注意:この DLL は「配置した瞬間」と「プロジェクトを開いた瞬間」のみ unity によってロードされ、以後リロードされる事はありません。

これの何が問題かというと、一旦配置した DLL のソースをちょっとだけ直してもう1回更新(Assets/ に上書きコピー)しても、内容が反映されないということです。

対処法としては思いついたのは2つ。

  1. プロジェクトを一旦閉じ、再度開く
  2. 以前の DLL を消し、名前をちょっとだけ変えて(Test.dll -> Test1.dll とか)再配置

1 はさすがにダルいので、2 がいいんじゃないかと思います。

もっといい方法はないものか…。

C# から呼び出してみる

適当な GameObject を配置し、NativePluginSample というスクリプトをアタッチします。

スクリプトを次のように書き換えます。

using System.Runtime.InteropServices;
using UnityEngine;

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

    void Start()
    {
        int a = 2;
        int b = 3;
        int c = Test(a, b);

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

実行すると、この通り!

色々な引数を試してみる

int だけでは使い道が限られるので、色々な引数を試してみます。
Edit > Project Settings > Player > Other Settings にある Allow 'unsafe' Code にチェックを入れてください。

C# だけではそうそう起こらない、unity が簡単にクラッシュする「やばい事」をする…という自覚を持ちましょう。

C++ のコード (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, int length)
    {
        if (length >= 1) msg[0] = 'i';
        if (length >= 2) msg[1] = 'n';
        if (length >= 3) msg[2] = 't';
        if (length >= 4) msg[3] = 'i';
        if (length >= 5) msg[4] = 'n';
        if (length >= 6) msg[5] = 't';
    }

    __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;
    }
}

C# のスクリプト (NativePluginSample.cs)

using System;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;

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

    [DllImport("NativeDLL")]
    private static extern string TestString(StringBuilder msg, int length);

    [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}");

        // StringBuilder を渡す。c++ 側で書き換えて元に戻す
        StringBuilder s = new StringBuilder();
        s.Append("string argument");

        TestString(s, s.Length);

        Debug.Log($"{s}");

        // 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);

        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 の解放を忘れずに
    }
}

int, string, byte data, struct の4パターンについて相互に値を渡すサンプルです。
実行するとこのように結果が返ってきます。

以下、1つ1つ抜粋し、解説していきます。

int

// int
int a = 2;
int b = 3;
int c = Test(a, b);

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

2 と 3 を c++ に渡し、足した結果(5)を返します。

string

//string
StringBuilder s = new StringBuilder();
s.Append("string argument");
TestString(s, s.Length);

Debug.Log($"{s}");

文字列の場合、C++ に渡すだけであれば string で構いませんが、C++ で変更した値を C# に返す場合は StringBuilder にする必要があります。
C++ では const char* 型となります。

C++ での文字加工に失敗すると簡単に unity が落ちます。注意してプログラムしましょう。

byte data

//byte data
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);

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

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

バイトデータの場合、少々複雑です。以下の2工程が必要になります。

  1. byte[] を Marshal で確保したメモリにコピーし、C++ に渡す
  2. C++ から返ってきた値を byte[] に変換する

また、確保したメモリは忘れずきちんと解放しましょう。

構造体 (struct)

//構造体
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 の解放を忘れずに

構造体は、バイトデータと大体同じ仕組みです。
struct でのみ可能、class は無理なので注意してください。
(クラスはメモリ配置が見た目通りとならないため、C++ とのやりとりは出来ません)

こちらも、確保したメモリを忘れずきちんと解放しましょう。

終わりに

どれだけ需要があるかわかりませんが、例えば Cocos で作った昔のリソースを unity で使いたい…暗号化ロジック部分は C++ で隠蔽したい…等々役立つ場面があったりなかったり?

想像以上に簡単ではあるものの、やはり C# で楽に慣れちゃうと C++ は…。
サンプル作るだけで何度か unity がクラッシュしてしまいました😢
メモリ解放を忘れた場合は、開発終盤で地獄のデバッグ作業を強いられる事でしょう…。

用法、用量は適切に!

返信を残す

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

CAPTCHA