9

我们有大量的本地 C++ 代码,编译成 DLL。

然后我们有几个包含 C++/CLI 代理代码的 dll 来包装 C++ 接口。

最重要的是,我们将 C# 代码调用到 C++/CLI 包装器中。

标准的东西,到目前为止。

但是我们有很多情况允许本地 C++ 异常传播到 .Net 世界,我们依靠 .Net 将它们包装为 System.Exception 对象的能力,并且在大多数情况下这工作正常。

然而,我们发现当异常传播时,抛出点范围内的对象的析构函数没有被调用!

经过一些研究,我们发现这是一个众所周知的问题。然而,解决方案/解决方法似乎不太一致。我们确实发现,如果使用 /EHa 而不是 /EHsc 编译本机代码,问题就会消失(至少在我们的测试用例中是这样)。然而,我们更喜欢使用 /EHsc,因为我们自己将 SEH 异常转换为 C++ 异常,并且我们宁愿让编译器有更多的优化空间。

这个问题还有其他解决方法吗 - 除了将每个调用都包装在(本机)try-catch-throw 中(除了 C++/CLI 层)之外?

4

4 回答 4

3

/E 编译器开关的 MSDN 页面确实说明了这种行为:-

http://msdn.microsoft.com/en-us/library/1deeycx5(VS.80).aspx

这是相关的报价:-

如果您使用 /EHs,那么您的 catch 子句将不会捕获异步异常。此外,在 Visual C++ 2005 中,即使处理了异步异常,也不会销毁生成异步异常时范围内的所有对象。

基本上 /EHsc 是乐观的观点 - 它假设唯一的例外是真正的 C++ 风格的例外,并将相应地进行优化。另一方面,/EHa 持悲观态度,并假设任何代码行都可能导致生成异常。

如果您可以保证永远不会导致访问冲突、页内错误或其他 SEH,请使用 /EHsc。但是,如果您正在编写服务和/或想要提供“尽力而为”,那么 /EHa 将是必要的。

我也同意@JaredPar 关于不允许异常跨越模块边界的观点。@nobugz 关于 CLR 处理异常方式的说法可能是正确的,但我认为 .Net 代码使用 P/Invoke 直接调用本机代码和调用 C++/CLI 互操作 DLL 之间存在差异。在前一种情况下,CLR 必须代表您处理情况,而在后一种情况下,您可以控制并进行相应的翻译。

于 2010-03-23T21:51:11.080 回答
3

不幸的是,我不相信有任何好的解决方法。MS 平台上的 C++ 异常实现是使用 SEH 异常 (IIRC) 实现的。CLR 挂钩到 SEH 处理以捕获本机异常并将它们处理为 CLR 异常。由于它在 SEH 级别捕获它们,因此异常看起来像 C++ 的 SEH 异常,并且析构函数会相应地运行或不运行。

因此,正如您所注意到的,最好的两个选项是

  • 用 /EHa 编译
  • 在函数的入口和出口处添加 try/catch

理想情况下,无论如何你都应该做第二个。根据我的经验,允许 C++ 异常跨越组件边界被认为是不好的做法。

您还可以使用_set_seh_translator文档)实现一个 hacky 解决方案。但是,我强烈建议避免使用该功能,因为它可能会无意中破坏 CLR 异常处理并导致许多不必要的问题。

于 2010-03-23T18:35:48.890 回答
3

我认为你做的不对。使用 _set_se_translator() 已经要求您使用 /EHa 进行编译。从MSDN 库页面

You must use /EHa when using _set_se_translator.

更严重的是,您在使用托管代码时会破坏它。CLR 依靠 SEH 异常来检测各种事故。它使用 SetUnhandledExceptionFilter 来捕获它们。特别是 NullReferenceException 和 OverflowException (x86) 是以这种方式引发的。当您注入自己的 __try 块时,您将阻止此异常流入 CLR。尽管您将“处理”它,但您将无法找到异常的确切原因。并且托管的 try/catch 块无法检测到它。

/EHa 效率的解决方法(如果它确实是一个问题)是 64 位代码。它的基于函数表的堆栈展开非常有效,try 块的开销为零。

于 2010-03-23T19:38:17.020 回答
1

2.0 版本的 CLR 中存在导致此问题的错误。在 4.0 CLR 上运行托管可执行文件将允许按预期调用析构函数。

有关详细信息,请参阅抛出异常后未释放 Boost 共享互斥锁

于 2011-10-19T20:00:04.813 回答