#python-module #generated-bindings #python #proc-macro #pyo3 #bindings-generator #bindgen

pyo3_bindgen_macros

用于自动生成 Rust 与 Python 模块绑定的过程宏

7 个版本 (4 个重大更改)

0.5.0 2024年5月9日
0.4.0 2024年3月8日
0.3.1 2024年3月5日
0.2.0 2024年1月23日
0.1.1 2024年1月20日

#24 in #python-module


pyo3_bindgen 中使用

MIT/Apache 协议

205KB
4K SLoC

pyo3_bindgen

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

通过 PyO3 自动生成 Rust FFI 与 Python 模块的绑定。Python 模块将被递归分析以生成与所有公共类、函数、属性和常量结构相同的 Rust 绑定。任何可用的文档字符串和类型注释也将保留在其 Rust 等价物中。

以下是一个生成的 Rust 函数签名及其预期用法的示例。当然,手动将生成的绑定部分包裹在更符合 Rust 习惯的 API 中可能在大多数情况下都是有益的。

源代码 (Python) 生成的代码 (Rust)
 
def answer_to(question: str) -> int:
  """Returns answer to a question."""

  return 42

 

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


if __name__ == "__main__":
  main()
 
/// Returns answer to a question.
pub fn answer_to<'py>(
  py: ::pyo3::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(())
  })
}

此项目旨在简化现有 Python 代码库与 Rust 的集成或过渡。您作为开发者,通过生成的绑定,立即获得 Rust 类型系统和现代编译语言的其他众多好处。此外,crates.io 上的所有高质量 crate 也都可供您使用。

生成的 Rust 代码本身并不提供比使用 Python 代码任何性能优势。然而,如果您决定用纯 Rust 重新编写代码库中性能关键的部分,它可以用作进一步优化的起点。

【注意】0.5 版本的 pyo3_bindgen 适配了在 pyo3 版本 0.21 中引入的新智能指针 pyo3::Bound<'py, T>。如果您需要旧的 "GIL Refs" API,请使用 0.4 版本的 pyo3_bindgen

概述

工作区包含以下包

pyo3_bindgen 的功能

  • macros [实验性]: 启用 import_python! 宏,该宏来自 pyo3_bindgen_macros
  • numpy [实验性]: 启用 Python numpy::ndarray 和 Rust numpy::PyArray 之间的类型映射

说明

选项 1:构建脚本

首先,将 pyo3_bindgen 添加到您的 Cargo.toml 清单中的 构建依赖项。要实际使用生成的绑定,您还需要将 pyo3 添加为常规依赖项(或使用重导出的 pyo3_bindgen::pyo3 模块)。

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

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

然后,在您的包根目录中创建一个 build.rs 脚本,生成所选 Python 模块的绑定。在本例中,同时为 "os","posixpath" 和 "sys" Python 模块生成绑定。在生成过程结束时,Rust 绑定将写入 ${OUT_DIR}/bindings.rs

【提示】使用此方法,您还可以通过传递给构造函数的 pyo3_bindgen::Config 来自定义生成过程,例如 Codegen::new(Config::builder().include_private(true).build()) 来自定义生成过程。

//! build.rs
use pyo3_bindgen::Codegen;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    Codegen::default()
        .module_names(["os", "posixpath", "sys"])?
        .build(format!("{}/bindings.rs", std::env::var("OUT_DIR")?))?;
    Ok(())
}

之后,您可以通过宏 include! 在 crate 的任何地方包含生成的 Rust 代码,并像使用常规 Rust 模块一样使用生成的绑定。但是,绑定必须在 pyo3::Python::with_gil 闭包中使用,以确保 Python GIL 被持有。

//! src/main.rs
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

fn main() -> pyo3::PyResult<()> {
    pyo3::Python::with_gil(|py| {
        // Get the path to the Python executable via "sys" Python module
        let python_exe_path = sys::executable(py)?;
        // Get the current working directory via "os" Python module
        let current_dir = os::getcwd(py)?;
        // Get the relative path to the Python executable via "posixpath" Python module
        let relpath_to_python_exe = posixpath::relpath(py, python_exe_path, current_dir)?;

        println!("Relative path to Python executable: '{relpath_to_python_exe}'");
        Ok(())
    })
}

