跨 C API 边界传递异常

Posted

技术标签:

【中文标题】跨 C API 边界传递异常【英文标题】:Passing exceptions across a C API boundary 【发布时间】:2012-03-04 08:38:37 【问题描述】:

我正在用 C++ 编写一个使用旧 C API 的库。我的库的客户端可以指定回调函数,这些回调函数是通过我的库间接调用的,而我的库是通过 C API 调用的。这意味着必须处理客户端回调中的所有异常。

我的问题是:如何在边界的一侧捕获异常并在重新跨越 C API 边界并且执行回到 C++ 领域后重新抛出异常,以便客户端可以处理异常代码?

【问题讨论】:

【参考方案1】:

对于 C++11,我们可以使用:

std::exception_ptr active_exception;

try

    // call code which may throw exceptions

catch (...)

    // an exception is thrown. save it for future re-throwing.
    active_exception = std::current_exception();


// call C code
...

// back to C++, re-throw the exception if needed.
if (active_exception)
    std::rethrow_exception(active_exception);

在 C++11 之前,这些仍然可以通过 Boost Exception 使用。

【讨论】:

如果异常是按值抛出的,exception_ptr会保持存活吗? @SethCarnegie:按照标准,是的。 (第 18.8.5/8 节“至少只要有一个引用它的 exception_ptr 对象,引用的对象就应该保持有效。”)Boost 实现应该做同样的事情。 这太棒了,看起来委员会在设计 C++11 时就考虑到了我。也是因为VS2010实现了exception_ptrcurrent_exceptionrethrow_exception,我才能接受这个答案。谢谢。【参考方案2】:

一些环境或多或少直接支持这一点。

例如,如果您通过/EH 编译器开关启用structured exception handling 和C++ 异常,则可以通过Microsoft 的结构化异常处理(C 的“异常”)实现C++ 异常。如果在编译所有代码(两端的 C++ 和中间的 C)时设置了这些选项,堆栈展开将“起作用”。

但是,这几乎总是一个坏主意 (TM)。 你问为什么?考虑中间的那段C代码是:

WaitForSingleObject(mutex, ...);
invoke_cxx_callback(...);
ReleaseMutex(mutex);

invoke_cxx_callback() (..drum roll...) 调用您的 C++ 代码,该代码会引发异常。您将泄漏一个互斥锁。哎哟。

您知道,大多数 C 代码并不是为了在函数执行的任何时刻处理 C++ 风格的堆栈展开而编写的。此外,它缺少析构函数,因此它没有RAII 来保护自己免受异常影响。

Kenny TM 为基于 C++11 和 Boost 的项目提供解决方案。 xxbbcc 对于一般情况有一个更通用但更繁琐的解决方案。

【讨论】:

这与 Kenny TM 的方式无关,因为 invoke_cxx_callback 将正常返回(已将异常存储在 exception_ptr 中)并且当 C 代码返回到 C++ 代码时,C++ 代码将检查是否抛出异常,如果是则重新抛出它。 @SethCarnegie:我知道,这就是为什么我提到“解决方案”的其他答案。这个答案只解释了为什么没有通过 C 代码“通过”异常。 “您可以通过 Microsoft 的结构化异常处理实现 C++ 异常” - 这是不正确的。微软的编译器一直都是根据 SEH 异常来实现 C++ 异常的。那里别无选择。编译器开关仅控制 MSC 实现的异常处理关键字的语义。 @IInspectable 我不确定这个答案的微妙之处是否重要,但请随时更新帖子以改进它:-)【参考方案3】:

您可能可以通过 C 接口传递一个结构,该结构在发生异常时填充错误信息,然后在客户端收到该信息时,根据来自的数据检查它并在客户端内部抛出异常结构。如果您只需要最少的信息来重新创建异常,您可能只需使用 32 位/64 位整数作为错误代码。例如:

typedef int ErrorCode;

...

void CMyCaller::CallsClient ()

    CheckResult ( CFunction ( ... ) );


void CheckResult ( ErrorCode nResult )

    // If you have more information (for example in a structure) then you can
    // use that to decide what kind of exception to throw.)
    if ( nResult < 0 )
        throw ( nResult );


...

// Client component's C interface

ErrorCode CFunction ( ... )

    ErrorCode nResult = 0;

    try
    
        ...
    
    catch ( CSomeException oX )
    
        nResult = -100;
    
    catch ( ... )
    
        nResult = -1;
    

    return ( nResult );

如果您需要比单个 int32/int64 更多的信息,那么您可以在调用之前分配一个结构并将其地址传递给 C 函数,该 C 函数反过来会在内部捕获异常,如果它们发生,则在其上抛出异常自己这边。

【讨论】:

如果可能的话,我不想丢失异常,例如,如果用户的回调抛出他们自己的异常类型,那么我也想抛出它 @SethCarnegie 如果您跨越模块边界(我假设您这样做,否则这整个事情不会成为问题),那么我看不到保留实际异常信息的方法。更复杂的是,C++ 允许任何类型成为异常,因此如果您想保留它,您需要知道要处理的类型。如果保证所有异常都是std::exception 派生类,这可能不是很难,但如果允许任何类型,那就改变了。

以上是关于跨 C API 边界传递异常的主要内容,如果未能解决你的问题,请参考以下文章

Java SE API know how-JNI-异常-日志

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

在C回调中的C ++中引发异常,可能会跨越动态库边界……是否安全?

WinForm跨线程访问控件异常

尝试在 Windows Phone 上显示相机胶卷中的图像时出现跨线程异常

如何在 C# 中解决跨域策略安全异常