Skip to content

Rust 与 Python 混合编程

前言

类似 pydantic-coreruff 这类的库都使用了 Python + Rust 开发的方式。Rust 可用于加速和扩展 Python 功能,本文提供了一些 Python 和 Rust 混合编程的示例。

1. IDE 配置

1.1 RustRover

可以在 RustRover 中安装插件 Python Community Edition 来提供对 Python 的支持。

1.2 VS Code

在 VS Code 中,安装 Python 和 Rust 对应的插件即可:

bash
# Python 语言支持
code --install-extension ms-python.python
# Ruff 格式化
code --install-extension charliermarsh.ruff
# Rust 语言支持
code --install-extension rust-lang.rust-analyzer
# Rust 语法高亮
code --install-extension dustypomerleau.rust-syntax
# TOML 文件支持
code --install-extension tamasfe.even-better-toml
# 依赖检查
code --install-extension fill-labs.dependi

2. Python 调用 Rust

2.1 PyO3

Python 调用 Rust 代码是通过 PyO3 来实现的。

首先,创建一个全新的 Rust 项目,项目类型为库,将 pyo3 添加到依赖:

toml
[package]
name = "rust-python-01"
version = "0.1.0"
edition = "2021"

[lib]
name = "rust_core"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.23.3", features = ["extension-module"] }

Cargo.toml 文件中,crate-type 设置为 cdylib,表示生成动态链接库。

编写 src/lib.rs 文件:

rust
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

/// Adds two numbers
/// # Arguments
/// * `x` - The first number
/// * `y` - The second number
#[pyfunction]
fn add(x: i32, y: i32) -> PyResult<i32> {
    Ok(x + y)
}

#[pymodule]
fn rust_core(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(add, m)?)?;
    Ok(())
}

这是一个简单的 Rust 函数,接收两个整数,返回它们的和。

将其编译为动态库文件:

bash
cargo build --release

编译后得到 target/release/rust_core.dll 文件(Windows 下),然后将 rust_core.dll 文件复制到 Python 项目的目录下,重命名为 rust_core.pyd

编写测试代码:

python
import rust_core

def main():
    print(rust_core.add(1, 2))

if __name__ == '__main__':
    main()

运行测试代码,输出 3,表示 Python 成功调用了 Rust 代码。

2.2 进阶示例

如果没有类型提示,则可以编写存根文件 rust_core.pyi

python
def add(x: int, y: int) -> int:
    """
    Adds two numbers
    :param x: The first number
    :param y: The second number
    :return: sum of the two numbers
    """

我们也可以输出 rust_core.add.__doc__ 来查看函数的文档注释,这和 Rust 代码中的文档注释是一致的。

下面,我们在 src/lib.rs 添加一个函数,计算矩阵中所有数字的和:

rust
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

/// Adds two numbers
/// # Arguments
/// * `x` - The first number
/// * `y` - The second number
#[pyfunction]
fn add(x: i32, y: i32) -> PyResult<i32> {
    Ok(x + y)
}

/// Sums all the elements in a matrix//
/// # Arguments//
/// * `matrix` - A 2D matrix//
#[pyfunction]
fn sum_matrix(matrix: Vec<Vec<i32>>) -> PyResult<i32> {
    let mut sum = 0;
    for row in matrix {
        for val in row {
            sum += val;
        }
    }
    Ok(sum)
}

#[pymodule]
fn rust_core(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(add, m)?)?;
    m.add_function(wrap_pyfunction!(sum_matrix, m)?)?;
    Ok(())
}

rust_core.pyi 中添加存根:

python
def sum_matrix(matrix: List[List[int]]) -> int:
    """
    Sums all the elements in a matrix
    :param matrix: The matrix
    :return: sum of all elements
    """

测试代码:

python
import rust_core

def main():
    print(rust_core.sum_matrix([[1, 2], [3, 4]]))

if __name__ == "__main__":
    main()

2.3 传递哈希表

同样的,我们编写一个函数,将邻接矩阵转换为邻接表:

rust
/// Converts adjacency matrix to adjacency map
/// # Arguments
/// * `matrix` - A 2D matrix
#[pyfunction]
fn matrix_to_map(matrix: Vec<Vec<i32>>) -> PyResult<HashMap<i32, HashSet<i32>>> {
    let mut hashmap = HashMap::new();
    for (i, row) in matrix.iter().enumerate() {
        for (j, &value) in row.iter().enumerate() {
            if value != 0 {
                hashmap.entry(i as i32)
                    .or_insert_with(HashSet::new)
                    .insert(j as i32);
            }
        }
        }
    Ok(hashmap)
}

然后可以使用 Python 调用这个函数:

python
import rust_core

def main():
    print(rust_core.matrix_to_map([[0, 1, 0], [1, 0, 1], [0, 1, 0]]))

if __name__ == "__main__":
    main()

3. 使用 Maturin

3.1 创建项目

Maturin 是使用 pyo3cffiuniffi 绑定以及 Rust 二进制文件构建和发布 Crates 作为 Python 包。

我们可以使用 maturin 来构建 Python 包,首先我们可以全局安装 maturin

bash
uv tool install maturin

我们创建一个全新的 Python + Rust 项目:

bash
maturin new rust-python-02

然后选择 pyo3 类型,接着我们创建虚拟环境:

bash
uv venv

下面进入虚拟环境即可,可以先安装项目:

bash
uv sync

下面新建 main.py 文件进行测试:

python
import rust_python_02

def main():
    print(rust_python_02.sum_as_string(1, 2))

if __name__ == "__main__":
    main()

输出 "3" 代表成功。

如果我们希望改动 Rust 代码并将其重新安装到 Python 环境中,可以使用 maturin develop,如果你不使用 uv,可以去掉 --uv 参数:

bash
maturin develop --uv

这样就可以在 Python 中调用最新的 Rust 代码了。

3.2 在 Rust 中使用 Python

我们有如下 Python 代码,用于生成一个 UUID:

python
import uuid

uuid_obj = uuid.uuid4()
print("uuid:", uuid_obj)

如果我们希望在 Rust 中调用 Python 模块来生成一个 UUID,可以使用 PyO3 提供的 Python 模块。

下面,创建一个 src/main.rs 文件:

rust
use pyo3::prelude::*;

fn main() {
    let res: Result<String, PyErr> = Python::with_gil(|py| {
        let uuid = py.import("uuid")?;
        let uuid_obj = uuid.call_method0("uuid4")?;
        Ok(uuid_obj.str()?.to_string())
    });
    match res {
        Ok(uuid) => println!("uuid: {}", uuid),
        Err(e) => println!("Error: {}", e),
    }
}

然后将编译目标添加到 Cargo.toml 文件:

toml
[[bin]]
name = "rust_python_test"
path = "src/main.rs"

编译并运行:

bash
cargo run

可以看到输出一个 UUID。