結論から行くと、そこそこ簡単にアクセスする事ができます。
あくまで、そこそこ、です。初心者であればつまづく点は結構あります。
引数を渡す時などちょっとクセがありますね。
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 がクラッシュしてしまいました😢
メモリ解放を忘れた場合は、開発終盤で地獄のデバッグ作業を強いられる事でしょう…。
用法、用量は適切に!




