在介绍复杂数据类型的传递之前,先说一下如何在C++中回调C#函数。
一、delegate与函数指针
Unity与C++交互最麻烦的是调试的过程,在C++ DLL中直接print或cout打印log是没法看到的,我们可以在C++中调用C#的函数来输出log,这需要将delegate映射到C++的函数指针。
在上一节用到的C#脚本中添加如下代码,并在Start()的第一行调用RegisterDebugCallback()。
void RegisterDebugCallback()
{
DebugDelegate callback_delegate = CallBackFunction;
//将Delegate转换为非托管的函数指针
IntPtr intptr_delegate = Marshal.GetFunctionPointerForDelegate(callback_delegate);
//调用非托管函数
SetDebugFunction(intptr_delegate);
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void DebugDelegate(IntPtr strPtr);
[DllImport("UnityCppInterop")]
public static extern void SetDebugFunction(IntPtr fp);
static void CallBackFunction(IntPtr strPtr)
{
Debug.LogError("CpppppppppLog: " + Marshal.PtrToStringAnsi(strPtr));
}
IntPtr代表的是C++的指针,Marshal.GetFunctionPointerForDelegate()的作用是将C#的委托转化为函数指针,通过SetDebugFunction()将回调函数注册到C++中。
在C++项目中新建Debuger类:
//Debuger.h
#pragma once
#include <iostream>
#include <string>
using namespace std;
class Debuger
{
public:
typedef void(*DebugFuncPtr)(const char *);
static DebugFuncPtr FuncPtr;
static void SetDebugFuncPtr(DebugFuncPtr ptr);
static char container[100];
static void Log(const string str);
};
//=========================================================================
//Debuger.cpp
#include "Debuger.h"
Debuger::DebugFuncPtr Debuger::FuncPtr;
void Debuger::SetDebugFuncPtr(DebugFuncPtr ptr)
{
FuncPtr = ptr;
}
char Debuger::container[100];
void Debuger::Log(const string str)
{
if (FuncPtr != nullptr)
{
FuncPtr(str.c_str());
}
}
修改Bridge类:
//Bridge.h
#ifdef WIN32
#ifdef UNITY_CPP_INTEROP_DLL_BRIDGE
#define UNITY_CPP_INTEROP_DLL_BRIDGE __declspec(dllexport)
#else
#define UNITY_CPP_INTEROP_DLL_BRIDGE __declspec(dllimport)
#endif
#else
// Linux
#define UNITY_CPP_INTEROP_DLL_BRIDGE
#endif
#include <string>
#include <sstream>
#include "Debuger.h"
using namespace std;
extern "C"
{
UNITY_CPP_INTEROP_DLL_BRIDGE void SetDebugFunction(Debuger::DebugFuncPtr fp);
UNITY_CPP_INTEROP_DLL_BRIDGE int Internal_Add(int a, int b);
}
//===============================================
//Bridge.cpp
#include "Bridge.h"
extern "C"
{
void SetDebugFunction(Debuger::DebugFuncPtr fp)
{
Debuger::SetDebugFuncPtr(fp);
}
int Internal_Add(int a, int b)
{
int res = a + b;
stringstream ss;
ss << res;
Debuger::Log(ss.str());
return res;
}
}
运行可以看到输出为:CpppppppppLog: 11
,说明C++中的log成功打印了。
二、Marshal和Blittable
1.Marshal(封送):指的是将数据从托管内存封送到非托管内存的过程。
2.Blittable和Non-blittable:blittable表示可以被直接复制到非托管内存,而不需要Marshal进行转换处理的数据类型,non-blittable相反。(详见Micorsoft官方文档)
Blittable类型包括:
System.Byte
System.SByte
System.Int16
System.UInt16
System.Int32
System.UInt32
System.Int64
System.UInt64
System.IntPtr
System.UIntPtr
System.Single
System.Double
此外,blittable类型的一维数组(如:int[]),以及只包含blittable类型的struct或class(如:struct中只包含int, float等),也属于blittable。
Non-blittable类型包括:
Non-blittable 类型 | 描述 |
---|---|
System.Array | 转换为 C 样式数组或 SAFEARRAY 。 |
System.Boolean | 转换为 1、2 或 4 字节的值,true 表示 1 或 -1。 |
System.Char | 转换为 Unicode 或 ANSI 字符。 |
System.Class | 转换为类接口。 |
System.Object | 转换为变量或接口。 |
System.Mdarray | 转换为 C 样式数组或 SAFEARRAY 。 |
System.String | 转换为空引用中的终止字符串或转换为 BSTR。 |
System.Valuetype | 转换为具有固定内存布局的结构。 |
System.Szarray | 转换为 C 样式数组或 SAFEARRAY 。 |
delegate |
C#与C++之间进行数据传递(函数参数、函数返回值等)时,blittable类型可以直接作为参数传递,non-blittable需要借助Marshal做相应转换,但作为函数返回的数据,必须是blittable类型。
三、struct和class的传递
前面提到只包含blittable类型的struct或class也属于blittable类型,因此对于简单的struct和class可以直接传递。
先看个简单的例子,在C#端定义Person结构体,并定义胶水方法ChangePerson()。
//TestCppInterop.cs
[StructLayout(LayoutKind.Sequential)]
public struct Person
{
public int Age;
public float Height;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public int[] Scores;
public bool Married;
}
[DllImport("UnityCppInterop", EntryPoint = "Internal_ChangePerson")]
private static extern int ChangePerson(ref Person p);
void Start()
{
Person p = new Person()
{
Age = 18,
Height = 178.6f,
Scores = new[] { 66, 77, 88, 99 },
Married = false
};
Debug.LogError("==========Before Cpp Change============");
Debug.LogError(string.Format("Age: {0}, Height: {1}, Score0: {2}, Score1: {3}, Sex: {4}", p.Age, p.Height, p.Scores[0], p.Scores[1], p.Married));
ChangePerson(ref p);
Debug.LogError("==========After Cpp Change============");
Debug.LogError(string.Format("Age: {0}, Height: {1}, Score0: {2}, Score1: {3}, Sex: {4}", p.Age, p.Height, p.Scores[0], p.Scores[1], p.Married));
}
在C++端也要定义同样的结构体Person,和ChangePerson()
//Brige.h
struct Person
{
public:
int Age;
float Height;
int Scores[4];
bool Married;
};
extern "C"
{
UNITY_CPP_INTEROP_DLL_BRIDGE void Internal_ChangePerson(Person* p);
}
//Bridge.cpp
extern "C"
{
void Internal_ChangePerson(Person* p)
{
p->Age += 10;
p->Height += 10;
p->Scores[0] += 10;
p->Scores[1] += 10;
p->Married = true;
}
}
输出如下:
StructLayout:传递struct或class时,C#和C++两端的struct映射是按照逐个变量去映射的,但.Net出于优化内存占用的目的,有可能会调整成员变量的排布顺序,而C++编译器不会做这些优化,为了保证两端的struct内存布局一致,需要标记[StructLayout(LayoutKind.Sequential)]
特性防止.Net进行优化。
struct内包含数组:此时必须指定数组大小,[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
在C++中,struct 和 class 除了成员的默认访问权限不一致外,二者基本一样,因此将C#中的struct映射为C++的struct或class都是可以的,C#的class也一样。但在C#中,struct是值类型,class是引用类型,由于C++端使用的是指针,struct类型作为参数传递时,必须使用ref关键字进行引用传递,class则无需ref关键字。
UnityEngine.Vector3:Vector3的成员变量布局顺序是x->y->,在C++中定义相同的Vector3结构体后,便可直接将UnityEngine.Vector3作为参数传递,Quarternion同理。
四、数组的传递
1. 普通数组的传递
传递普通数组很简单,只需要注意同时将数组的长度作为参数传递即可。
//TestCppInterop.cs
[DllImport("UnityCppInterop", EntryPoint = "Internal_ChangeArray")]
private static extern int ChangeArray(int[] array, int size);
void Start()
{
int[] numArray = new[] { 1, 2, 3 };
ChangeArray(numArray, numArray.Length);
for (var i = 0; i < numArray.Length; i++)
{
Debug.LogError(numArray[i]);
}
}
//Bridge.h
extern "C"
{
UNITY_CPP_INTEROP_DLL_BRIDGE void Internal_ChangeArray(int* arr, int len);
}
//Bridge.cpp
extern "C"
{
void Internal_ChangeArray(int* arr, int len)
{
for (int i = 0; i < len; i++)
{
arr[i]++;
}
}
}
2. struct数组的传递
还是用上面的Person,同样需要传递数组长度。
//TestCppInterop.cs
[DllImport("UnityCppInterop", EntryPoint = "Internal_ChangePersonArray")]
private static extern int ChangePersonArray([In, Out]Person[] array, int size);
void Start()
{
var p1 = new Person(){ Age = 11, Height = 133, Married = false, Scores = new[] { 66, 77} };
var p2 = new Person() { Age = 22, Height = 177, Married = true, Scores = new[] { 88, 99 } };
Person[] persons = new Person[2];
persons[0] = p1;
persons[1] = p2;
ChangePersonArray(persons, 2);
foreach (var person in persons)
{
Debug.LogError(person.Age);
Debug.LogError(person.Height);
Debug.LogError(person.Married);
Debug.LogError(person.Scores[0]);
Debug.LogError(person.Scores[1]);
Debug.LogError("========================");
}
}
//Bridge.h
extern "C"
{
UNITY_CPP_INTEROP_DLL_BRIDGE void Internal_ChangePersonArray(Person* arr, int len);
}
//Bridge.cpp
extern "C"
{
void Internal_ChangePersonArray(Person* arr, int len)
{
Person* curr = arr;
for (int i = 0; i < len; i++)
{
arr->Age += 10;
arr->Height += 10;
arr->Married = !arr->Married;
arr->Scores[0] += 10;
arr++;
}
}
}