在介绍复杂数据类型的传递之前,先说一下如何在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.ByteSystem.SByteSystem.Int16System.UInt16System.Int32System.UInt32System.Int64System.UInt64System.IntPtrSystem.UIntPtrSystem.SingleSystem.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.hstruct Person {public: int Age; float Height; int Scores[4]; bool Married; };extern "C"{ UNITY_CPP_INTEROP_DLL_BRIDGE void Internal_ChangePerson(Person* p); }//Bridge.cppextern "C"{ void Internal_ChangePerson(Person* p) { p->Age += 10; p->Height += 10; p->Scores[0] += 10; p->Scores[1] += 10; p->Married = true; } }
输出如下:
ChangePerson.png
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.hextern "C"{ UNITY_CPP_INTEROP_DLL_BRIDGE void Internal_ChangeArray(int* arr, int len); }//Bridge.cppextern "C"{ void Internal_ChangeArray(int* arr, int len) { for (int i = 0; i < len; i++) { arr[i]++; } } }
ChangeArray.png
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.hextern "C"{ UNITY_CPP_INTEROP_DLL_BRIDGE void Internal_ChangePersonArray(Person* arr, int len); }//Bridge.cppextern "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++; } } }
ChangePersonArray.png
struct数组作为参数传递时需要使用[In, Out]特性,详见0,详见1
作者:食不知味_夜不能寐
链接:https://www.jianshu.com/p/8877c1d3f2e2
共同学习,写下你的评论
评论加载中...
作者其他优质文章