Skip to content

Automatic Python —> Rust FFI bindings

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

AndrejOrsula/pyo3_bindgen

Repository files navigation

pyo3_bindgen

crates.io docs.rs Rust deps.rs codecov.io

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.

Source code (Python) Generated code (Rust)
 
def answer_to(question: str) -> int:
  """Returns answer to question."""

  return 42

 

 
def main():
  assert answer_to("life") == 42


if __name__ == "__main__":
  main()
 
/// Returns answer to question.
pub fn answer_to<'py>(
  py: ::pyo3::marker::Python<'py>,
  question: &str,
) -> ::pyo3::PyResult<i64> {
  ... // Calls function via `pyo3`
}

pub fn main() -> pyo3::PyResult<()> {
  pyo3::Python::with_gil(|py| {
    assert_eq!(
      answer_to(py, "universe")?, 42
    );
    Ok(())
  })
}

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

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

[dependencies]
pyo3 = { version = "0.20", features = ["auto-initialize"] }

[build-dependencies]
pyo3_bindgen = { version = "0.3" }

Option 1: Build script

Create a build.rs script in the root of your crate that generates bindings to the py_module Python module.

// build.rs
use pyo3_bindgen::{Codegen, Config};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Generate Rust bindings to Python modules
    Codegen::new(Config::default())?
        .module_name("py_module")?
        .build(std::path::Path::new(&std::env::var("OUT_DIR")?).join("bindings.rs"))?;
    Ok(())
}

Afterwards, include the generated bindings anywhere in your crate.

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
pub use py_module::*;

Option 2: CLI tool

Install the pyo3_bindgen executable with cargo.

cargo install --locked pyo3_bindgen_cli

Afterwards, run the pyo3_bindgen executable while passing the name of the target Python module.

# Pass `--help` to show the usage and available options
pyo3_bindgen -m py_module -o bindings.rs

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.

[build-dependencies]
pyo3_bindgen = { version = "0.3", features = ["macros"] }

Then, you can call the import_python! macro anywhere in your crate.

pyo3_bindgen::import_python!("py_module");
pub use py_module::*;

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. For this reason, some additional typecasting might be currently required when using the generated bindings (e.g. let typed_value: py_module::MyClass = get_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. Here are some preliminary results for version 0.3.0 with the default configuration (measured: parsing IO & codegen | not measured: compilation of the generated bindings, which takes much longer):
    • sys: 1.24 ms (0.66k total LoC)
    • os: 8.38 ms (3.88k total LoC)
    • numpy: 1.02 s (294k total LoC)
    • torch: 7.05 s (1.08M total LoC)
  • The generation of bindings should never panic as long as the target Python module can be successfully imported. If it does, please report this as a bug.
  • The generated bindings should always be compilable and usable in Rust. If you encounter any issues, consider manually fixing the problematic parts of the bindings and please report this as a bug.
  • However, the generated bindings are based on the introspection of the target Python module. Therefore, the correctness of the generated bindings is directly dependent on the quality of the type annotations and docstrings in the target Python module. Ideally, the generated bindings should be considered unsafe and serve as a starting point for safe and idiomatic Rust APIs.
  • Although implemented, the procedural macro does not work in many cases because PyO3 fails to import the target Python module when used from within a proc_macro crate. Therefore, it is recommended to use build scripts instead for now.

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.