在 C 回调中在 C++ 中引发异常,可能跨越动态库边界......它安全吗?

Posted

技术标签:

【中文标题】在 C 回调中在 C++ 中引发异常,可能跨越动态库边界......它安全吗?【英文标题】:Throwing an exception in C++ in a C callback, possibly crossing over dynamic library boundary... is it safe? 【发布时间】:2012-06-08 21:08:42 【问题描述】:

我现在使用libjpeg 来保存JPEG 图像。如果有错误,libjpeg 的默认行为是调用exit(),我想避免这种情况,因为这对我的程序来说不是致命错误。 libjpeg allows you to use your own error manager,并规定如果您使用自己的 error_exit() 函数(默认调用 exit()),您必须不要将控制权返回给调用者。 libjpeg 建议使用setjmp.h 来满足此要求,而不是使用exit() 程序。

但是,我正在编写一个 C++ 程序,并且可以访问异常。 This question's answer 声明从回调中抛出异常是安全的(就像在定义明确的行为中一样)。但它没有提到动态库,并且有一个一般的经验法则是不要跨动态库边界抛出异常。

这是一个例子:

#include <iostream>
#include <jpeglib.h>
#include <cstdio>
#include <stdexcept>

static void handleLibJpegFatalError(j_common_ptr cinfo)

  (*cinfo->err->output_message)(cinfo);
  throw std::runtime_error("error in libjpeg, check stderr");


int main()

  struct jpeg_compress_struct cinfo;
  struct jpeg_error_mgr jerr;
  FILE* file = std::fopen("out.jpeg", "wb"); // assume this doesn't fail for this example

  try
    
      cinfo.err = jpeg_std_error(&jerr);
      jerr.error_exit = handleLibJpegFatalError;

      // let's say this triggers a fatal error in libjpeg and handleLibJpegFatalError() is called
      // by libjpeg
      jpeg_create_compress(&cinfo);
    
  catch (...)
    
      std::cerr << "Error saving the JPEG!\n";
    

  jpeg_destroy_compress(&cinfo);
  std::fclose(file);

我想知道的是:即使 libjpeg 被编译为动态库,我是否可以从此回调中抛出异常,并将其捕获到我的应用程序中? libjpeg 可能是静态的或动态库,如果它是动态库,则可能使用不同的编译器构建。但是,抛出和捕获异常的代码肯定会在同一个编译单元中。 上面的代码安全吗?

仅供参考,我正在为 OS X 和 Windows 进行开发(并牢记 Linux 的未来可能性),所以我更感兴趣的是这是否被认为是一般定义明确的行为,而不是特定平台/编译器。

【问题讨论】:

绝对安全。为什么不呢?共享库调用仍然使用相同的调用堆栈。 这可能是相关的:***.com/questions/10318363/… @nw:我想不出为什么它不会被保存;我只是想确保在展开堆栈时没有任何东西会被丢弃。这可能是非常安全的,但我已经被过去的假设所咬伤,所以我在这里玩得安全并仔细检查。 @nw。这是假设 DLL 使用与调用应用程序相同的堆栈管理约定,只有在两者都使用相同的编译器和运行时编译时才能保证。一般来说,跨越 DLL 边界(至少在 Windows 中)是个坏主意。 【参考方案1】:

另一个答案在这里适用。展开堆栈时,没有任何东西会被丢弃。库是否在内部使用了一些疯狂的调用约定甚至都没有关系,只要它没有特别混淆您的 C++ 实现的异常处理结构(它不会作为 C 程序)。我所知道的任何 C++ 实现都没有通过弹出堆栈帧来找到 catch 块(这将使优化成为一场噩梦),它们都维护用于异常处理的内部结构。只要调用链中较低的调用不会与这些结构混淆,堆栈展开就可以完美地适用于您的所有个人代码。现在,一般来说,这很可能会使库内部状态混乱,因为您永远不会将执行返回到库进行清理,但是在错误回调的情况下,libjpeg 期望控制流不会返回并且有大概已经清理干净了。

在这种情况下,我会去的。一般来说,我只会从 C 回调中抛出致命异常。

希望有所帮助。

【讨论】:

据我所知,异常处理要么需要一种定位堆栈上所有内容的方法,要么能够定位一个对线程上运行的所有代码都是全局的对象,但不会与其他线程冲突。后一种方法在可用时可能会更好,但对于对底层系统的线程机制一无所知的独立系统会出现问题。【参考方案2】:

这不安全。根据相关非 C++ 库代码的编译方式,可能不存在必要的展开表。这只是它可能失败的实际原因;概念上的原因是它只是未定义的行为。

您应该遵循文档并使用setjmp/longjmp 来获取对 libjpeg 代码的调用之外,然后如果您想使用异常,请立即在 if (setjmp(...)) ... 正文中抛出异常。

【讨论】:

您是在谈论展开库调用链中的框架吗? setjmp 也不会解除这些问题。 C 代码必须专门覆盖 SEH 框架/.eh_frame/etc 以扰乱 C++ 调用的展开,除非有人故意试图搞砸,否则不会发生这种情况。 在回调中抛出异常是不安全的,除非它也会在回调中处理。至于它在哪里/如何失败,如果 C 库代码根本没有展开信息(没有 .eh_frame)并且不使用帧指针,那么绝对没有办法回溯 C 代码之外并理解应该处理异常的较早的调用框架。【参考方案3】:

正如其他答案中所讨论的,只要您控制所有模块并且它们将由相同的工具链(包括静态链接)构建,它就应该是安全的。

但是我想在这里添加一个警告 some toolchains require this support to be turned on,因为 libjpeg 的函数标记为 extern "C"。默认情况下,Visual Studio 假定此类函数不会传播异常。

如果您不打开此功能,预计会很痛苦。在我意识到这一点之前,我花了几个小时在一个几乎与你相同的测试用例上。 ?

【讨论】:

以上是关于在 C 回调中在 C++ 中引发异常,可能跨越动态库边界......它安全吗?的主要内容,如果未能解决你的问题,请参考以下文章

跨 C API 边界传递异常

两例典型的C++软件异常排查实例分享

使用不同的C++支持库的模块混合开发时,引发异常展开不正常,抛异常竟引出一个SIGSEGV

使用 P/Invoke 在托管和非托管回调链上引发异常

访问动态 2D 字符数组时引发访问冲突异常

引发C++软件异常的常见原因分析