pyo3_bindgen
Automatic generation of Rust FFI bindings to Python modules via PyO3. Python modules are analyzed recursively to generate Rust bindings with an identical structure for all public classes, functions, properties, and constants. Any available docstrings and type annotations are also preserved in their Rust equivalents.
An example of a generated Rust function signature and its intended usage is shown below. Of course, manually wrapping parts of the generated bindings in a more idiomatic Rust API might be beneficial in most cases.
"""Returns answer to question."""
return 42
assert == 42
/// Returns answer to question.
This project is intended to simplify the integration or transition of existing Python codebases into Rust. You, as a developer, gain immediate access to the Rust type system and countless other benefits of modern compiled languages with the generated bindings. Furthermore, the entire stock of high-quality crates from crates.io becomes at your disposal.
On its own, the generated Rust code does not provide any performance benefits over using the Python code (it might actually be slower — yet to be benchmarked). However, it can be used as a starting point for further optimization if you decide to rewrite performance-critical parts of your codebase in pure Rust.
Overview
The workspace contains these packages:
- pyo3_bindgen: Public API for generation of bindings (in
build.rsscripts or via procedural macros if enabled) - pyo3_bindgen_cli: CLI tool for generation of bindings via
pyo3_bindgenexecutable - pyo3_bindgen_engine: The underlying engine for generation of bindings
- pyo3_bindgen_macros: [Experimental] Procedural macros for in-place generation
Instructions
Add pyo3 as a dependency and pyo3_bindgen as a build dependency to your Cargo.toml manifest (auto-initialize feature of pyo3 is optional and shown here for your convenience).
[]
= { = "0.20", = ["auto-initialize"] }
[]
= { = "0.1" }
Option 1: Build script
Create a build.rs script in the root of your crate that generates bindings to the target_module Python module.
// build.rs
Afterwards, include the generated bindings anywhere in your crate.
Option 2: CLI tool
Install the pyo3_bindgen executable with cargo.
Afterwards, run the pyo3_bindgen executable while passing the name of the target Python module.
# Pass `--help` to show the usage and available options
Option 3 [Experimental]: Procedural macros
Note: This feature is experimental and will probably fail in many cases. It is recommended to use build scripts instead.
Enable the macros feature of pyo3_bindgen.
[]
= { = "0.1", = ["macros"] }
Then, you can call the import_python! macro anywhere in your crate.
pub
Status
This project is in early development, and as such, the API of the generated bindings is not yet stable.
- Not all Python types are mapped to their Rust equivalents yet. Especially the support for mapping types of module-wide classes for which bindings are generated is also still missing. For this reason, a lot of boilerplate might be currently required when using the generated bindings (e.g.
let typed_value: target_module::Class = any_value.extract()?;). - The binding generation is primarily designed to be used inside build scripts or via procedural macros. Therefore, the performance of the codegen process is benchmarked to understand the potential impact on build times. Surprisingly, even the initial unoptimized version of the engine is able to process the entire
numpy1.26.3 in ~300 ms on a modern laptop while generating 166k lines of formatted Rust code (line count includes documentation). Adding more features might increase this time, but there is also plenty of room for optimization in the current naive implementation. - The generation of bindings should never panic as long as the target Python module can be successfully imported. If it does, it is a bug resulting from an unexpected edge-case Python module structure or an unforeseen combination of enabled PyO3 features.
- Although implemented, the procedural macros might not work in all cases - especially when some PyO3 features are enabled. In most cases, PyO3 fails to import the target Python module when used from within a
proc_macrocrate. Therefore, it is recommended to use build scripts instead for now. - The code will be refactored and cleaned up in the upcoming releases. The current implementation is a result of a very quick prototype that was built to test the feasibility of the idea.
Please report any issues that you might encounter. Contributions are more than welcome! If you are looking for a place to start, consider searching for TODO comments in the codebase.
License
This project is dual-licensed to be compatible with the Rust project, under either the MIT or Apache 2.0 licenses.
Contributing
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.