Search code examples
c++functionlambdacoroutinereinterpret-cast

Is it safe to convert a template lambda to a `void *`?


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).


Solution

  • 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.