【问题标题】:A mechanism to ensure exception propagation when mixing C and C++ code混合 C 和 C++ 代码时确保异常传播的机制
【发布时间】:2011-12-08 13:00:52
【问题描述】:

我不是在问 C++ 异常通过 C 代码传播是否安全,也不是在这种情况发生时会发生什么。我在 SO(1, 2, 3) 和 this FAQ 中阅读了以下问题。我在问如何继续:

  • 避免将任何 C++ 异常泄露给 C 代码(这意味着在调用 C 代码之前捕获 C++ 领域中的所有异常)
  • 还能够捕获 C 代码之外的异常(在更高的 C++ 代码中)。

让我说明一下我的想法:

假设libfoo 是一个C 库,我想在我的bar C++ 程序中使用它。 libfoo 需要我必须提供的回调函数 foo_callback。我的回调中使用的函数和方法可能会抛出异常,所以我写了:

void my_callback(void)
{
    try
    {
        // Do processing here.
    }
    catch(...)
    {
        // Catch anything to prevent an exception reaching C code.
        // Fortunately, libfoo provides a foo_error function to
        // signal errors and stop processing.
        foo_error() ;
    }
}

然后我使用如下所示的回调:

// The bar program.
int main()
{
    // Use libfoo function to set the desired callback
    foo_set_callback(&my_callback) ;

    // Start processing. This libfoo function uses internally my_callback.
    foo_process() ;

    // Check for errors
    if( foo_ok() )
    {
        // Hurray ! 
    }
    else
    {
        // Something gone wrong.
        // Unfortunately, we lost the exception that caused the error :(
    }
}

我想要的是能够在main 函数中捕获从my_callback 抛出的异常,而不会通过libfoo 传播异常(是的,这是一种通过C 进行quantum tunnelling 实验的量子异常代码)。

所以我想使用的代码:

void my_callback(void)
{
    try
    {
        // Do processing here.
    }
    catch(...)
    {
        // Catch anything to prevent an exception reaching C code.
        // We save the exception using (the magic) ExceptionHolder.
        ExceptionHolder::Hold() ;

        // Call foo_error function to signal errors and stop processing.
        foo_error() ;
    }
}

// The bar program.
int main()
{
    // Use libfoo function to set the desired callback
    foo_set_callback(&my_callback) ;

    try
    {
        // Start processing. This libfoo function uses internally my_callback.
        foo_process() ;

        // Once gone out of the C land, release any hold exception.
        ExceptionHolder::Release() ;
    }
    catch(exception & e)
    {
        // Something gone wrong.
        // Fortunately, we can handle it in some manner.
    }
    catch( /*something else */ )
    {
    }
    // ...
}

鉴于以下限制:

  • libfoo 是源代码封闭的,用 C 语言编写,并以供应商的编译格式提供。在库上进行的测试表明异常不能通过它传播。我无法访问源文件,也无法获得支持异常的编译版本。
  • 回调函数广泛使用了使用异常的 C++ 代码。所有的错误处理都是围绕异常机制构建的。我绝对不能简单地使用吞下所有异常的代码。
  • 不涉及多线程。
  • 不支持 c++0x。

我的问题:

  • 它是否已经被某些库或某些 C++ 魔法(如 boost)甚至 c++0x 解决了?
  • 如果不是,我如何编写一个适用于任何异常类型的 ExceptionHolder ?我对 C++ 很满意,但我还没有找到一种方法来编写一个可靠且易于使用的 ExceptionHolder,它适用于任何异常类型。

非常感谢您的建议!


编辑:我添加了一个响应,其中包含一些异常保持/释放机制的实现。欢迎所有批评或建议。

