結論から行くと、そこそこ簡単にアクセスする事ができます。
あくまで、そこそこ、です。初心者であればつまづく点は結構あります。
引数を渡す時などちょっとクセがありますね。
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つ。
- プロジェクトを一旦閉じ、再度開く
- 以前の 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工程が必要になります。
- byte[] を Marshal で確保したメモリにコピーし、C++ に渡す
- 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 がクラッシュしてしまいました😢
メモリ解放を忘れた場合は、開発終盤で地獄のデバッグ作業を強いられる事でしょう…。
用法、用量は適切に!