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.
I'm using GTest.
> g++ --version
g++ (GCC) 5.3.1 20160406 (Red Hat 5.3.1-6)
...
> 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 version 1.69.0 compiled for amd64
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.