在 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++ 中引发异常,可能跨越动态库边界......它安全吗?的主要内容,如果未能解决你的问题,请参考以下文章