Crate pyo3_asyncio[−][src]
Expand description
Rust Bindings to the Python Asyncio Event Loop
Motivation
This crate aims to provide a convenient interface to manage the interop between Python and Rust’s async/await models. It supports conversions between Rust and Python futures and manages the event loops for both languages. Python’s threading model and GIL can make this interop a bit trickier than one might expect, so there are a few caveats that users should be aware of.
Why Two Event Loops
Currently, we don’t have a way to run Rust futures directly on Python’s event loop. Likewise, Python’s coroutines cannot be directly spawned on a Rust event loop. The two coroutine models require some additional assistance from their event loops, so in all likelihood they will need a new unique event loop that addresses the needs of both languages if the coroutines are to be run on the same loop.
It’s not immediately clear that this would provide worthwhile performance wins either, so in the interest of keeping things simple, this crate creates and manages the Python event loop and handles the communication between separate Rust event loops.
Python’s Event Loop
Python is very picky about the threads used by the asyncio
executor. In particular, it needs
to have control over the main thread in order to handle signals like CTRL-C correctly. This
means that Cargo’s default test harness will no longer work since it doesn’t provide a method of
overriding the main function to add our event loop initialization and finalization.
Event Loop References
One problem that arises when interacting with Python’s asyncio library is that the functions we use to get a reference to the Python event loop can only be called in certain contexts. Since PyO3 Asyncio needs to interact with Python’s event loop during conversions, the context of these conversions can matter a lot.
The core conversions we’ve mentioned so far in this guide should insulate you from these concerns in most cases, but in the event that they don’t, this section should provide you with the information you need to solve these problems.
The Main Dilemma
Python programs can have many independent event loop instances throughout the lifetime of the application (asyncio.run
for example creates its own event loop each time it’s called for instance), and they can even run concurrent with other event loops. For this reason, the most correct method of obtaining a reference to the Python event loop is via asyncio.get_running_loop
.
asyncio.get_running_loop
returns the event loop associated with the current OS thread. It can be used inside Python coroutines to spawn concurrent tasks, interact with timers, or in our case signal between Rust and Python. This is all well and good when we are operating on a Python thread, but since Rust threads are not associated with a Python event loop, asyncio.get_running_loop
will fail when called on a Rust runtime.
The Solution
A really straightforward way of dealing with this problem is to pass a reference to the associated Python event loop for every conversion. That’s why in v0.14
, we introduced a new set of conversion functions that do just that:
pyo3_asyncio::into_future_with_loop
- Convert a Python awaitable into a Rust future with the given asyncio event loop.pyo3_asyncio::<runtime>::future_into_py_with_loop
- Convert a Rust future into a Python awaitable with the given asyncio event loop.pyo3_asyncio::<runtime>::local_future_into_py_with_loop
- Convert a!Send
Rust future into a Python awaitable with the given asyncio event loop.
One clear disadvantage to this approach (aside from the verbose naming) is that the Rust application has to explicitly track its references to the Python event loop. In native libraries, we can’t make any assumptions about the underlying event loop, so the only reliable way to make sure our conversions work properly is to store a reference to the current event loop at the callsite to use later on.
use pyo3::{wrap_pyfunction, prelude::*}; #[pyfunction] fn sleep(py: Python) -> PyResult<&PyAny> { let current_loop = pyo3_asyncio::get_running_loop(py)?; let loop_ref = PyObject::from(current_loop); // Convert the async move { } block to a Python awaitable pyo3_asyncio::tokio::future_into_py_with_loop(current_loop, async move { let py_sleep = Python::with_gil(|py| { // Sometimes we need to call other async Python functions within // this future. In order for this to work, we need to track the // event loop from earlier. pyo3_asyncio::into_future_with_loop( loop_ref.as_ref(py), py.import("asyncio")?.call_method1("sleep", (1,))? ) })?; py_sleep.await?; Ok(Python::with_gil(|py| py.None())) }) } #[pymodule] fn my_mod(py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(sleep, m)?)?; Ok(()) }
A naive solution to this tracking problem would be to cache a global reference to the asyncio event loop that all PyO3 Asyncio conversions can use. In fact this is what we did in PyO3 Asyncio
v0.13
. This works well for applications, but it soon became clear that this is not so ideal for libraries. Libraries usually have no direct control over how the event loop is managed, they’re just expected to work with any event loop at any point in the application. This problem is compounded further when multiple event loops are used in the application since the global reference will only point to one.
Another disadvantage to this explicit approach that is less obvious is that we can no longer call our #[pyfunction] fn sleep
on a Rust runtime since asyncio.get_running_loop
only works on Python threads! It’s clear that we need a slightly more flexible approach.
In order to detect the Python event loop at the callsite, we need something like asyncio.get_running_loop
that works for both Python and Rust. In Python, asyncio.get_running_loop
uses thread-local data to retrieve the event loop associated with the current thread. What we need in Rust is something that can retrieve the Python event loop associated with the current task.
Enter pyo3_asyncio::<runtime>::get_current_loop
. This function first checks task-local data for a Python event loop, then falls back on asyncio.get_running_loop
if no task-local event loop is found. This way both bases are covered.
Now, all we need is a way to store the event loop in task-local data. Since this is a runtime-specific feature, you can find the following functions in each runtime module:
pyo3_asyncio::<runtime>::scope
- Store the event loop in task-local data when executing the given Future.pyo3_asyncio::<runtime>::scope_local
- Store the event loop in task-local data when executing the given!Send
Future.
With these new functions, we can make our previous example more correct:
use pyo3::prelude::*; #[pyfunction] fn sleep(py: Python) -> PyResult<&PyAny> { // get the current event loop through task-local data // OR `asyncio.get_running_loop` let current_loop = pyo3_asyncio::tokio::get_current_loop(py)?; pyo3_asyncio::tokio::future_into_py_with_loop( current_loop, // Store the current loop in task-local data pyo3_asyncio::tokio::scope(current_loop.into(), async move { let py_sleep = Python::with_gil(|py| { pyo3_asyncio::into_future_with_loop( // Now we can get the current loop through task-local data pyo3_asyncio::tokio::get_current_loop(py)?, py.import("asyncio")?.call_method1("sleep", (1,))? ) })?; py_sleep.await?; Ok(Python::with_gil(|py| py.None())) }) ) } #[pyfunction] fn wrap_sleep(py: Python) -> PyResult<&PyAny> { // get the current event loop through task-local data // OR `asyncio.get_running_loop` let current_loop = pyo3_asyncio::tokio::get_current_loop(py)?; pyo3_asyncio::tokio::future_into_py_with_loop( current_loop, // Store the current loop in task-local data pyo3_asyncio::tokio::scope(current_loop.into(), async move { let py_sleep = Python::with_gil(|py| { pyo3_asyncio::into_future_with_loop( pyo3_asyncio::tokio::get_current_loop(py)?, // We can also call sleep within a Rust task since the // event loop is stored in task local data sleep(py)? ) })?; py_sleep.await?; Ok(Python::with_gil(|py| py.None())) }) ) } #[pymodule] fn my_mod(py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(sleep, m)?)?; m.add_function(wrap_pyfunction!(wrap_sleep, m)?)?; Ok(()) }
Even though this is more correct, it’s clearly not more ergonomic. That’s why we introduced a new set of functions with this functionality baked in:
pyo3_asyncio::<runtime>::into_future
Convert a Python awaitable into a Rust future (using
pyo3_asyncio::<runtime>::get_current_loop
)pyo3_asyncio::<runtime>::future_into_py
Convert a Rust future into a Python awaitable (using
pyo3_asyncio::<runtime>::get_current_loop
andpyo3_asyncio::<runtime>::scope
to set the task-local event loop for the given Rust future)pyo3_asyncio::<runtime>::local_future_into_py
Convert a
!Send
Rust future into a Python awaitable (usingpyo3_asyncio::<runtime>::get_current_loop
andpyo3_asyncio::<runtime>::scope_local
to set the task-local event loop for the given Rust future).
These are the functions that we recommend using. With these functions, the previous example can be rewritten to be more compact:
use pyo3::prelude::*; #[pyfunction] fn sleep(py: Python) -> PyResult<&PyAny> { pyo3_asyncio::tokio::future_into_py(py, async move { let py_sleep = Python::with_gil(|py| { pyo3_asyncio::tokio::into_future( py.import("asyncio")?.call_method1("sleep", (1,))? ) })?; py_sleep.await?; Ok(Python::with_gil(|py| py.None())) }) } #[pyfunction] fn wrap_sleep(py: Python) -> PyResult<&PyAny> { pyo3_asyncio::tokio::future_into_py(py, async move { let py_sleep = Python::with_gil(|py| { pyo3_asyncio::tokio::into_future(sleep(py)?) })?; py_sleep.await?; Ok(Python::with_gil(|py| py.None())) }) } #[pymodule] fn my_mod(py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(sleep, m)?)?; m.add_function(wrap_pyfunction!(wrap_sleep, m)?)?; Ok(()) }
A Note for v0.13
Users
Hey guys, I realize that these are pretty major changes for v0.14
, and I apologize in advance for having to modify the public API so much. I hope
the explanation above gives some much needed context and justification for all the breaking changes.
Part of the reason why it’s taken so long to push out a v0.14
release is because I wanted to make sure we got this release right. There were a lot of issues with the v0.13
release that I hadn’t anticipated, and it’s thanks to your feedback and patience that we’ve worked through these issues to get a more correct, more flexible version out there!
This new release should address most the core issues that users have reported in the v0.13
release, so I think we can expect more stability going forward.
Also, a special thanks to @ShadowJonathan for helping with the design and review of these changes!
Rust’s Event Loop
Currently only the async-std and Tokio runtimes are supported by this crate.
In the future, more runtimes may be supported for Rust.
Features
Items marked with
attributes
are only available when the attributes
Cargo feature is enabled:
[dependencies.pyo3-asyncio] version = "0.13" features = ["attributes"]
Items marked with
async-std-runtime
are only available when the async-std-runtime
Cargo feature is enabled:
[dependencies.pyo3-asyncio] version = "0.13" features = ["async-std-runtime"]
Items marked with
tokio-runtime
are only available when the tokio-runtime
Cargo feature is enabled:
[dependencies.pyo3-asyncio] version = "0.13" features = ["tokio-runtime"]
Items marked with
testing
are only available when the testing
Cargo feature is enabled:
[dependencies.pyo3-asyncio] version = "0.13" features = ["testing"]
Re-exports
pub use inventory;
Modules
async-std-runtime
PyO3 Asyncio functions specific to the async-std runtime
Errors and exceptions related to PyO3 Asyncio
Generic implementations of PyO3 Asyncio utilities that can be used for any Rust runtime
testing
Utilities for writing PyO3 Asyncio tests
tokio-runtime
PyO3 Asyncio functions specific to the tokio runtime
Functions
Get a reference to the Python event loop cached by try_init
(0.13 behaviour)
Get a reference to the Python Event Loop from Rust
Convert a Python awaitable
into a Rust Future
Convert a Python awaitable
into a Rust Future
Run the event loop forever
Shutdown the event loops and perform any necessary cleanup
Attempt to initialize the Python and Rust event loops
Wraps the provided function with the initialization and finalization for PyO3 Asyncio