Guillaume Racicot
Guillaume Racicot

Reputation: 41820

Initiate an async operation with a result

I'm currently working on a project that uses boost asio for networking. I want to implement a function that look like this:

template<command T, response R = typename T::response_type>
auto send_command(T c) -> boost::asio::awaitable<R> {
    // ...
}

In my code, when I'm writing the command to the socket, I will eventually receive a response with the result. I may receive any number of other responses in the socket before getting the response to a specific command.

I figured I could use async_initiate to make sending a command an asynchronous operation composed of a write and a read.

I've already used async_initiate to start asynchronous operations, but it was of the type boost::asio::awaitable<void>. And the code looked like this:

// returns the awaitable for the coroutine to suspend
return asio::async_initiate<CompletionToken, void(void)>(
    [self = shared_from_this()](auto&& handler) {
        self->callback_queue.push(std::forward<decltype(handler)>(handler));
    }
);

// later

auto& resume = callback_queue.front();
resume(); // resume the suspended coroutine

But I can't figure where to put the return value here when It's not void!

auto const command_id = std::int32_t{...};

auto& resume_command = commands_continuations[command_id];
resume_command(response_result); // Send the `co_await` result in parameters??

I'm simply confused how to send the return of the awaitable and how to initiate the asynchronous operation with a result.

Upvotes: 4

Views: 3125

Answers (1)

Guillaume Racicot
Guillaume Racicot

Reputation: 41820

It turns out that there are a number of fixed signature a async operation can take.

The void(void) is one, but to have a result, we can simply use void(T):

return asio::async_initiate<CompletionToken, void(R)>(
    [self = shared_from_this()](auto&& handler) {
        self->callback_queue.push(std::forward<decltype(handler)>(handler));
    }
);

The handler will be a callable that takes a R as parameter and will become the result of the co_await operation.

To have exception handling, cancellation, I tried two different ways to propagate exceptions. One is to send an std::exception_ptr and the other was to use boost::system::error_code. In my case, I choose error code as I'm more comfortable using them.

return asio::async_initiate<CompletionToken, void(boost::system::error_code, R)>(
    [self = shared_from_this()](auto&& handler) {
        self->callback_queue.push(std::forward<decltype(handler)>(handler));
    }
);

The callback take form of a move only function that matches the signature void(boost::system::error_code, R). I call it like this:

auto callback = std::exchange(callback_queue[command_id], {});
if (result) {
    // resumes the coroutine normally with the result
    callback(boost::system::error_code{}, *result);
} else {
    // Throws exception in the coroutine
    callback(make_error_code(boost::system::errc::operation_canceled), R{});
}

There is more information about it in the documentation about ASIO/Networking TS

Upvotes: 3

Related Questions