0

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

ioquatix
  • 1,411
  • 17
  • 32
  • I don't know if it's valid or not, but it does make me scream internally. ***Why*** do you want to use a `void *` to pass around a callable object? Why not use the `FunctionT` template type? Or `std::function`? – Some programmer dude Jul 03 '17 at 13:12
  • I thought I explained it, but, because the current implementation of `cocall` is only able to handle one `void *` argument to the new stack. – ioquatix Jul 03 '17 at 13:14
  • You should at least add a `static_assert(sizeof(void *) == sizeof(&function));` because a pointer to function may not have the same size as pointer to void. – user7860670 Jul 03 '17 at 13:15
  • 3
    C does [not allow casting function pointers to `void*`](https://stackoverflow.com/questions/5579835/c-function-pointer-casting-to-void-pointer) and I know it was alike in C++ - unless this changed recently (which I am not aware of), you are doing illegal stuff anyway... – Aconcagua Jul 03 '17 at 13:16
  • 1
    @VTT shouldn't all pointers be the same size? – ioquatix Jul 03 '17 at 13:16
  • 2
    No, the size of pointers may vary. On x86 the size of a pointer to a regular function (lambda in your case is just a plain function as it does not capture anything) is the same as size of data pointer. But it may vary. Size of a pointer to a member function is much bigger. Also in this example you may actually pass a pointer to a lambda object, not to a function. – user7860670 Jul 03 '17 at 13:19
  • @Aconcagua Interesting, I have all warnings turned on including pedantic and don't get any messages about it being invalid. – ioquatix Jul 03 '17 at 13:19
  • 1
    @Aconcagua If I understood [this question](https://stackoverflow.com/questions/27229578/using-reinterpret-cast-to-cast-a-function-to-void-why-isnt-it-illegal) correctly, it is allowed since C++11. – Rakete1111 Jul 03 '17 at 13:20
  • 3
    @ioquatix There can be some special cpus, where a function pointer and a data pointer do not have the same size (it has something to do with the size of the address-, data- and instruction- bus), but on the common cpus today they have the same size. – mch Jul 03 '17 at 13:21
  • 1
    @Aconcagua - Fortunately, a lambda is not a function pointer, but an object. – StoryTeller - Unslander Monica Jul 03 '17 at 13:27
  • @VTT - A lambda will not decay to a function pointer here. `FunctionT` will be deduced as the closure type of the lambda, an object. – StoryTeller - Unslander Monica Jul 03 '17 at 13:29
  • @StoryTeller Interesting enough: Lambdas with empty capture list can be assigned to function pointers. What are these then? Indeed functions or some kind of compatible callable object? – Aconcagua Jul 03 '17 at 13:41
  • 1
    @Aconcagua - Functors. Capture-less lambdas define a conversion to a function pointer, but they are still objects themselves. And in this context it makes a huge difference, because the deduced type is of an object. – StoryTeller - Unslander Monica Jul 03 '17 at 13:44

1 Answers1

1

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • This looks really interesting, I will experiment with this approach. I appreciate the code example as it's really helpful to see exactly what you mean. – ioquatix Jul 04 '17 at 01:23