We have a codebase in python which uses asyncio, and co-routines (async
methods and await
s), what I'd like to do is to call one of these method from a C++ class which has been pulled into python (using pybind11)
Let's say there is this code:
class Foo:
async def bar(a, b, c):
# some stuff
return c * a
Assuming that the code is being invoked from python and there is an io loop handling this, at some point, the code drops into C++ land where this bar
method needs to be invoked - how does one await
the result of this in C++?
To run an async function (coroutine) you have to call it using an Event Loop. Event Loops: You can think of Event Loop as functions to run asynchronous tasks and callbacks, perform network IO operations, and run subprocesses. Example 1: Event Loop example to run async Function to run a single async function: Python3.
Asyncio is a C++20 coroutine library to write concurrent code using the await syntax, and imitate python asyncio library.
There are two basic types of methods in the Parallels Python API: synchronous and asynchronous. When a synchronous method is invoked, it completes executing before returning to the caller. An asynchronous method starts a job in the background and returns to the caller immediately.
It is possible to implement a Python coroutine in C++, but takes some work. You need to do what the interpreter (in static languages the compiler) normally does for you and transform your async function into a state machine. Consider a very simple coroutine:
async def coro():
x = foo()
y = await bar()
baz(x, y)
return 42
Invoking coro()
doesn't run any of its code, but it produces an awaitable object which can be started and then resumed multiple times. (But you don't normally see these operations because they are transparently performed by the event loop.) The awaitable can respond in two different ways: by 1) suspending, or by 2) indicating that it is done.
Inside a coroutine await
implements suspension. If a coroutine were implemented with a generator, y = await bar()
would desugar to:
# pseudo-code for y = await bar()
_bar_iter = bar().__await__()
while True:
try:
_suspend_val = next(_bar_iter)
except StopIteration as _stop:
y = _stop.value
break
yield _suspend_val
In other words, await
suspends (yields) as long as the awaited object does. The awaited object signals that it's done by raising StopIteration
, and by smuggling the return value inside its value
attribute. If yield-in-a-loop sounds like yield from
, you're exactly right, and that is why await
is often described in terms of yield from
. However, in C++ we don't have yield
(yet), so we have to integrate the above into the state machine.
To implement async def
from scratch, we need to have a type that satisfies the following constraints:
__await__
method that returns an iterable, which can just be self
;__iter__
which returns an iterator, which can again be self
;__next__
method whose invocation implements one step of the state machine, with return meaning suspension and raising StopIteration
meaning finishing.The above coroutine's state machine in __next__
will consist of three states:
foo()
sync functionbar()
coroutine for as long as it suspends (propagating the suspends) to the caller. Once bar()
returns a value, we can immediately proceed to calling baz()
and returning the value via the StopIteration
exception.So the async def coro()
definition shown above can be thought of as syntactic sugar for the following:
class coro:
def __init__(self):
self._state = 0
def __iter__(self):
return self
def __await__(self):
return self
def __next__(self):
if self._state == 0:
self._x = foo()
self._bar_iter = bar().__await__()
self._state = 1
if self._state == 1:
try:
suspend_val = next(self._bar_iter)
# propagate the suspended value to the caller
# don't change _state, we will return here for
# as long as bar() keeps suspending
return suspend_val
except StopIteration as stop:
# we got our value
y = stop.value
# since we got the value, immediately proceed to
# invoking `baz`
baz(self._x, y)
self._state = 2
# tell the caller that we're done and inform
# it of the return value
raise StopIteration(42)
# the final state only serves to disable accidental
# resumption of a finished coroutine
raise RuntimeError("cannot reuse already awaited coroutine")
We can test that our "coroutine" works using real asyncio:
>>> class coro:
... (definition from above)
...
>>> def foo():
... print('foo')
... return 20
...
>>> async def bar():
... print('bar')
... return 10
...
>>> def baz(x, y):
... print(x, y)
...
>>> asyncio.run(coro())
foo
bar
20 10
42
The remaining part is to write the coro
class in Python/C or in pybind11.
This isn't pybind11, but you can call an async function directly from C. You simply add a callback to the future using add_done_callback. I assume pybind11 allows you to call python functions so the steps would be the same:
https://github.com/MarkReedZ/mrhttp/blob/master/src/mrhttp/internals/protocol.c
result = protocol_callPageHandler(self, r->func, request))
Now the result of an async function is a future. Just like in python you need to call create_task using the resulting future:
PyObject *task;
if(!(task = PyObject_CallFunctionObjArgs(self->create_task, result, NULL))) return NULL;
And then you need to add a callback using add_done_callback:
add_done_callback = PyObject_GetAttrString(task, "add_done_callback")
PyObject_CallFunctionObjArgs(add_done_callback, self->task_done, NULL)
self->task_done is a C function registered in python which will be called when the task is done.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With