Search code examples
exceptionboostexecutioncontextboost-context

boost context: problems with exception propagation


I'm using boost::context::execution_context (version 2) to write a C++ 11 library and I want to propagate exceptions from an execution_context to the calling execution.

I'd like to handle exceptions inside of a lambda that a client gives to my library function; however, I have encountered a strange issue where the exception is not handled by boost::context correctly in some cases.

This works as expected and is very similar to some of boost's tests and examples:

TEST(execution_context, works) {
  // Client callable
  auto &&f = [](boost::context::execution_context<void> &&ctx) {
    throw std::runtime_error("help!");
    return std::move(ctx);
  };

  // Library code
  std::exception_ptr exc{};
  boost::context::execution_context<void> source(
      [&exc, &f](boost::context::execution_context<void> &&ctx) {
        try {
          ctx = f(std::move(ctx));
        } catch (boost::context::detail::forced_unwind const &) {
          throw;
        } catch (...) {
          exc = std::current_exception();
        }
        return std::move(ctx);
      });

  try {
    source = source();
    if (exc) {
      std::rethrow_exception(exc);
    }
  } catch (std::runtime_error const &) {
    std::cout << "Runtime Error Caught" << std::endl;
  }
}

Output:

[ RUN      ] execution_context.works
Runtime Error Caught
[       OK ] execution_context.works (0 ms)

But the following change does not work:

We add a class which wraps the execution_context:

class Core {
  boost::context::execution_context<void> ctx_;

public:
  explicit Core(boost::context::execution_context<void> &&ctx)
      : ctx_{std::move(ctx)} {}

  auto &&done() { return std::move(ctx_); }
};

Now we make the same test as before but using the defined class:

TEST(execution_context, fails) {
  // Client callable
  auto &&f = [](Core c) {
    throw std::runtime_error("help!");
    return c.done();
  };

  // Library code
  std::exception_ptr exc{};
  boost::context::execution_context<void> source(
      [&exc, &f](boost::context::execution_context<void> &&ctx) {
        try {
          ctx = f(Core(std::move(ctx)));
        } catch (boost::context::detail::forced_unwind const &) {
          throw;
        } catch (...) {
          exc = std::current_exception();
        }
        return std::move(ctx);
      });

  try {
    source = source();
    if (exc) {
      std::rethrow_exception(exc);
    }
  } catch (std::runtime_error const &) {
    std::cout << "Runtime Error Caught" << std::endl;
  }
}

Output:

[ RUN      ] execution_context.fails
unknown file: Failure
Unknown C++ exception thrown in the test body.
generators.t.tsk: /home/plewis/dpkg/refroot/amd64/opt/include/boost/context/detail/exception.hpp:37: boost::context::detail::forced_unwind::~forced_unwind(): Assertion `caught' failed.
zsh: abort (core dumped)  ./test.t.tsk

The only difference I notice is that the execution_context is contained in a class, and that causes the exception to be handled improperly. That makes no sense.

Environment

I'm using GTest.

Compiler

> g++ --version
g++ (GCC) 5.3.1 20160406 (Red Hat 5.3.1-6)
...

System

> uname -a
Linux myhostname 2.6.32-642.6.2.el6.x86_64 #1 SMP Mon Oct 24 10:22:33 EDT 2016 x86_64 x86_64 x86_64 GNU/Linux

Boost

boost version 1.69.0 compiled for amd64


Solution

  • The issue is with moving ctx instance. Suppose you have two instances of execution_context:

    execution_context src, dest;
    // src and dest have some states
    dest = std::move(src); // [1]
    

    in [1] we are calling move assignment operator which "steals" resources of src and puts them into dest. If execution_context has a pointer, this pointer after moving will be 0 in src instance, so this object is useless and should not be used. Any operation on it may invoke some undesired behaviour.

    In short version your code looks like:

    void foo ()
    {
      auto &&f = [](Core c) {};
    
      boost::context::execution_context<void> source(
          [&exc, &f](boost::context::execution_context<void> &&ctx) {
            // many lines        
            return std::move(ctx);
          });
    
        source = source();
    }
    

    we have two contexts, foo and body of lambda passed into source constructor. When source() is called the context is switched and foo is resumed and lambda is executed. In this lambda context of foo is destroyed because it is just moved into Core instance. So how do you want to resume foo execution? You cannot.

    Problem with Core:

    class Core {
      boost::context::execution_context<void> ctx_;
    
    public:
      explicit Core(boost::context::execution_context<void> &&ctx)
          : ctx_{std::move(ctx)} {} // [2]
    
      auto &&done() { return std::move(ctx_); }
    };
    

    ctx_ is non-reference data member, so in [2] move constructor is called which steals ctx's resources.

    The next issue would be with done method if it didn't throw exception: Look at this:

      auto &&f = [](Core c)
      {
        // throw std::runtime_error("help!"); COMMENTED
        return c.done();
      };
    

    c is local inside lambda. done returns reference to data member of Core which is destroyed when lambda ends. So you have dangling reference.

    Fix: you can just store reference to context inside Core. Then original context of foo will be safe.