我将 C# 实例方法传递给 Win32 API 调用,该调用稍后将用作从 Windows 到我的应用程序的回调函数。当我传递对对象的引用时,该引用会暂时固定,直到调用返回(请参阅 Jason Clark 的这篇文章)。
如果 API 调用将保留地址以供调用返回后稍后使用,我必须在调用之前显式固定对象(我可以通过 Marshal.AllocHGlobal 从非托管内存中分配它,或者我可以固定托管对象通过 GCHandle.Alloc).
但是 Win32 API 保留用作回调的方法又如何呢?具体来说,我有这个代码:
protected const int CALLBACK_FUNCTION = 0x30000;
private delegate void MidiInProc(
int handle,
uint msg,
int instance,
int param1,
int param2);
[DllImport("winmm.dll")]
private static extern int midiInOpen(
out int handle,
int deviceID,
MidiInProc proc,
int instance,
int flags);
private void MidiInProcess(
int hMidiIn,
uint uMsg,
int dwInstance,
int dwParam1,
int dwParam2)
{
}
...
int hResult = midiInOpen(
out hHandle,
deviceID,
MidiInProcess, // Might this move after the call returns?
0,
CALLBACK_FUNCTION);
在存档教程中,微软表示:“……确保委托实例的生命周期覆盖非托管代码的生命周期;否则,委托在被垃圾收集后将不可用。”这是完全有道理的,因为如果没有托管代码引用该类,则该类可能会被卸载,从而导致该方法(“委托”)不再位于内存中。
但是由于堆上分配了对象,因此方法重定位的可能性又如何呢?该方法的地址在其生命周期内可能会改变吗?
换句话说:如果定义
MidiInProcess
的类保持加载状态,我能否确定上面的 MidiInProcess
方法在 midiInOpen
返回后不会更改地址,或者我必须采取一些步骤来固定是吗?
更新
根据 Hans 的第一条评论,传递给上面的
midiInOpen
的委托是短暂的,并且不能保证稍后调用时可用(因为在托管代码端没有对其的持久引用)。我相信在封闭实例的私有成员中保留对它的引用应该足以使其保持活动状态,前提是只要可能需要回调,将对封闭实例本身的引用就保留在应用程序中的其他位置。虽然不完整,但看起来可能是这样的:
private MidiInProc midiInProc;
...
midiInProc = MidiInProcess;
int hResult = midiInOpen(
out hHandle,
deviceID,
midiInProc, // Pass the reference you retained.
0,
CALLBACK_FUNCTION);
来自 MSDN 文章如何:使用 C++ 互操作来编组回调和委托:
请注意,是否可以(但不是必须)使用 pin_ptr (C++/CLI) 固定委托,以防止它被垃圾收集器重新定位或处置。需要防止过早的垃圾收集,但是固定提供了比必要的更多的保护,因为它可以防止收集,但也可以防止重新定位。
如果通过垃圾回收重新定位委托,则不会影响底层托管回调,因此使用 Alloc 添加对委托的引用,允许重新定位委托,但阻止处置。使用 GCHandle 而不是 pin_ptr 可以减少托管堆的碎片可能性。
重点是我的。
当然,它与使用 P/Invoke 相关,而不仅仅是使用 C++ IJW 互操作,正如 Hand 在评论中所说和 Chris Brumme 在博客文章中所说 我在评论中链接,但我认为这是最好的文档.
如果您足够关心,您可以通过文档提交错误。它现在托管在 GitHub 上,因此可能比以前更容易。