Rust Extensions
This page explains how to add your own async Rust code and use it alongside
rsloop in Python.
The idea is:
- Python still uses
rsloopas the event loop - your own PyO3 extension module exposes extra functions
- those functions return Python awaitables backed by Rust futures
rsloop provides a small Rust helper module for this:
rsloop::rust_async::get_current_locals(...)rsloop::rust_async::future_into_py(...)rsloop::rust_async::future_into_py_with_locals(...)rsloop::rust_async::local_future_into_py(...)rsloop::rust_async::local_future_into_py_with_locals(...)rsloop::rust_async::TaskLocalsrsloop::rust_async::into_future_with_locals(...)
When to use this
Use this pattern when:
- you want to keep the main application in Python
- you want some operations implemented in Rust
- those Rust operations should be
await-able from Python - you want them to run under the same active
rslooploop
This is not for modifying rsloop itself. It is for building a separate Rust
extension crate that depends on rsloop.
What the developer builds
You usually create a second crate with:
- a
Cargo.tomlfor your PyO3 extension - a
pyproject.tomlformaturin - Rust functions marked with
#[pyfunction] - a
#[pymodule]that exports those functions
The important part is that each async Rust function should return a Python
awaitable using rsloop::rust_async::future_into_py(...).
Minimal Rust example
use std::time::Duration;
use pyo3::prelude::*;
#[pyfunction]
fn sleep_and_tag(py: Python<'_>, label: String, delay_ms: u64) -> PyResult<Bound<'_, PyAny>> {
rsloop::rust_async::future_into_py(py, async move {
async_std::task::sleep(Duration::from_millis(delay_ms)).await;
Ok(format!("rust finished: {label}"))
})
}
#[pymodule(gil_used = false)]
fn my_rust_ext(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sleep_and_tag, m)?)?;
Ok(())
}
In this example:
- the Python caller gets an awaitable back immediately
- the real work happens in the Rust future
- the future is attached to the currently running Python event loop
Python side usage
From Python, the Rust function looks like a normal async operation:
import asyncio
import rsloop
import my_rust_ext
async def main() -> None:
loop = asyncio.get_running_loop()
print(type(loop).__name__)
result = await my_rust_ext.sleep_and_tag("hello from python", 50)
print(result)
rsloop.run(main())
This works because rsloop.run(...) creates the running loop first, and then
the Rust extension captures that loop when future_into_py(...) is called.
Project setup
Your extension crate should depend on:
[dependencies]
async-std = "1"
pyo3 = { version = "0.28", features = ["extension-module"] }
rsloop = { version = "0.1.14" }
For a real package, replace the path dependency with the form that matches your project layout.
Your pyproject.toml should use maturin in the normal PyO3 way:
[build-system]
requires = ["maturin>=1.7,<2"]
build-backend = "maturin"
[tool.maturin]
module-name = "my_rust_ext"
How the bridge works
future_into_py(...) does two important things:
- It captures the current Python event loop and contextvars.
- It converts the Rust future into a Python awaitable.
That means your Rust future can be awaited from Python code that is already
running on rsloop.
If you need lower-level control, use get_current_locals(...) and
future_into_py_with_locals(...) directly. That is useful when you capture the
loop/context once and reuse it across multiple Rust operations.
Calling Python awaitables from Rust
rsloop also re-exports into_future_with_locals(...).
Use that when your Rust future needs to await a Python awaitable:
use pyo3::prelude::*;
fn call_python_awaitable(
py: Python<'_>,
awaitable: Py<PyAny>,
) -> PyResult<Bound<'_, PyAny>> {
let locals = rsloop::rust_async::get_current_locals(py)?;
rsloop::rust_async::future_into_py_with_locals(py, locals.clone(), async move {
let python_future = Python::attach(|py| {
rsloop::rust_async::into_future_with_locals(&locals, awaitable.bind(py).clone())
})?;
let value = python_future.await?;
Ok(value)
})
}
That pattern is more advanced, but it is useful when Rust is coordinating both Rust futures and Python coroutines.
Full example in this repository
The repository includes a complete example in:
examples/rust/src/lib.rsexamples/rust/demo.pyexamples/rust/README.md
If you want the shortest end-to-end check from the repository root, run:
uv run --with . --with ./examples/rust python examples/rust/demo.py
Practical advice
- Keep the Rust extension separate from the
rsloopcrate itself. - Start with small
#[pyfunction]wrappers that return one awaitable each. - Prefer returning normal Python-friendly values such as strings, integers, dictionaries, and lists.
- Use
future_into_py(...)first. Drop to the*_with_locals(...)variants only when you actually need them. - Make sure your Rust function is called while a Python event loop is already running.
If you call the helper outside a running event loop, capturing the current loop will fail, because there is no active Python loop to attach to.