什么是平台调用 (P/Invoke)
- P/Invoke 是可用于从托管代码访问非托管库中的结构、回调和函数的一种技术。
托管代码与非托管的区别
托管代码和非托管代码的主要区别是内存管理方式和对计算机资源的访问方式。托管代码通常运行在托管环境中,如 mono 或 java 虚拟机等,这些环境提供了垃圾回收器(GC)等工具来管理内存。在托管环境中,程序员通常不需要手动分配和释放内存,因为这些任务由运行时系统自动完成。托管代码通常具有更高的安全性和可移植性,因为它们运行在虚拟机中,而不是直接在操作系统上运行。相比之下,非托管代码通常是直接在操作系统上运行的,这意味着程序员需要手动分配和释放内存来避免内存泄漏和其他内存相关的问题。非托管代码可以更直接地访问计算机硬件和操作系统资源,因此它们通常具有更高的性能和更好的控制性。
-
P/Invoke API 包含在以下两个命名空间中:
System
和System.Runtime.InteropServices
。 使用这两个命名空间可提供用于如何与 native 通信的工具。 -
P/Invoke 从功能上来说,只支持函数调用,在被导出的函数前面一定要添加extern "C"来指明导出函数的时候使用C语言方式编译和连接,这样保证函数定义的名字和导出的名字相同,否则如果默认按C++方式导出,那个函数的名字就会变得乱七八糟,我们的程序就无法找到入口点了。
互调的基本原理
首先,我们需要了解 数据类型 的概念
在大多数编译型语言中定义变量的时候都需要先指定其数据类型,所谓数据类型,是语言约定的一个便于记忆的名称(int,long,float),究其本质就是在指明了这个数据在内存中到底是占用了几个字节,程序在运行的时候,首先找到这个数据的地址,然后再按照该类型的长度,读取响应的内存,然后再处理。
基于这个原理,编程语言直接就可以进行互调了,对于不同语言之间的互调,只要将改数据的指针(内存地址)传递给另一个语言,在另一个语言中根据通信协议将指针所指向的数据存储入长度对应的数据类型即可,当然要满足以下条件:
- 对于像 java,C# 这样有运行时虚拟机变成语言来讲,由于虚拟内存会让堆内存来回转移,因此在进行互调的时候,需要保证被调用的数据所在内存一定要固定,不能被转移。
- 有一些编程语言支持指针,有一些语言不支持指针(如Java),这个问题并不重要,所谓指针,其实就是一个内存地址,对于32位OS的指针是一个32位整数,而对于64位机OS的指针是一个64位整数。因为大多数语言中都有整型数,所以可以利用整型来接收指针。
Native 库的加载
举例被加载库的名称为: nativelib.so/nativelib.dll
- 在 windows 平台运行时,将按以下顺序搜寻 dll:
- nativelib
- nativelib.dll
[DllImport("nativelib")]
[DllImport("nativelib.dll")]
static extern int ExportedFunction();
- 在 linux 或 macOS 上运行时,运行时会尝试在
lib
前添加,并追加规范共享库扩展。在这些 OS 上按以下顺序尝试名称变体:- nativelib.so/nativelib.dylib
- libnativelib.so/libnativelib.dylib
- nativelib
- libnativelib
基本数据类型传递
C# 关键字 | .Net类型 | C/C++ |
---|---|---|
byte | System.Byte | uint8_t |
sbyte | System.SByte | int8_t |
short | System.Int16 | int16_t |
ushort | System.UInt16 | uint16_t |
int | System.Int32 | int32_t |
uint | System.UInt32 | uint32_t |
long | System.Int64 | int64_t |
ulong | System.UInt64 | uint64_t |
char | System.Char | char |
nint | System.IntPtr | intptr_t |
bool | System.Boolean | Win32 BOOL 类型 |
decimal | System.Decimal | COM DECIMAL 结构 |
调用流程
基础数据类型调用
#define EXPORTAPI __declspec(dllexport)extern "C" EXPORTAPI int add(int a, int b)
{return a + b;
}
extern "C" EXPORTAPI void writeString(char* content)
{cout << content << endl;
}
- 第一行代码中定义了一个
EXPORTAPI
的宏,对应的内容是__declspec(dllexport)
意思是将后面修饰的内容定义为 Dll 中要导出的内容 ,当然也剋以不使用这个宏,直接将__declspec(dllexport)
写在要导出的函数前。 - 第二行中的
extern "C"
,表示该函数编译和连接时使用 C 语言的方式以保证函数名称不变,由于 C++ 支持函数重载,因此编译器会将函数的参数类型也加到编译后的代码中。例如函数 void fun(int,int),编译后的可能是 _fun_int_int(不同的编译器可能不同,但都采取了类似的机制),extern
是 C/C++ 语言中表明函数和全局变量的作用范围的关键字,这个关键字告诉编译器,其声明的函数和变量可以在本模块或其他模块中使用。
[DllImport("TestCPPDll.dll",EntryPoint = "add")]extern static int add(int a, int b);[DllImport("TestCPPDll.dll")]extern static void writeString(string content);public static void Add(){int result = add(1, 2);Console.WriteLine($"CallLibraryAdd result :{result}");}public static void WriteString(string str){writeString(str);}
DllImport
中第一个参数表示 Dll 文件的位置,第二个参数 EntryPoint
用来指明对应的 C/C++ 中的函数名称是什么, extern
关键字表明该方法是一个外部调用,这个方法声明后,就可以像调用一个普通的静态方法一样去使用了。
指针的传递
extern "C" EXPORTAPI void addInt(int* i)
{*i += 1;cout << *i << endl;
}//传入一个整型数组的指针以及数组长度,遍历每一个元素并且输出
extern "C" EXPORTAPI void sendIntArray(int* firstElement, int arraylength)
{cout << "C# send int array to CPP" << endl;int* currentPointer = firstElement;for (int i = 0; i < arraylength; i++){cout << currentPointer[i];}cout << endl;
}
在第一个方法中参数为一个 int 类型的指针,并将其所指向的内容 +1
第二个方法传入一个整型数组的指针以及数组长度,遍历数组的每一个元素并且输出
#region 指针的传递[DllImport("TestCPPDll.dll")]extern unsafe static void addInt(int* i);[DllImport("TestCPPDll.dll")]extern unsafe static void sendIntArray(int* firstElement, int arraylength);[DllImport("TestCPPDll.dll", EntryPoint = "getArrayFromCPP")]extern unsafe static IntPtr getArrayFromCPPUseInpPtr(IntPtr count);[DllImport("TestCPPDll.dll", EntryPoint = "getArrayFromCPP")]extern unsafe static IntPtr getArrayFromCPPUseRef(ref int count);// 调用 C++ 中的 AddInt 方法public static void AddInt(){int i = 10;unsafe{addInt(&i);}}//调用 C++ 中的 sendIntArray 方法将 C# 中的数组数据传递到 C++ 中,并在 C++ 中输出public static void AddIntArry(){int[] csArry = new int[10];for (int iArr = 0; iArr < 10; iArr++){csArry[iArr] = iArr;}unsafe{fixed (int* pCSArray = &csArry[0]){sendIntArray(pCSArray, 10);}}}//调用C++中的GetArrayFromCPP方法获取一个C++中建立的数组,使用 InpPrt 类型传参,IntPtr 与指针可以互相强转public static void GetArrayFromCPPUseInpPtr(){Console.WriteLine("CPP send int array to C# user IntPtr");int count = 0;GCHandle gch = GCHandle.Alloc(count, GCHandleType.Pinned);IntPtr countInptr = gch.AddrOfPinnedObject();IntPtr pArrayPointer = getArrayFromCPPUseInpPtr(countInptr);count = Marshal.PtrToStructure<int>(countInptr);int[] array = new int[count];for (int i = 0; i < count; i++){IntPtr intPtr = IntPtr.Add(pArrayPointer, i * Marshal.SizeOf<int>());array[i] = Marshal.PtrToStructure<int>(intPtr);Console.Write(array[i]);}Console.WriteLine();}//调用C++中的GetArrayFromCPP方法获取一个C++中建立的数组, 使用 ref 会出现防御性拷贝,造成 GCpublic static void GetArrayFromCPPUseRef(){Console.WriteLine("CPP send int array to C# user ref");int count = 0;IntPtr pArrayPointer = getArrayFromCPPUseRef(ref count);int[] array = new int[count];for (int i = 0; i < count; i++){IntPtr intPtr = IntPtr.Add(pArrayPointer, i * Marshal.SizeOf<int>());array[i] = Marshal.PtrToStructure<int>(intPtr);Console.Write(array[i]);}Console.WriteLine();}#endregion
函数指针的传递
C# 中并没有函数指针的概念,但是可以使用 委托 来代替函数指针
在 C/C++ 运行的某一时刻调用 C# 的对应函数, 这个时候就需要将一个 C# 中已经指向某一个函数的函数指针(委托)传递给 C++
//定义一个函数指针
typedef void (*OnCSCallBack)(int value);OnCSCallBack onCSCallBack;//用于设置函数指针的方法
extern "C" EXPORTAPI void setCallback(OnCSCallBack callback)
{onCSCallBack = callback;cout << "set CPP callback success" << endl;cout << endl;
}//对 C# 中传递过来的委托进行调用
extern "C" EXPORTAPI void testCPPInvoke() {int value = 999;onCSCallBack(value);
}
public delegate void OnCSDelegate(int value);[DllImport("TestCPPDll.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]extern static void setCallback(OnCSDelegate action);[DllImport("TestCPPDll.dll")]extern static void testCPPInvoke();//测试过程中发现 .Net 中 deletegate 可以传递。action<int> 不行。//而在 Unity 中 将 Action 方法添加[MonoPInvokeCallback(typeof(Action<int>))] 标签后可以. MonoPInvokeCallback 标签在 AOT 命名空间下public static void SetCPPCallBack(){OnCSDelegate onCSCallBack = OnCSCallback;setCallback(onCSCallBack);}public static void OnCSCallback(int value){Console.WriteLine($"CPP call CSharp founction value:{value}");}
结构体的传递
传递结构体的想法和传递一个int类型数据类似,struct中的数据是在内存中顺序排列的
- C#中结构体字段类型与C++结构体中的字段类型相兼容
- C#结构中的字段顺序与C++结构体中的字段顺序相同,要保证该功能,需要将C#结构体标记为[StructLayout( LayoutKind.Sequential)]
typedef struct Vector3_
{float x;float y;float z;
} Vector3;// 输出从 C# 侧传入的结构体内容
extern "C" EXPORTAPI void sendStructFormCSToCPP(Vector3 vector3)
{cout << "get vector3 int cpp,x:";cout << vector3.x;cout << ",y:";cout << vector3.y;cout << ",z:";cout << vector3.z << endl;cout << endl;
}//包含固定长度数组的结构体
typedef struct ContainArray_
{Vector3 vectorArray[5];float valueArray[2];
}ContainArray;//不固定数组长度的结构体
typedef struct ContainArrayHasCount_
{int arrayCount;float* floatArray;int vectorArrayCount;Vector3* vectorArray;
}ContainArrayHasCount;//输出从 C# 侧传入数组固定长度的结构体信息
extern "C" EXPORTAPI void sendContainArrayStructFormCSToCPP(ContainArray containArray)
{int vectorArrayCount = sizeof(containArray.vectorArray) / sizeof(*containArray.vectorArray);for (size_t i = 0; i < vectorArrayCount; i++){cout << "vectorArray i: " << i << ",x:" << containArray.vectorArray[i].x << ",y:" << containArray.vectorArray[i].y << ",z:" << containArray.vectorArray[i].z << endl;}int valueArrayCount = sizeof(containArray.valueArray) / sizeof(*containArray.valueArray);for (size_t i = 0; i < valueArrayCount; i++){cout << "valueArray i: " + i << ", value: " << containArray.valueArray[i] << endl;}cout << endl;
}//输出运行时决定数组长度的结构体信息
extern "C" EXPORTAPI void sendContainArrayHasCountStructFormCSToCPP(ContainArrayHasCount containArrayHasCount)
{for (size_t i = 0; i < containArrayHasCount.arrayCount; i++){cout << "containArrayHasCount i: " << i << ",ArrayValue: " << containArrayHasCount.floatArray[i] << endl;}for (size_t i = 0; i < containArrayHasCount.vectorArrayCount; i++){cout << "vectorArray i: " << i << ",x:" << containArrayHasCount.vectorArray[i].x << ",y:" << containArrayHasCount.vectorArray[i].y << ",z:" << containArrayHasCount.vectorArray[i].z << endl;}
}//返回 C++ 侧创建的结构体指针
extern "C" EXPORTAPI ContainArrayHasCount * getArrayStructFormCPP()
{ContainArrayHasCount* containArrayHasCount = new ContainArrayHasCount();containArrayHasCount->arrayCount = 3;containArrayHasCount->floatArray = new float[3];for (size_t i = 0; i < containArrayHasCount->arrayCount; i++){containArrayHasCount->floatArray[i] = i;}containArrayHasCount->vectorArrayCount = 4;containArrayHasCount->vectorArray = new Vector3[4];for (size_t i = 0; i < containArrayHasCount->vectorArrayCount; i++){containArrayHasCount->vectorArray[i].x = i;containArrayHasCount->vectorArray[i].y = i;containArrayHasCount->vectorArray[i].z = i;}return containArrayHasCount;
}
[StructLayout(LayoutKind.Sequential)]public struct Vector3{public Vector3(float x, float y, float z){X = x;Y = y;Z = z;}public float X, Y, Z;}[StructLayout(LayoutKind.Sequential)]public struct ContainArray{[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]public Vector3[] vectorArray;[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]public float[] valueArray;}[StructLayout(LayoutKind.Sequential)]public struct ContainArrayHasCount{public int floatArrayCount;public IntPtr floatArray;public int vectorArrayCount;public IntPtr vectorArray;}[DllImport("TestCPPDll")]extern static void sendStructFormCSToCPP(Vector3 vector3);[DllImport("TestCPPDll")]extern static void sendContainArrayStructFormCSToCPP(ContainArray containArrayStruct);[DllImport("TestCPPDll")]extern static void sendContainArrayHasCountStructFormCSToCPP(ContainArrayHasCount containArrayHasCount);[DllImport("TestCPPDll")]extern static IntPtr getArrayStructFormCPP();// 结构体作为参数传递给 C++ 方法public static void SendStructFormCSToCPP(){Vector3 vector3 = new Vector3();vector3.X = 1;vector3.Y = 2;vector3.Z = 3;sendStructFormCSToCPP(vector3);}// 传入包含指定长度数组的结构体public static void SendContainArrayStructFormCSToCPP(){ContainArray containArrayStruct = new ContainArray();containArrayStruct.vectorArray = new Vector3[5];for (int i = 0; i < 5; i++){containArrayStruct.vectorArray[i].X = i;containArrayStruct.vectorArray[i].Y = i;containArrayStruct.vectorArray[i].Z = i;}containArrayStruct.valueArray = new float[2];containArrayStruct.valueArray[0] = 1.234f;containArrayStruct.valueArray[1] = 5.678f;sendContainArrayStructFormCSToCPP(containArrayStruct);}// 传入运行时决定数组大小的结构体public static void SendContainArrayHasCountStructFormCSToCPP(){ContainArrayHasCount containArrayHasCount = new ContainArrayHasCount();float[] floatArray = new float[] { 1, 22, 333, 4444, 55555 };Vector3[] vectorArray = new Vector3[] { new Vector3(123, 123, 123), new Vector3(456, 456, 456), new Vector3(789, 789, 789) };containArrayHasCount.floatArrayCount = floatArray.Length;containArrayHasCount.vectorArrayCount = vectorArray.Length;GCHandle floatArrayHandle = GCHandle.Alloc(floatArray,GCHandleType.Pinned);containArrayHasCount.floatArray = floatArrayHandle.AddrOfPinnedObject();GCHandle vectorArrayHandle = GCHandle.Alloc(vectorArray,GCHandleType.Pinned);containArrayHasCount.vectorArray = vectorArrayHandle.AddrOfPinnedObject();sendContainArrayHasCountStructFormCSToCPP(containArrayHasCount);floatArrayHandle.Free();vectorArrayHandle.Free();}// 接收从 C++ 返回的结构体public static void GetArrayStructFormCPP(){unsafe{IntPtr structIntPtr = getArrayStructFormCPP();ContainArrayHasCount containArrayHasCount = Marshal.PtrToStructure<ContainArrayHasCount>(structIntPtr);for (int i = 0; i < containArrayHasCount.floatArrayCount; i++){IntPtr floatIntPtr = IntPtr.Add(containArrayHasCount.floatArray,i*Marshal.SizeOf<float>());float value = Marshal.PtrToStructure<float>(floatIntPtr);Console.WriteLine($"floatArray {i}:{value}");}for (int i = 0; i < containArrayHasCount.vectorArrayCount; i++){IntPtr vectorIntPtr = IntPtr.Add(containArrayHasCount.vectorArray,i*Marshal.SizeOf<Vector3>());Vector3 vector = Marshal.PtrToStructure<Vector3>(vectorIntPtr);Console.WriteLine($"vectorArray {i}:{vector.X},{vector.Y},{vector.Z}");}Console.WriteLine($"floatArrayCount: {containArrayHasCount.floatArrayCount}");Console.WriteLine($"vectorArrayCount: {containArrayHasCount.vectorArrayCount}");}}