cppgraphqlgen
cppgraphqlgen copied to clipboard
Any real-world examples where the coroutine backing can be applied?
I've been having a lot of ... fun implementing boost::asio
alongside this lib - mainly due to the fact that the ASIO coroutine co_await
handler looks like this
// Support for co_await keyword.
template <class U>
void await_suspend(
detail::coroutine_handle<detail::awaitable_frame<U, Executor>> h)
{
frame_->push_frame(&h.promise());
}
Obviously, once cppgraphqlgen's resolve
function has been entered, all the context needed for filling out the std::coroutine_handle<detail::awaitable_frame<U, Executor>>
disappears, and is replaced with graphql::service::AwaitableObject<#value>::promise_type
My not-really-ideal workaround has been
- Enter the GraphQL system from a
boost::asio::awaitable
context - Drop into the cppgraphqlgen
resolve
context, lose my immediateboost::asio
context and have all functions returnstd::future<T>
to fullfill thegraphql::service::AwaitableObject
- Use
boost::asio:co_spawn
to restart theboost::asio::awaitable
contexts and return futures viaboost::asio::use_future
I'm curious that due to the fact that boost::asio::awaitable
's context is basically the internal guts for its actual coroutine-execution-operation-element-bits, are there any other good examples for async-execution frameworks/libraries that can be used with this library and don't require modifying that underlying service::Awaitable
type to be aware of how the async library works?
I suppose for comparison, I'm thinking of C libraries which do a sync version of their calls, then specialise for libuv
, libevent
etc - I am really hoping that C++ coroutines give us some means of abstracting the libuv
-equivalent away, but given my experience so far, I'm guessing maybe it can't.
I'm not sure if I understand the question, it's been a while since I played with Boost.asio
, and I wasn't using C++ coroutines yet at the time. Can you point me to a minimal sample of your workaround?
The idea I used for integrating with other coroutines was to make the type-erased service::await_async
type. The unit tests in test/CoroutineTests.cpp demonstrate passing different default implementations wrapped in service::await_async
, including the std::suspend_never
type for synchronous/blocking execution. If you can wrap an awaitable from Boost.asio
in service::await_async
, you can get it to return control to that executor at suspend points in each resolver.
If you're implementing a coroutine with a Boost.asio
awaitable return type, you should still be able to co_await
on the top-level resolve
call. If the custom awaitable never suspends inside of the resolver, I think it'll just run synchronously and block the current thread.
So, I think the best way to integrate with Boost.asio
is probably to figure out how to wrap its awaitable type in graphql::service::await_async
as a type-erased awaitable. Then it should call back into Boost.asio
's await_suspend
and await_resume
methods/functions. Inside of your Boost.asio
coroutine, use co_await
to get the result of the top-level resolver, and C++20 should wire up the suspend points in a way that's compatible in the other direction with Boost.asio
.
Can you point me to a minimal sample of your workaround?
I'll try to write something up this arvo or tomorrow
I think the best way to integrate with Boost.asio is probably to figure out how to wrap its awaitable type in graphql::service::await_async as a type-erased awaitable
Yeah that is what I'm trying to chase now, hitting the issue of how do I "bridge" the Boost::Asio coro-promise type with the graphqlgen::promise-type - which I think is primarily due to how Boost Asio has implemented their coro-promise type and me just not understanding the coro-promise idea really well. But thats for me to nut out.
I suppose my question is more general - its more around the idea that given your library makes no assumption on executor/event-loop mechanics behind the scenes, can an off-the-shelf implementation of a general-purpose-user-level async library be integrated without needing to have all its specifics be known in advance. I was unsure if you'd written this in mind with plugging it into another async framework that might illustrate how that would work.
This is in comparison to how the same async-awareness concepts get implemented in C, which from what I've seen is along the lines of "reimplement helpers for the 3x big event loop libraries (libuv, libevent, libev) specifically" - nothing "general" about it. But the one nice thing about that style of implementation is that if you know your networking lib uses libuv
and your DB driver uses libuv
and this lib uses libuv
- then it all should go together with minimal glue.
Boost::Asio is a good example here given its conceptual relevance to the Networking TS - so I'll try and write up the minimal example of what I've gotten here (which is basically a HTTP server, GraphQL behind it, and a proxied HTTP client call behind a resolver)
Made https://github.com/ALTinners/cppgraphql-asio-demo-problem as an example of something reasonably indicative
So thats a hacked up copy of the Boost Beast HTTP code from https://www.boost.org/doc/libs/1_80_0/libs/beast/doc/html/beast/examples.html - with C++20 coroutine implementations instead of callbacks for most async capable operations
It basically gets a HTTP request, proxies a request off to http://google.com:80
for its status code (301) and returns it
Of interest mainly are the two calls into and out-of GQL
https://github.com/ALTinners/cppgraphql-asio-demo-problem/blob/739890e77a20be1a125c1b5ce02dab5f6065e038/src/main.cpp#L241 goes into GQL - but co_await
won't work due to the change of coro-promise-type
https://github.com/ALTinners/cppgraphql-asio-demo-problem/blob/739890e77a20be1a125c1b5ce02dab5f6065e038/src/main.cpp#L88 is the proxied call - again co_await
won't work due to the promise type change
I've got to get on with some other stuff this arvo before I throw some more time at trying to get a await_async
that might have some capability of handling this - but AFAIK I can't ever do a simple co_await
for the Boost async routines on the resolver side of the Operations
object, because of the change of coro-promise type.
I took a look at your sample, and I think this may require a change in cppgraphqlgen
, specifically for the awaitable types. The await_suspend
call only takes a coroutine_handle<void>
, whereas Boost.asio
uses a template method to specialize coroutine_handle<U>
for other promise types, including its own. IOW, I think I understand your question now. 😆
The type-erased awaitable may be flexible enough already, I also haven't fully investigated that.
I tried some tweaks to AwaitableScalar<>::await_suspend
, but I wasn't able to get it to work with the boost::asio::awaitable<>
promise_type
(or vice versa). I think the lack of executors in the standard may just mean that there's not a good way to do that yet in C++20.
I was able to use the type-erased graphql::service::await_async
as a graphql::service::RequestResolveParams::launch
option to get it to at least push execution to the boost::asio
executor:
struct await_executor : coro::suspend_always
{
using asio_executor = boost::asio::any_io_executor;
await_executor(asio_executor &&executor)
: m_executor{std::move(executor)}
{
}
void await_suspend(coro::coroutine_handle<> h)
{
m_future = boost::asio::co_spawn(
m_executor,
[h]() -> boost::asio::awaitable<void>
{
h.resume();
co_return;
},
boost::asio::use_future);
}
void await_resume()
{
if (m_future)
{
m_future->get();
m_future.reset();
}
}
private:
asio_executor m_executor;
std::optional<std::future<void>> m_future;
};
Since AwaitableScalar<>::await_suspend
will spin up another thread for a future, I also had it block the thread and return the value from the std::future<>
returned by boost::asio::co_spawn
with use_future
. That way the only extra threads are the ones that co_spawn
uses on its executor to start the async task and wrap it in a future:
std::optional<int> getStatusCode(std::string addressArg)
{
return getAsyncCode(addressArg).get();
}
I think maybe I could implement something like this on AwaitableScalar<T>::promise_type
as:
template <typename U>
AwaitableScalar<T> await_transform(U awaitable)
{
// ...
}
That should implicitly convert co_await
for boost::asio::awaitable<T>
to an AwaitableScalar<T>
. It would probably still need some sort of adapter type though, like await_executor
because the logic for how you get from one type of awaitable to the other needs to be provided by the integrator.
It wouldn't work the other way either, where a boost::asio::awaitable<T>
coroutine could co_await
on an AwaitableScalar<T>
, unless one of boost::asio::awaitable<T>
's await_transform
template methods support plugging in another type/helper. If not, maybe there's a way to declare an overloaded operator co_await
which can map from AwaitableScalar<T>
back to boost::asio::awaitable<T>
.