当本机 (C++) 异常传播到 CLR 组件时,不会调用析构函数
我们有大量的本机 C++ 代码,已编译成 DLL。
然后我们有几个包含 C++/CLI 代理代码的 dll 来包装 C++ 接口。
最重要的是,我们有 C# 代码调用 C++/CLI 包装器。
标准的东西,到目前为止。
但在很多情况下,本机 C++ 异常被允许传播到 .Net 世界,并且我们依赖 .Net 将这些异常包装为 System.Exception 对象的能力,并且在大多数情况下这都可以正常工作。
然而,我们发现,当异常传播时,抛出点范围内的对象的析构函数不会被调用!
经过一些研究,我们发现这是一个众所周知的问题。然而,解决方案/解决方法似乎不太一致。我们确实发现,如果使用 /EHa 而不是 /EHsc 编译本机代码,问题就会消失(至少在我们的测试用例中是这样)。然而,我们更喜欢使用 /EHsc,因为我们自己将 SEH 异常转换为 C++ 异常,并且我们宁愿让编译器有更多的优化空间。
对于此问题,是否还有其他解决方法 - 除了将跨本机管理边界的每个调用包装在(本机)try-catch-throw 中(除了 C++/CLI 层之外)?
We have a large body of native C++ code, compliled into DLLs.
Then we have a couple of dlls containing C++/CLI proxy code to wrap the C++ interfaces.
On top of that we have C# code calling into the C++/CLI wrappers.
Standard stuff, so far.
But we have a lot of cases where native C++ exceptions are allowed to propagate to the .Net world and we rely on .Net's ability to wrap these as System.Exception objects and for the most part this works fine.
However we have been finding that destructors of objects in scope at the point of the throw are not being invoked when the exception propagates!
After some research we found that this is a fairly well known issue. However the solutions/ workarounds seem less consistent. We did find that if the native code is compiled with /EHa instead of /EHsc the issue disappears (at least in our test case it did). However we would much prefer to use /EHsc as we translate SEH exceptions to C++ exceptions ourselves and we would rather allow the compiler more scope for optimisation.
Are there any other workarounds for this issue - other than wrapping every call across the native-managed boundary in a (native) try-catch-throw (in addition to the C++/CLI layer)?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
不幸的是,我不相信有任何好的解决方法。 MS 平台上的 C++ 异常实现是使用 SEH 异常 (IIRC) 来实现的。 CLR 挂钩 SEH 处理以捕获本机异常并将其处理为 CLR 异常。由于它在 SEH 级别捕获它们,因此异常看起来像 C++ 的 SEH 异常,并且析构函数相应地运行或不运行。
因此,正如您所指出的,最好的两个选项是
理想情况下,无论如何您都应该执行第二个选项。根据我的经验,允许 C++ 异常跨越组件边界被认为是不好的做法。
您还可以使用
_set_seh_translator
(文档)。不过,我强烈建议避免使用该函数,因为它可能会无意中破坏 CLR 异常处理并导致许多不必要的问题。Unfortunately no I do not believe there are any good workarounds. The C++ exception implementation on MS platforms are implemented using SEH exceptions (IIRC). The CLR hooks into SEH handling to catch native exceptions and process them into CLR exceptions. Since it catches them at an SEH level the exception looks like an SEH exception to C++ and destructors are run or not run accordingly.
So as you've noted the best two options are
Ideally you should be doing the second one anyways. In my experience it's considered bad practice to allow C++ exceptions to cross component boundaries.
There is also likely a hacky solution you could achieve using
_set_seh_translator
(Documentation). However I highly recommend avoiding that function as it can inadventently subvert CLR exception handling and cause a lot of unwanted problems.我认为你做得不对。使用 _set_se_translator() 已经要求您使用 /EHa 进行编译。从 MSDN 库页面:
更严重的是,您在使用托管代码时会破坏它。 CLR 依赖 SEH 异常来检测各种事故。它使用 SetUnhandledExceptionFilter 来捕获它们。特别是 NullReferenceException 和 OverflowException (x86) 是通过这种方式引发的。当您注入自己的 __try 块时,您将阻止此异常流入 CLR。尽管您将“处理”它,但您不会知道异常的确切原因。并且托管的 try/catch 块无法检测到它。
解决 /EHa 效率的方法(如果它确实是一个问题)是 64 位代码。其基于函数表的堆栈展开非常高效,try 块的开销为零。
You are not doing this right I think. Using _set_se_translator() already requires you to compile with /EHa. From the MSDN Library page:
More seriously, you'll break managed code when you use it. The CLR relies on SEH exceptions to detect various mishaps. It uses SetUnhandledExceptionFilter to trap them. Particularly NullReferenceException and OverflowException (x86) are raised this way. When you inject your own __try block, you'll prevent this exception from flowing into the CLR. Although you'll "handle" it, you won't have any trace of the exact reason for the exception. And managed try/catch blocks can't detect it.
A cure for /EHa efficiency (if it is actually a problem) is 64-bit code. Its function table based stack unwinding is very efficient with zero overhead for a try block.
/E 编译器开关的 MSDN 页面确实说明了此行为:-
http://msdn.microsoft.com/en-us/library/1deeycx5(VS.80).aspx
以下是相关引用:-
基本上 /EHsc 是乐观的观点 - 它假设唯一的例外是真正的 C++ 风格的例外,并将相应地进行优化。另一方面,/EHa 采取悲观观点,并假设任何代码行都可能导致生成异常。
如果您可以保证永远不会导致访问冲突、页内错误或其他 SEH,那么请使用 /EHsc。但是,如果您正在编写服务和/或想要提供“尽力而为”,那么 /EHa 将是必要的。
我也同意 @JaredPar 关于不允许异常跨越模块边界的观点。 @nobugz 关于 CLR 处理异常的方式所说的可能是正确的,但我认为 .Net 代码使用 P/Invoke 直接调用本机代码与调用 C++/CLI 互操作 DLL 之间存在差异。在前一种情况下,CLR 必须代表您处理这种情况,而在后一种情况下,您可以控制并可以进行相应的转换。
The MSDN page for the /E compiler switch does state this behaviour:-
http://msdn.microsoft.com/en-us/library/1deeycx5(VS.80).aspx
Here is the relevant quote:-
Basically /EHsc is the optimistic view - it assumes that the only exceptions are true C++ style ones and will optimise accordingly. /EHa on the other hand takes the pessimistic view and assumes that any line of code could cause an exception to be generated.
If you can guarentee that you'll never cause an Access Violation, or In-Page Error or other SEH then use /EHsc. But If you're writing a service and/or want to provide a "best effort" then /EHa is going to be necessary.
I also agree with @JaredPar's sentiments about not allowing exceptions to cross module boundaries. What @nobugz says about the way the CLR handles exceptions may be true, but I think there is a difference between .Net code calling out directly to native code using P/Invoke and calling into a C++/CLI interop DLL. In the former case the CLR has to handle the situation on your behalf, whereas in the latter you are in control and can translate accordingly.
2.0 版本的 CLR 中存在导致此问题的错误。在 4.0 CLR 上运行托管可执行文件将允许按预期调用析构函数。
有关详细信息,请参阅 Boost 共享互斥体在抛出异常后未释放。
There is a bug in the 2.0 version of the CLR causing this issue. Running your managed executable on the 4.0 CLR will allow the destructors to be called as expected.
See Boost shared mutex not released after exception thrown for details.