项目进行中,使用Unity 对接非托管DLL时,遇到问题,翻遍了资料没有看见太多直接性帮助的代码,只能在请教了相应的大佬后,自己再一点点的摸过来,最终做一下汇总,希望对后续遇到相应问题的同学有所帮助。
DLL是 Dynamic Link Library 的縮写,中文意思为动态链接库文件,
Unity 导入的 DLL 库主要分为两种,托管DLL和非托管 DLL
简而言之,相应的C/C++(非托管代码)编写的DLL为非托管 DLL,
C#或其他托管代码编写的 DLL为托管 DLL
Unity引用托管DLL,需要将相应DLL放入Assets文件夹下的Plugins文件夹内,在VS工程内,添加相应依赖,使用时,直接引入相应命名空间即可调用相应DLL内部函数。
这里主要讲下 Unity 引用非托管 DLL。
Uinty.引用非托管 DLL 的方法
代码如下(示例):
[DllImport("$ DLLName", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
Unity导入非托管DLL的方法为[DLLImport(“方法名”)] ,下面是一些其余参数
EntryPoint 指定要调用的 DLL 入口点。在不指定时,则采用声名的托管函数名称。
SetLastError 判断在执行该方法时是否出错(使用 Marshal.GetLastWin32Error API 函 数来确定)。C#中默认值为 false。
CharSet 控制名称及函数中字符串参数的编码方式。默认值为 CharSet.Ansi。
ExactSpelling 是否修改入口点以对应不同的字符编码方式。
CallingConvention 指定用于传递方法参数的调用约定。默认值为 WinAPI。该值对应于基于32位Intel平台的 __stdcall。(注:CallingConvention的值为枚举,分为Winapi,Cdecl,StdCall,ThisCall,FastCall,本次交互中使用的是Cdecl, 关于PASCAL这种调用约定的函数都是由它本身来清栈,而__cdecl的函数都是由调用者来清栈)
BestFitMapping 是否启用最佳映射功能,默认为 true。最佳映射功能提供在没有匹配项时,自动提供匹配的字符。无法映射的字符通常转换为默认的“?”。
PreserveSig 托管方法签名是否转换成返回 HRESULT,默认值为 true(不应转换签名)。并且返回值有一个附加的 [out, retval] 参数的非托管签名。
ThrowOnUnmappableChar 控制对转换为 ANSI ‘?’ 字符的不可映射的 Unicode 字符引发异常。
Unity基于.net平台,应用内存为托管型内存,非托管DLL应用内存为非托管型内存,二者在内存上的分布时分开的,所以在进行数据传输时,需要进行相应的内存传递。在内存传递时,发送方的发送类型和接收方的接受类型,都必须具有相同的表示,即内存表示应该相同,例如:非托管函数形参为int类型,那么托管向该函数传递参数必须是32bit System.Int类型。 大多数数据类型在托管和非托管内存中都有一个通用的表示,传递时不须要的特殊处理,这些类型被称为blittable类型。经常使用的blittable类型包括
Int,
Double,
Enum(枚举类型)
具有实例字段只有 blittable 值类型的固定布局的结构固定的布局需要 [StructLayout(LayoutKind.Sequential)] 或 [StructLayout(LayoutKind.Explicit)]
默认情况下结构为 LayoutKind.Sequential。
关于接口中的非bilttable类型,除了Bool类型外,其余的附带函数指针的类型我们一般转换为IntPtr类型,C#中的IntPtr类型称为“平台特定的整数类型”
在进行上文所说的由托管类型向非托管类型转换的过程中,我们通常使用Marshal 类来完成上述操作,Marshal类提供了一个方法集合,这些方法用于分配非托管内存、复制非托管内存块、将托管类型转换为非托管类型,此外还提供了在与非托管代码交互时使用的其他杂项方法。Unity在使用相关方法时,需要引入命名空间“System.Runtime.InteropServices”,
Marshal类的具体函数方法,请参考 ”https://docs.microsoft.com/zh-cn/dotnet/api/system.runtime.interopservices.marshal?view=net-6.0“。
这里只举例几个简单应用
1,Marshal.SizeOf(Type)
该方法返回非托管类型的大小(以字节为单位)
2,PtrToStructure(IntPtr)
将数据从非托管内存块封送到泛型类型参数指定的类型的新分配托管对象
3 Marshal.UnsafeAddrOfPinnedArrayElement(Array, int32)
获取指定类型的数组中指定索引处的元素地址。
4 PtrToStructure(IntPtr, Type)
将数据从非托管内存块封送到新分配的指定类型的托管对象
这里我们写一些伪代码来做范例,先写一点Dll接口文档,设定Dll名称为 “Demo”
// Dll 接口原型
SampleDemo GetDemo();
//Dll 返回结构体实例
struct SampleDemo
{
Sublevel m_SubLevel;
}
struct Sublevel
{
Sublevel_interface* m_Sublevel_interface;
int m_Sublevel_interfaceNum;
}
struct Sublevel_interface
{
int ID;
char* Name;
int NameLenth;
GroundType Type;
void* Sublevel_Ground;
}
enum GroundType
{
Ground1;
Ground2;
}
struct Ground1
{
}
struct Ground2
{
}
OK,上面就是C++编译Dll后给的接口文档,包括接口名称“GetDemo”,包括返回的结构体示例。可以说是一个很复杂的结构体了。其中特别标注了
1,“Sublevel_interface*”为一个“Sublevel_interface” 数组,数组长度为“m_Sublevel_interfaceNum”
2,“GroundType” 为一个 枚举,根据Type不同,“Sublevel_Ground”的实例分别为Ground1,Ground2。
接下来我们需要写一下C#中对接部分的代码,
需要注意, C#对接Dll结构的结构体,要采用上面说的类型转换
[StructLayout(LayoutKind.Sequential)]
public struct SampleDemo
{
public Sublevel m_SubLevel;
}
[StructLayout(LayoutKind.Sequential)]
public struct Sublevel
{
IntPtr m_Sublevel_interface;
int m_Sublevel_interfaceNum;
}
[StructLayout(LayoutKind.Sequential)]
public struct Sublevel_interface
{
public int ID;
public IntPtr Name;
public int NameLenth;
public GroundType Type;
Public IntPtr Sublevel_Ground;
}
public enum GroundType
{
Ground1;
Ground2;
}
[StructLayout(LayoutKind.Sequential)]
struct Ground1
{
}
[StructLayout(LayoutKind.Sequential)]
struct Ground2
{
}
//**可以看到,我们把所有指针类的数据类型都用IntPtr类型来做了承接,下面就是相应的数据转换了。仔细观察下面的代码。**
[DllImport("Demo", CharSet = CharSet.Ansi,CallingConvention = CallingConvention.Cdecl),]
public static extern SampleDemo GetDemo();
public void GetDemoData()
{
SampleDemo DemoData = GetDemo();
Sublevel m_SubLevel = DemoData .m_SubLevel;
for (int i = 0; i < m_SubLevel .m_Sublevel_interfaceNum; ++i) //注意此处为什么++i
{
var elementSize = Marshal.SizeOf(typeof(Sublevel_interface));
var elementPtr = new IntPtr(m_SubLevel .m_Sublevel_interface.ToInt64() + i * elementSize);
Sublevel_interface m_interface = Marshal.PtrToStructure<Sublevel_interface>(elementPtr);
switch (m_interface .GroundType )
{
case GroundType .Ground1:
Ground1 M_Ground1= Marshal.PtrToStructure<Ground1>(m_interface.Sublevel_Ground);
break;
case GroundType .Ground2:
Ground2 M_Ground2= Marshal.PtrToStructure<Ground2>(m_interface.Sublevel_Ground);
break;
}
}
}
以上就是今天要讲的内容,本文仅仅简单介绍了C#对接C/C++ 非托管Dll的方法,代码简单而丑陋。在实际项目中,为了优化这部分的代码,也仔细研究了C#的反射机制,同时加入泛型方法,最终代码量还是得到了一定优化。
本次代码中还遗留了一个小的彩蛋,我在“Sublevel_interface” 的结构体中定义了一个IntPtr 类型的变量Name,如果想要直接使用的代码的人,可以直接定义为string来承接。但是更希望同学能够根据本文中的内容,以及上文中提到的Marshal类的具体函数方法的链接,来找到相应的解决方法。