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

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

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

【问题讨论】:

    标签: c++ exception exception-handling c-api


    【解决方案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 派生类,这可能不是很难,但如果允许任何类型,那就改变了。
    猜你喜欢
    • 2011-07-03
    • 1970-01-01
    • 1970-01-01
    • 2013-02-15
    • 2012-06-02
    • 1970-01-01
    • 2013-07-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多