pybind11 icon indicating copy to clipboard operation
pybind11 copied to clipboard

[FEAT] def_async

Open hiaselhans opened this issue 5 years ago • 7 comments

i made a simple Awaitable class in c++ to support pythons async/await syntax with c++ deferred functions:

template<typename ResultType>
class Awaitable : public std::enable_shared_from_this<Awaitable<ResultType>>{
    public:
        Awaitable();
        Awaitable(std::future<ResultType>&);
        std::shared_ptr<Awaitable<ResultType>> __iter__();
        std::shared_ptr<Awaitable<ResultType>> __await__();
        
        void __next__();

    private:
        std::future<ResultType> future;
};

is this something that would have a chance to land in pybind11? So, supporting an interface like:

m.def_async("name", &function_returning_a_future);

What seems to be missing for this is a StopIteration exception type with templated "return" type.

hiaselhans avatar Nov 13 '20 14:11 hiaselhans

What would need to happen in pybind11 for this? Could this e.g. work as some kind of extension in a py::async_class in a separate <pybind11/async.h> header? (or project, even; depends a bit on how much code/maintenance this would be)

YannickJadoul avatar Nov 13 '20 15:11 YannickJadoul

thx for the quick answer @YannickJadoul!

I wanted to see first how you would agree with it, but i can get my head around it and create a PR for it. It would definitely take me some time but my wish would be to have .def_async for both: instance methods and python methods. They would require a function to return an std::future and python would receive an awaitable.

It's not a big deal to implement it in a few lines of c++, but with python (and other languages) moving more and more towards using async/await it could be a good feature to provide.

From your comment i read that you prefer to have it separate from py::class and module.def?

hiaselhans avatar Nov 13 '20 18:11 hiaselhans

I wanted to see first how you would agree with it, but i can get my head around it and create a PR for it.

It's not a big deal to implement it in a few lines of c++, but with python (and other languages) moving more and more towards using async/await it could be a good feature to provide.

Not the only one going over this, btw, but if there are use cases for this, and it can be done cleanly & without overhead (but runtime as well as binary size), then I don't see why this wouldn't be welcome! :-)

instance methods and python methods

How do you mean, exactly? I'm not sure I see the distinction.

From your comment i read that you prefer to have it separate from py::class and module.def?

Possibly. Just curious how easy this would be. Not per se for this specifically, but in general, it would be strive towards making things as extensible as possible, such that py::class_ doesn't end up with dozens of methods, and it would probably help to make things "opt-in"?

YannickJadoul avatar Nov 13 '20 23:11 YannickJadoul

after some tries, i think, for now, there is one main pitfall for extensibility:

class_ &def( always downcasts to class_...

hiaselhans avatar Nov 15 '20 11:11 hiaselhans

after some tries, i think, for now, there is one main pitfall for extensibility:

class_ &def( always downcasts to class_...

That's a pity indeed. There should be some way of doing this (CRTP?), but ... already interested in what you have, actually. That might make it easier to discuss (and maybe find solutions for this extensible approach; or maybe we find out that's not very necessary after all).

YannickJadoul avatar Nov 15 '20 13:11 YannickJadoul

I think I'd be interested in helping out with this, but I think the asyncio support should be decoupled from the threading support, as it is in Python.

If the primary interest is in threading support, then it would make sense to first target compatibility with concurrent.futures, and wrap the thread Future in an asyncio.Future at the Python level, as normal.

Separate Awaitable support (for classes) has almost no overlap in the pybind-level implementation, and could be done as extensions to class_<>. Extra template parameters and/or annotations might be necessary so that def() doesn't have to downgrade the class, but this should only be an issue if we actually use a C API implementation (e.g. tp_as_async.am_await). No updates are needed to just .def("__await__", ...) in a current pybind::class_. (It might be helpful to have a boilerplate example of implementing __await__. As a separate feature, pybind could add a variant of py::make_iterator that uses a callable to make a "generator" in terms of PyCallIter_New.)

Thirdly and fourthly, async free functions and methods just need a little bit of alteration to def(). Precedents like def_buffer() and def_property() indicate that def_async() may be good for clarity and ease-of-implementation / maintainability, but I think this is very analogous to the annotations like call_guard.

Fifthly, an adapter to std::thread, std::promise, and std::future doesn't have to be coupled to asyncio or concurrent.futures for a simple case that blocks (either once, yielding the GIL, or at the second result() step of the protocol). Again, this isn't really necessary because you can just call the asyncio.loop.run_in_executor() for any regular pybind-exposed callable. But maybe it would be another handy call_guard like path to apply an additional wrapper.

Finally, let's remember that an asyncio Future is an extra layer on top of a coroutine object. Pybind could handle one, the other, or both. I'm tempted to say it would be more useful for pybind to let you do the equivalent of async def and explicitly wrap the callable in a factory that produces a true PEP 492 coroutine object using the C API and (at least allowing for future) handling of the additional throw(), send() and close() methods. It can always be wrapped in a asyncio.Future or asyncio.Task. I don't think there is any reason to reimplement or extend one of the native Future classes unless there is a use case for adding a special pybind future_factory for the asyncio event loop (for some sort of bridge in the other direction, maybe? if it is too hard to wait on Python Futures from C++ for some reason?) or as part of implementing a new event loop or executor.

Is anyone planning to work on this any time soon? Does anyone have any priorities, requests, or suggestions if I were to submit some PRs as I describe?

eirrgang avatar Feb 25 '22 12:02 eirrgang

I would definitely be interested in support for async stuff in pybind11. I am regularly using async/await in python, and also have some experience with C++20's coroutines (co_await and the like).

There are a number of things that we should consider in designing this feature:

  • There are multiple "competing" (in a good way) event loop implementations in python. I personally am a huge fan of trio and don't use anything else. It would be shameful if this feature were to be tied to asyncio only.
  • It would be great if there were support for C++20 coroutines. Ideally, they would work transparently both ways (i.e. calling C++20 coroutines using await from python, and calling python async functions using co_await from C++).
  • Python's async has nothing to do with threading. Shouldn't pybind's support for that also be free from ties to anything thread related? std::future should interface with concurrent.futures - but again, that has nothing to do with async.

burnpanck avatar Mar 24 '22 18:03 burnpanck

the best possible way, i think, is to support std::async / std::future in the bindings

https://en.cppreference.com/w/cpp/thread/async https://en.cppreference.com/w/cpp/thread/future

if the function binding is std::async then the appropriate wrapper should be generated by pybind11 to obtain the std::future at the function call time, and return a wrapped __await__able object to the caller

this should be fairly simple and should work for all scenarios

the c++ user can then opt to use a queue or a pool or coroutine or whatever they want to execute the function at call time

python's async is a single-threaded queue, but there should no limits on the c++ implementation

earonesty avatar Dec 09 '22 16:12 earonesty