I'm working on implementing fibers using coroutines implemented in assembler. The coroutines work by cocall
to change stack.
I'd like to expose this in C++ using a higher level interface, as cocall
assembly can only handle a single void*
argument.
In order to handle template lambdas, I've experimented with converting them to a void*
and found that while it compiles and works, I was left wondering if it was safe to do so, assuming ownership semantics of the stack (which are preserved by fibers).
template <typename FunctionT>
struct Coentry
{
static void coentry(void * arg)
{
// Is this safe?
FunctionT * function = reinterpret_cast<FunctionT *>(arg);
(*function)();
}
static void invoke(FunctionT function)
{
coentry(reinterpret_cast<void *>(&function));
}
};
template <typename FunctionT>
void coentry(FunctionT function)
{
Coentry<FunctionT>::invoke(function);
}
int main(int argc, const char * argv[]) {
auto f = [&]{
std::cerr << "Hello World!" << std::endl;
};
coentry(f);
}
Is this safe and additionally, is it efficient? By converting to a void*
am I forcing the compiler to choose a less efficient representation?
Additionally, by invoking coentry(void*)
on a different stack, but the original invoke(FunctionT)
has returned, is there a chance that the stack might be invalid to resume? (would be similar to, say invoking within a std::thread
I guess).
Everything done above is defined behaviour. The only performance hit is that inlining something aliased thro7gh a void pointer could be slightly harder.
However, the lambda is an actual value, and if stored in automatic storage only lasts as long as the stored-in stack frame does.
You can fix this a number of ways. std::function
is one, another is to store the lambda in a shared_ptr<void>
or unique_ptr<void, void(*)(void*)>
. If you do not need type erasure, you can even store the lambda in a struct with deduced type.
The first two are easy. The third;
template <typename FunctionT>
struct Coentry {
FunctionT f;
static void coentry(void * arg)
{
auto* self = reinterpret_cast<Coentry*>(arg);
(self->f)();
}
Coentry(FunctionT fin):f(sts::move(fin)){}
};
template<class FunctionT>
Coentry<FunctionT> make_coentry( FunctionT f ){ return {std::move(f)}; }
now keep your Coentry
around long enough until the task completes.
The details of how you manage lifetime depend on the structure of the rest of your problem.