【问题讨论】:

    标签: c++ c exception


    【解决方案1】:

    在 c++11 中,您可以在回调中使用 current_exception() 来设置 exception_ptr 类型的变量,然后当您的调用代码被库通知错误时使用 rethow_exception()。

    这与 c++0x 中用于跨线程传播异常的机制相同。

    例子:


    void foo();                      // C++
    extern "C" void bar(void *exp);  // C
    void baz(void *exp);             // C++
    
    void foo() {
        std::exception_ptr exp;
        bar(&exp);
        if (exp) {
          std::rethrow_exception(exp);
        }
    }
    
    extern "C" void bar(void *exp) {
        baz(exp);
    }
    
    void baz(void *exp) {
        try {
            // some code that might throw an exception
            // ...
        } catch (...) { // catch all exceptions, so as to avoid leaking any into the calling C code.
            // capture the exception and make it available to the C++ code above the C code.
            static_cast<std::exception_ptr*>(exp) = std::current_exception();
        }
    }
    

    【讨论】:

      【解决方案2】:

      编辑:您可以使用fungo,这是我在下面描述的想法的更好实现。来自其作者:

      fungo 是一个 C++ 库,专为我们这些使用尚不支持 std::exception_ptr 的旧 C++ 实现的人而设计。

      换句话说,fungo 允许您公平地尝试存储和稍后重新抛出被 catch(...) 块捕获的异常。这对于跨线程连接或 C/C++ 边界接口传播异常很有用。

      我会将此标记为答案。


      正如我在我的问题中提到的,我现在不能使用 C++0x/11 功能(现在还没有计划使用新功能),我将在这里展示我到目前为止所做的事情:

      异常的生命周期跨越 try-catcher 块。为了保存一个异常,必须在堆上创建一个副本。我们在重新抛出异常时摆脱了副本。我写了一个异常持有者接口:

      class ExceptionHolderInterface
      {
          public :
            ExceptionHolderInterface(void) ;
            virtual ~ExceptionHolderInterface(void) ;
            
            /* For holding an exception. To be called inside a catch block.*/
            virtual void  Hold(void)    = 0 ;
      
            /* For releasing an exception. To be called inside a try block.*/
            virtual void  Release(void) = 0 ;
          private :
      } ;
      

      这是一个类型独立的类。使用模板引入异常类型:

      template<typename ExceptionType>
      class ExceptionHolder : public ExceptionHolderInterface
      {
          public :
            ExceptionHolder(void) ;
            virtual ~ExceptionHolder(void) ;
      
            virtual void Hold(void)
            {
                 try
                 {
                     throw ;
                 }
                 catch(const ExceptionType & e)
                 {
                     exception.reset(new ExceptionType(e)) ;
                 }
            }
      
            virtual void Release(void)
            {
                if(exception.get())
                {
                    throw ExceptionType(*exception.get()) ;
                }
            }
          private :
            std::auto_ptr<ExceptionType> exception ;
            
            // declare the copy-constructor and the assignment operator here to make the class non-copyable
      } ;
      

      我删除了一堆测试/优化/验证,我保留了主要思想。到目前为止,我们有一个类型的异常持有者,因此我们可以构建一个异常存储,它可以一次持有多种类型。

      class ExceptionStore
      {
          public :
            ExceptionStore(void) ;
            ~ExceptionStore(void)
            {
                for(Iterator holder = exception_holders.begin() ; holder != exception_holders.end() ; ++holder)
                {
                    delete (*holder) ;
                }
            }
      
            // Add an exception type to handle
            template<typename ExceptionType>
            void AddExceptionHolder(void)
            {
                exception_holders.push_back(new ExceptionHolder<ExceptionType>()) ; 
            }
      
            // Try to hold an exception using available holders. Use this inside a catch block.
            void Hold(void)
            {
                Iterator holder = exception_holders.begin() :
                while(holder != exception_holders.end())
                {
                    try
                    {
                        (*holder)->Hold() ;
                        break ;
                    }
                    catch(...)
                    {
                        ++holder ;
                    }
                }
            }
      
            // Try to release any hold exception. Call this inside a try-block.
            void Release(void)
            {
                Iterator holder = exception_holders.begin() :
                while(holder != exception_holders.end())
                {
                    (*holder++)->Release() ;
                }
            }
      
          private :
            std::list<ExceptionHolderInterface *>  exception_holders ;
            typedef std::list<ExceptionHolderInterface *>::iterator Iterator ;
            
            // Declare the copy-constructor and the assignment operator here to make the class non-copyable
      } ;
      

      我可以使用如下所示的异常存储:

      // I made a global ExceptionStore just to keep the example simple.
      ExceptionStore exception_store ;
      
      void callable_from_c_code(void)
      {
          // Normally, we should retrieve the exception store in some manner.
          
          try
          {
              // Do processing here. Exceptions may be thrown.
          }
          catch(...)
          {
              // Something wrong happened. Let's store the error for later.
              exception_store.Hold() ;
          }
      
          // Exceptions do not propagate to C code. 
      } 
      
      int main(int, char * [])
      {
          // First, set the exception types we want to handle. The handling is done in
          // the same order as below.
          exception_store.AddExceptionHolder<std::runtime_error>() ;
          exception_store.AddExceptionHolder<std::logic_error>() ;
          exception_store.AddExceptionHolder<MyFancyException>() ;
      
          // Somehow invoke some C code that uses `callable_from_c_code`
          use_some_c_library_with_callback(&callable_from_c_code) ;
      
          // Handle any caught exception
          try
          {
              exception_holder.Release() ;
          }
          catch(std::exception &)
          {
              // Something gone wrong ...
          }
          catch(MyFancyException &)
          {
              // Nothing fancy despite the name. We have problems here ...
          }
      }
      

      这是非常基本的,可能会出现一些本示例未处理的意外场景。如果抛出未使用AddExceptionHolder 声明的类型的异常,则有两种可能性:

      • 基础类型的持有者被添加到存储中,因此异常将被捕获和切片,只保留基础类型的副本。
      • 没有持有人适合例外,它只是丢失了。没有任何东西泄漏到 C 土地上。

      目前,我更喜欢使用此解决方案而不是经过更多测试/使用/验证的 boost::enable_current_exception,因为我无法负担重构整个 C++ 代码以使用 boost::enable_current_exception(...) 包围所有抛出站点。

      无论如何,std::exception_ptr 似乎是完美的解决方案,一旦我可以迁移到新的 C++ 标准,我将替换上面的代码。

      【讨论】:

        【解决方案3】:

        如果您不准备进行重大重构,您是否能够识别可能引发的特定异常类型子集?

        如果是这样,您可能可以编写一个容器来专门存储每个副本,然后在从 C 调用返回时检索它们。这需要能够复制异常类型。

        或者,考虑各种异常的最终解决方案是什么(如果可以的话),并将其打包到某个对象中以便在返回时进行处理。这仅在您拥有足够的代码库以了解异常可能产生的潜在处理时才有效。

        混合模型会捕获异常,根据发现的内容创建一个新异常,然后在从 C 库返回时抛出该异常。

        【讨论】:

        • 谢谢!这基本上就是我目前正在做的事情,但处理特定的异常类型子集是我当前解决方案的弱点。缺少一些案例会破坏可靠性。
        • 不确定我是否关注。如果库包装器不知道如何处理异常,那么调用代码是否经常这样做?如果不是,则映射到单个类型以进行重新抛出的 catch (...) 将为您提供所需的已定义行为。
        • 好主意。坚持例外让我觉得使用错误的工具进行流量控制。我的代码需要重新思考...
        【解决方案4】:

        我相信 boost.exception 有一种机制可以适应你的目的。请参阅此处获取灵感:

        http://www.boost.org/doc/libs/1_47_0/libs/exception/doc/tutorial_exception_ptr.html

        它似乎是为了在线程之间传达它们的特殊异常类型,但我认为这几乎是一回事——阻止异常传播穿过线程边界或 C 代码边界,将它的副本隐藏在一个指针,然后稍后通过边界另一侧的指针检索它,并可选择重新抛出它。

        我不确定让你自己的异常从 boost 的神奇异常类型派生有多可行,但如果我记得大约一年前使用它的工具时没记错的话,那是相当合理的。

        【讨论】:

        • 可能值得一提的是,boost 方法相当直接地反映了已在 C++0x 中引入标准库以解决相同问题的机制。不过,据我所知,它们更神奇。
        • 谢谢!我会调查你的提议,因为它看起来很有希望:)
        • 在玩过boost::enable_current_exception 之后,我发现它的灵活性很小,因为它强制用户在所有可能的抛出站点中用boost::enable_current_exception 包围所有异常,然后才能使用它。我无法承受当前代码库中的这种主要重构。
        猜你喜欢
        • 2011-01-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-02-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多