[DllImport("c++库名", CallingConvention = CallingConvention.StdCall)]
private static extern void method_exsample([MarshalAs(UnmanagedType.LPStr)] string url, [MarshalAs(UnmanagedType.LPStr)] string str, IntPtr context, IntPtr bytes, int length);
extern关键字
修饰符用于声明在外部实现的方法。extern 修饰符的常见用法是在使用 Interop 服务调入非托管代码时与 DllImport 属性一起使用;在这种情况下,该方法还必须声明为 static
DllImport特性
[DllImport(“c++库名”, CallingConvention = CallingConvention.StdCall)]
CallingConvention 调用约定,决定了参数入栈的顺序,从左往右还是从右往左,出栈方式之类的。根据C++导出的设置来处理
MarshalAs特性
[MarshalAs(UnmanagedType.LPStr)]
在C++中 字符串或者某类型数组是char* 或者某类型的指针
这个特性可以帮忙将指针转为C#类型 C++ char*->C# string
如果不用这个特性,参数类型就写IntPtr,用到的时候Marshal.PtrToStringAnsi(ptr)可以转string,也是差不多的效果。
C#对象属于托管堆,有垃圾回收机制管理内存。C++的内存是非托管的,当C++使用C#对象时,有可能这个对象被GC回收了。为了避免在C++使用期间被回收,需要使用GCHandle将它“钉住”,让GC暂时不要回收。用完也要记得解除这个钉子。
GCHandle gcHandler = GCHandle.Alloc(C#对象, GCHandleType.Pinned);
gcHandler.Free();//C++结束使用后,释放它
除了基础数据类型,其他C#类型对象传递到C++,我们都以IntPtr的形式,这两种方式都可以获得IntPtr。
GCHandler.ToIntPtr(gcHandler);
gcHandler.AddrOfPinnedObject();
C++有函数指针,对C#来说就是委托。
我们要将C#方法注册到C++,就定义一个跟C++函数指针相同的委托
C++注册C#函数的方法
[DllImport("c++库名", CallingConvention = CallingConvention.StdCall)]
private static extern void cpp_callback_register(csharp_delegate cb, Intptr context);
C#定义委托
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate int csharp_delegate (IntPtr cpp_ptr, int size, IntPtr context);
C#被回调的方法
标记MonoPInvokeCallback特性,这个是Unity用到的
[AOT.MonoPInvokeCallback(typeof(csharp_delegate))]
static void CSharp_Callback_Method(IntPtr cpp_ptr,int size, IntPtr context)
{
if(cpp_ptr != Intptr.Zero)//如果C++指针不为空
{
//...
}
}
如果将C#对象作为C++回调的上下文,我们将C#对象转为IntPtr传入C++,回调的时候也是IntPtr
GCHandle.FromIntPtr(ptr).Target as C#对象
还有一些用法,根据具体使用的时候查阅文档就可了。
Marshal.Copy(IntPtr.Add(ptr, recvSize), bytes, 0, copySize);
–2022.7.5
最近又开始写一点C++的东西,在写到函数指针传递上下文的时候又一次踩进同一个坑,一定要记录下来
我在C++回调到C#的方法中,会传递一个上下文是ojbect类型。我用AddrOfPinnedObject获取IntPtr,在回调时用GCHandle.FromIntPtr,却在一开始AddrOfPinnedObject就报错了。
具体原因可以看这个文章
里面提到这两者都可以获得IntPtr,但是使用上有些不同。
AddrOfPinnedObject,要求类型有严格的布局。
如果你使用GCHandle.ToIntPtr,传入的GCHandler在Alloc的时候,使用了GCHandleType.Pinned,也会报错的。
object t = new object();
GCHandle handler = GCHandle.Alloc(t, GCHandleType.Pinned);
var ptr = GCHandle.ToIntPtr(handler);
上面这种写法,ptr无论是用ToIntPtr还是AddrOfPinnedObject,都会报错。
报错内容是:System.ArgumentException:“Object contains non-primitive or non-blittable data. ”
就是说,非基础类型或者非blittable类型,就无法使用Pinned的方式了。
什么是blittable
通过上面的文章,可以发现object,class都不是blittable类型。
所以修改一下,Alloc的时候不要时候用GCHandleType.Pinned,然后直接ToIntPtr
object t = new object();
GCHandle handler = GCHandle.Alloc(t);
var ptr = GCHandle.ToIntPtr(handler);
microsoft文档中也提到:The only thing you can do with this IntPtr is pass it back to GCHandle.FromIntPtr to get back the GC handle.
这种方式获取的IntPtr,后续能做的就只有通过FromIntPtr再取出GCHandle。
显然这很符合我处理上下文的需求。
这两个文章会更好理解GCHandleType的作用
GCHandle: when to use GCHandleType.Normal explicitly
GCHandle:什么时候显式使用GCHandleType.Normal?上面的问题的中文翻译