关于P/Invoke和AccessViolationException

 

最近试验了一阵子.NET Framework中使用P/Invoke与Windows API进行互操作的方法,其间在封装标有CALLBACK标记的回调函数时遇到了随时间推移而出现的AccessViolationException异常。这个异常在.NET Framework 1.x中由更为一般的NullReferenceException描述,在2.0版中才分离出来,成为一类特殊的异常。由于它属于系统级别的异常,所以在程序中无法使用try...catch...块捕获到,加上其在DEBUG模式下销声匿迹、在RELEASE模式下随时间推移而产生,着实让我费了些脑细胞。查了些资料,得知这个异常是因为托管代码中某些用于封送的数据对象被CLR的垃圾收集器回收了,这导致了非托管代码在访问这些对象时出现了非法访问,继而CLR封装异常对象并将其抛出。我看到有的朋友在封送C++中char类型数组/指针或结构体指针是遇到了这个异常,而我则是在封送C++函数指针时遇到的,由此看来,导致这个异常的原因都与封送指针有关。

举一个例子:
WINAPI中有一个处理MIDI设备的函数:
MMRESULT midiInOpen(
    ...        
   DWORD_PTR dwCallback,           // 需要一个回调函数,DWORD_PTR可以理解为一个函数指针
    ...
);

而其所需回调函数为:
void CALLBACK MidiInProc(...);    // CALLBACK是一个宏,代表__stdcall调用约定

这两个函数的关系是,当应用程序调用midiInOpen函数将MidiInProc函数的指针传入后,每当MIDI设备有数据到达,Windows将根据传入(注册)的指针,自动调用MidiInProc函数处理相关消息(这也是CALLBACK回调的含义)。

对这两个WINAPI的互操作代码可以按如下定义:

[DllImport("Winmm.dll", SetLastError = true)]
private extern static UInt32 midiInOpen(
...
[MarshalAs(UnmanagedType.FunctionPtr)] // 按非托管类型的函数指针封送
MidiInProc dwCallback, // 封送函数指针需要事先定义一个委托
...
);

对应的委托定义为:
[UnmanagedFunctionPointer(CallingConvention.StdCall)] // 指定调用规定,当然也可以不指定,因为StdCall是默认值
public delegate void MidiInProc(...);

将他们两个都定义好之后,就可以在应用程序中使用了,使用的代码可以像这样:
midiInOpen(..., new MidiInProc(this.midiInProc), ...);

其他的参数可以不用关心,重点在那个委托身上。

由于回调函数在系统收到MIDI数据的时候就会自动被调用,也就是非托管代码在系统收到MIDI消息时会自动调用由委托对象传递的函数指针所指的代码块,而且这个过程在关闭MIDI设备前是不会终止的。然而,这里却有另外一件事,由于当初传递给midiInOpen方法的委托对象是直接使用new运算符产生的,所以它在应用程序中并没有被引用,在程序运行一段时间后,它就会被GC回收。一边是非托管代码几乎无休止地调用由委托对象传递的函数,一边是传递回调函数的委托对象被GC回收,最终矛盾的结果就是非托管代码访问了被回收的地址空间,导致了非法内存访问,因此CLR抛出了AccessViolationException异常。

其实解决的方法很简单,那就是在封装P/Invoke操作的类中保留一个封送回调函数指针的委托对象引用就可以了,比如:
class Test
{
private MidiInProc proc = new MidiInProc(this.midiInProc);

public void midiInProc(...){...}

[DllImport("Winmm.dll", SetLastError = true)]
private extern static UInt32 midiInOpen(
...
[MarshalAs(UnmanagedType.FunctionPtr)] // 按非托管类型的函数指针封送
MidiInProc dwCallback, // 封送函数指针需要事先定义一个委托
...
);

public void TestIt()
{
   // midiInOpen(..., new MidiInProc(this.midiInProc), ...);    这样会引发访问冲突异常
    midiInOpen(...,proc,...);    // 这样则不会
}
}

由这个试验可以看出,在进行互操作的时候,要时常留意GC。除了用这种方式来保持对象不被垃圾回收外,还有一种方式可以做到类似的功能:

GCHandle reportingFunctionPinHandle = GCHandle.Alloc(reportingFunction, GCHandleType.Pinned);

这种方法是将某个对象在分配在内存中,并且钉住它(pin),这样,其他的对象被回收或是内存重排,都不会影响到这个对象的地址,就像是“钉”在内存中一样。不过最近的实践没用到这个功能,具体的还有待探索。


http://hi.baidu.com/fancyaj/blog/item/2f59f41f7f340861f624e43b.html
 

In .NET 4.0, the runtime handles certain exceptions raised as Windows Structured Error Handling (SEH) errors as indicators of Corrupted State. These Corrupted State Exceptions (CSE) are not allowed to be caught by your standard managed code. I won't get into the why's or how's here. Read this article about CSE's in the .NET 4.0 Framework:

http://msdn.microsoft.com/en-us/magazine/dd419661.aspx#id0070035

But there is hope. There are a few ways to get around this:

  1. Recompile as a .NET 3.5 assembly and run it in .NET 4.0.
  2. Add a line to your application's config file under the configuration/runtime element: <legacyCorruptedStateExceptionsPolicy enabled="true|false"/>
  3. Decorate the methods you want to catch these exceptions in with the HandleProcessCorruptedStateExceptions attribute. See http://msdn.microsoft.com/en-us/magazine/dd419661.aspx#id0070035 for details.

For more reference: http://connect.microsoft.com/VisualStudio/feedback/details/557105/unable-to-catch-accessviolationexception

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值