Есть ли способ вызвать асинхронный метод Python из C ++? - PullRequest
0 голосов
/ 06 февраля 2019

У нас есть кодовая база в python, которая использует asyncio и сопрограммы (async методы и await s), что я хотел бы сделать, это вызвать один из этих методов из класса C ++, который былпотянул в python (используя pybind11)

Допустим, есть этот код:

class Foo:
  async def bar(a, b, c):
    # some stuff
    return c * a

Предполагается, что код вызывается из python, и в какой-то момент есть цикл io, обрабатывающий это, код попадает в землю C ++, где этот bar метод должен быть вызван - как один await результат этого в C ++?

Ответы [ 3 ]

0 голосов
/ 13 февраля 2019

Возможно реализовать сопрограмму Python в C ++, но это требует некоторой работы.Вам нужно сделать то, что обычно делает для вас интерпретатор (на статических языках компилятор), и преобразовать вашу асинхронную функцию в конечный автомат.Рассмотрим очень простую сопрограмму:

async def coro():
    x = foo()
    y = await bar()
    baz(x, y)
    return 42

Invoking coro() не выполняет ни один из его кодов, но создает ожидаемый объект, который можно запустить, а затем возобновить несколько раз.(Но вы обычно не видите эти операции, потому что они прозрачно выполняются циклом событий.) Ожидаемое может ответить двумя различными способами: 1) приостановить или 2), указывая, что это сделано.

Внутри сопрограмма await реализует подвеску.Если сопрограмма была реализована с помощью генератора, то y = await bar() будет десагарно:

# 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

Другими словами, await приостанавливает (возвращает) столько, сколько ожидает ожидаемый объект.Ожидаемый объект сигнализирует о том, что это сделано путем поднятия StopIteration и контрабанды возвращаемого значения внутри его атрибута value.Если выход в цикле звучит как yield from, вы совершенно правы, и именно поэтому await часто описывается в терминах из yield from.Однако в C ++ у нас нет yield ( пока ), поэтому мы должны интегрировать вышеперечисленное в конечный автомат.

Чтобы реализовать async def с нуля, нам нужноиметь тип, удовлетворяющий следующим ограничениям:

  • мало что делает при построении - обычно он просто хранит полученные аргументы
  • имеет метод __await__, который возвращаетитерация, которая может быть просто self;
  • , имеет __iter__, который возвращает итератор, который снова может быть self;
  • , имеет метод __next__, вызов которого реализуетодин шаг конечного автомата, с возвратом, означающим приостановку и поднятием StopIteration, означающим окончание.

Вышеупомянутый конечный автомат сопрограммы в __next__ будет состоять из трех состояний:

  1. начальный, когда он вызывает функцию foo() sync
  2. , следующее состояние, когда он продолжает ожидать сопрограмму bar() до тех пор, пока он приостанавливает (распространяет приостановки) для вызывающей стороны.Как только bar() возвращает значение, мы можем немедленно перейти к вызову baz() и возвращению значения через исключение StopIteration.
  3. конечное состояние, которое просто вызывает исключение, информирующее вызывающего абонента о том, что сопрограмма потрачена.

Таким образом, приведенное выше определение async def coro() можно рассматривать как синтаксический сахар для следующего:

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

Мы можем проверить, что наша "сопрограмма" работает с использованием реальной асинхронности:

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

Оставшаяся часть - написать класс coro в Python / C или в pybind11.

0 голосов
/ 08 марта 2019

Для подобных вещей, если я не хочу углубляться в API CPython, я просто пишу свои вещи на Python и вызываю это, используя pybind s интерфейс Python.

Пример:https://github.com/RobotLocomotion/drake/blob/a7700d3/bindings/pydrake/init.py#L44 https://github.com/RobotLocomotion/drake/blob/a7700d3/bindings/pydrake/pydrake_pybind.h#L359

Для рендеринга в этом случае использования, возможно, вы можете сделать:

# cpp_helpers.py
def await_(obj):
    return await obj
py::object py_await = py::module::import("cpp_helpers").attr("await_");
auto result = py::cast<MyResult>(py_await(py_obj));

Однако, скорее всего, это будет менее производительным, чем вышерешения.

0 голосов
/ 10 февраля 2019

Это не pybind11, но вы можете вызвать асинхронную функцию непосредственно из C. Вы просто добавляете обратный вызов в будущее, используя add_done_callback.Я предполагаю, что pybind11 позволяет вам вызывать функции python, поэтому шаги будут одинаковыми:

https://github.com/MarkReedZ/mrhttp/blob/master/src/mrhttp/internals/protocol.c

result = protocol_callPageHandler(self, r->func, request))

Теперь результатом асинхронной функции является будущее.Как и в python, вам нужно вызвать create_task, используя результирующее будущее:

PyObject *task;
if(!(task = PyObject_CallFunctionObjArgs(self->create_task, result, NULL))) return NULL;

И затем вам нужно добавить обратный вызов, используя add_done_callback:

add_done_callback = PyObject_GetAttrString(task, "add_done_callback")
PyObject_CallFunctionObjArgs(add_done_callback, self->task_done, NULL)

self-> task_done является Cфункция, зарегистрированная в python, которая будет вызываться после выполнения задачи.

...