Turning callback into coroutine while keeping symmetric transfer

Viewed 88

Based on the answer of Turning a function call which takes a callback into a coroutine I'm able to come up with a my version of a generic CallbackAwaiter class that I can inherent and wait for callbacks. However I can't figure out how to make it support symmetric transfer. Causing stack overflow in certain cases (mostly in complicated business logic). My code looks like this

template <typename T>
struct CallbackAwaiter
{
    bool await_ready() noexcept
    {
        return false;
    }

    const T &await_resume() const noexcept(false)
    {
        assert(result_.has_value() == true || exception_ != nullptr);

        if (exception_)
            std::rethrow_exception(exception_);
        return result_.value();
    }

  private:
    optional<T> result_;
    std::exception_ptr exception_{nullptr};

  protected:
    void setException(const std::exception_ptr &e)
    {
        exception_ = e;
    }
    void setValue(const T &v)
    {
        result_.emplace(v);
    }
    void setValue(T &&v)
    {
        result_.emplace(std::move(v));
    }
};

// For example. I can inherent the class and fill in `await_suspend` to convert callbacks into coroutines. 
struct SQLAwaiter : CallbackAwaiter<std::map<std::string, std::string>>
{
...
    void await_suspend(std::coroutine_handle<> handle)
    {
        dbClient->runSQL(..., [handle](){
            ...
            setValue(...);
            handle.resume();
        });
    }
};

This works. But by calling handle.resume() manually I don't support symmetric transfer. Which stack overflows after deep corouting resume. So far I tried adding promise_type and using std::noop_coroutine to get symmetric transfer working. For example

    std::noop_coroutine_handle await_suspend(std::coroutine_handle<> handle)
    {
        dbClient->runSQL(..., [handle](){
            ...
            setValue(...);
            handle.resume();
        });
        return std::noop_coroutine{};
    }

// and

struct CallbackAwaiter
{
    CallbackAwaiter() : coro_(std::noop_coroutine{}) {}
    std::coroutine_handle<promise_type> coro_;
}

But obviously these wouldn't work. Returning noop_coroutine doesn't magically make handle.resume() not take up stack space. Nor does adding promise_type would work as there's no compiler generated coroutine.

I'm out of idea. How can I support symmetric transfer for such case?

1 Answers

In most cases, your existing code

    void await_suspend(std::coroutine_handle<> handle)
    {
        dbClient->runSQL(..., [handle](){
            ...
            setValue(...);
            handle.resume();
        });
    }

should already be fine.

Depending on your implementation of dbClient->runSQL, the callback which calls handle.resume will be executed on a fresh stack, with only the stack frames from the IO multiplexer, and a couple of other dbClient-internal functions on it.

Symmetric transfer is only a concern if your runSQL function calls its callback synchronously. As long as runSQL always calls its callback asynchronously (i.e. on a "fresh" stack), then you are already fine

Related