选项 2:过程宏(实验性)

作为构建脚本的替代方案,您可以使用过程宏就地生成绑定。首先,将 pyo3_bindgen_macros 添加为 常规依赖 到您的 Cargo.toml 清单中,并启用 macros 功能。

[dependencies]
pyo3_bindgen = { version = "0.5", features = ["macros"] }

随后,可以在 crate 的任何地方使用宏 import_python! 生成所选 Python 模块的 Rust 绑定。如下例所示,为 "math" Python 模块生成了 Rust 绑定,可以直接在同一作用域中使用。与之前的方法类似,生成的绑定必须在 pyo3::Python::with_gil 闭包中使用,以确保 Python GIL 被持有。

[!NOTE] 与使用构建脚本不同,这种方法不提供通过 pyo3_bindgen::Config 进行相同级别的自定义。此外,过程宏相当实验性,可能不会在所有情况下都工作。

use pyo3_bindgen::import_python;
import_python!("math");

// Which Pi do you prefer?
// a) 🐍 Pi from Python "math" module
// b) 🦀 Pi from Rust standard library
// c) 🥧 Pi from your favourite bakery
pyo3::Python::with_gil(|py| {
    let python_pi = math::pi(py).unwrap();
    let rust_pi = std::f64::consts::PI;
    assert_eq!(python_pi, rust_pi);
})

选项 3:CLI 工具

为了快速开始和测试,您可以使用 pyo3_bindgen 可执行文件生成和检查所选 Python 模块的绑定。该可执行文件作为一个独立包提供,可以通过 cargo 安装。

cargo install --locked pyo3_bindgen_cli

之后,运行 pyo3_bindgen 可执行文件以生成所选 Python 模块的 Rust 绑定。默认情况下,生成的绑定打印到 STDOUT,但也可以通过 -o 选项写入文件(有关更多信息,请参阅 pyo3_bindgen --help)。

pyo3_bindgen -m os sys numpy -o bindings.rs

状态

该项目处于早期开发阶段,因此生成的绑定 API 还未稳定。

  • 绑定生成主要设计用于在构建脚本或通过过程宏中使用。因此,对代码生成过程的性能进行了基准测试,以了解其对构建时间可能产生的影响。以下是针对版本 0.3 和默认配置的一些初步结果(未测量:生成的绑定的编译,这需要更长的时间)
    • sys: 1.24 毫秒(0.66k 总 LoC)
    • os: 8.38 毫秒(3.88k 总 LoC)
    • numpy: 1.02 秒(294k 总 LoC)
    • torch: 7.05 秒(1.08M 总 LoC)
  • 只要目标 Python 模块可以成功导入,绑定生成永远不会崩溃。如果确实发生了这种情况,请 报告 作为错误。
  • 生成的绑定应该始终是可编译和可用的 Rust。如果您遇到任何问题,请考虑手动修复绑定的问题部分,并请报告此问题作为错误。
  • 然而,生成的绑定是基于目标 Python 模块的反射。因此,生成的绑定的完整性和正确性直接取决于目标 Python 模块的结构、类型注释和文档字符串的质量。理想情况下,生成的绑定应被视为不安全的,并作为安全且惯用的 Rust API 的起点。如果您发现生成的绑定中存在不正确或缺失的内容,请报告此问题。
  • 并非所有 Python 类型都已映射到其 Rust 等价类型。因此,在使用生成的绑定时可能需要进行一些额外的类型转换(例如:let typed_value: MyType = any_value.extract()?;)。
  • 虽然实现了过程宏,但它在许多情况下可能不起作用。因此,建议尽可能使用构建脚本。

许可证

本项目双许可,以与 Rust 项目兼容,可使用MITApache 2.0许可证。

贡献

除非您明确声明,否则任何有意提交以包含在您的工作中的贡献(根据 Apache-2.0 许可证定义),均应双许可,如上所述,不附加任何额外条款或条件。

依赖项

~4–10MB
~96K SLoC