有一些事情你需要知道。首先,如果你从非托管代码中调用.NET委托,除非你遵循一些相当狭窄的限制,否则你将会遭受痛苦。
理想情况下,你可以在C#中创建一个委托,将其传递到托管代码中,将其封送到函数指针中,随意持有它,然后调用它而不会产生任何不良影响。.NET文档是这样说的。
我可以告诉你,这简直就是不真实的。最终,你的委托或其thunk的一部分将被垃圾回收,当你从非托管代码中调用函数指针时,你将被送入无尽虚无之中。我不在乎微软说什么,我已经严格按照他们的处方执行过了,但看到函数指针变成垃圾,尤其是在服务器端代码背后。
鉴于此,使用函数指针最有效的方法如下:
- C#代码调用非托管代码,传递委托。
- 非托管代码将委托封送到函数指针中。
- 非托管代码进行一些工作,可能调用函数指针。
- 非托管代码放弃所有对函数指针的引用。
- 非托管代码返回托管代码。
鉴于此,假设我们在C#中有以下内容:
public void PerformTrick(MyManagedDelegate delegate)
{
APIGlue.CallIntoUnamangedCode(delegate);
}
然后在托管 C++ 中(不是 C++/CLI):
static CallIntoUnmanagedCode(MyManagedDelegate *delegate)
{
MyManagedDelegate __pin *pinnedDelegate = delegate;
SOME_CALLBACK_PTR p = Marshal::GetFunctionPointerForDelegate(pinnedDelegate);
CallDeepIntoUnmanagedCode(p);
}
我最近没有在C++/CLI中做过这个 - 语法不同 - 我想它最终会看起来像这样:
static CallIntoUnamangedCode(MyManagedDelegate ^delegate)
{
pin_ptr<MyManagedDelegate ^> pinnedDelegate = &delegate;
SOME_CALLBACK_PTR p = Marshal::GetFunctionPointerForDelegate(pinnedDelegate);
CallDeepIntoUnmanagedCode(p);
}
当您退出这些例程时,固定会被释放。
当您真正需要在调用之前挂起函数指针一段时间时,我在C++/CLI中执行了以下操作:
- 创建一个哈希表,其中包含从int到委托的映射。
- 创建注册/注销例程,将新委托添加到哈希表中,并增加哈希int的计数器。
- 创建一个单个静态非托管回调例程,使用来自注册调用的int在未管理的代码中进行注册。当调用此例程时,它会回调到托管代码,说“查找与关联的委托,并在这些参数上调用它”。
发生的情况是,委托不再具有执行转换的thunk,因为它们是隐含的。它们可以自由地徘徊在空中,并根据需要由
GC移动。当它们被调用时,委托将被
CLR固定并根据需要释放。我也见过这种方法失败的情况,特别是在静态注册回调函数并期望它们一直存在到时间结束的代码中。我在ASP.NET代码后面以及通过
WCF工作的
Silverlight服务器端代码中看到了这种情况。这相当令人不安,但解决方法是重构您的API以允许更晚绑定到函数调用。
举个例子,当发生这种情况时,假设您有一个包括以下函数的库:
typedef void * (*f_AllocPtr) (size_t nBytes);
typedef void *t_AllocCookie;
extern void RegisterAllocFunction(f_AllocPtr allocPtr, t_AllocCookie cookie);
预期情况是,当您调用分配内存的API时,它将被定向到提供的f_AllocPtr
。信不信由你,您可以使用C#编写此代码。很棒:
public IntPtr ManagedAllocMemory(long nBytes)
{
byte[] data = new byte[nBytes];
GCHandle dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
unsafe {
fixed (byte *b = &data[0]) {
dataPtr = new IntPtr(b);
RegisterPointerHandleAndArray(dataPtr, dataHandle, data);
return dataPtr;
}
}
}
RegisterPointerHandleAndArray将三元组存储起来以供安全保管。这样,当相应的free被调用时,您可以执行以下操作:
public void ManagedFreeMemory(IntPtr dataPointer)
{
GCHandle dataHandle;
byte[] data;
if (TryUnregister(dataPointer, out dataHandle, out data)) {
dataHandle.Free();
}
}
当然,这是愚蠢的,因为分配的内存现在被固定在GC堆中,并且会使其碎片化 - 但重点是它是可行的。
但同样地,我个人曾经见过这种方法失败,除非实际指针的生命周期很短。通常意味着包装您的API,以便在调用完成特定任务的例程时,注册回调函数,执行任务,然后拉出回调函数。