Skip to content

Rust编写Python扩展

https://github.com/PyO3/pyo3

pyo3.rs

简单示例

新建 pyo3_test 文件夹,在文件夹下进行操作

执行 cargo new --lib simple_calc 新建 simple_calc 文件夹:

修改 simple_calc/Cargo.toml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[package]
name = "simple_calc"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

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

[dependencies.pyo3]
version = "0.13.2"
features = ["extension-module"]

修改 simple_calc/src/lib.rs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyfunction]
fn add(a: usize, b: usize) -> PyResult<usize> {
    Ok(a + b)
}

#[pyfunction]
fn hello() -> PyResult<()> {
    println!("Hello world!");
    Ok(())
}

/// A Python module implemented in Rust.
#[pymodule]
fn simple_calc(py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(add, m)?)?;
    m.add_function(wrap_pyfunction!(hello, m)?)?;
    Ok(())
}

On Windows and Linux, you can build normally with cargo build --release.
On macOS, you need to set additional linker arguments.
One option is to compile with cargo rustc --release -- -C link-arg=-undefined -C link-arg=dynamic_lookup

我是在 linux 下测试的,所以在 simple_calc 目录下执行 cargo build --release

While developing, you can symlink (or copy) and rename the shared library from the target folder:
On MacOS, rename libstring_sum.dylib to string_sum.so,
on Windows libstring_sum.dll to string_sum.pyd, and
on Linux libstring_sum.so to string_sum.so.
Then open a Python shell in the same folder and you'll be able to import string_sum.

在 pyo3_test 目录下新建 py_run 文件夹

simple_calc/target/release/libsimple_calc.so 复制到 py_run 文件夹下

py_run 文件夹下的 libsimple_calc.so 重命名为 simple_calc.so

py_run 文件夹下新建 test.py:

1
2
3
4
import simple_calc

simple_calc.hello()
print(simple_calc.add(1, 2))

执行 python test.py 输出结果:

1
2
Hello world!
3

对象

lib.rs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyclass]
struct MyClass {
    num: i32,
}

#[pymethods]
impl MyClass {
    #[new]
    fn new(num: i32) -> Self {
        MyClass { num }
    }
    pub fn add(&self, a: i32, b: i32) -> PyResult<i32> {
        Ok(self.num + a + b)
    }
}

#[pymodule]
fn trade_rs(py: Python, m: &PyModule) -> PyResult<()> {
    m.add_class::<MyClass>()?;
    Ok(())
}
1
2
3
4
import trade_rs

c = trade_rs.MyClass(1)
print(c.add(1, 2))

输出:

1
4

异步

Cargo.toml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[package]
name = "trade_rs"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

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

[dependencies]
async-std = "1.9.0"
pyo3-asyncio = { version = "0.13.3", features = ["async-std-runtime"] }

[dependencies.pyo3]
version = "0.13.2"
features = ["extension-module"]

src/lib.rs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
use std::time::Duration;

async fn rust_sleep() {
    async_std::task::sleep(Duration::from_secs(10)).await;
}

#[pyfunction]
fn call_rust_sleep(py: Python) -> PyResult<PyObject> {
    pyo3_asyncio::async_std::into_coroutine(py, async move {
        println!("await sleep 10s");
        rust_sleep().await;
        println!("finish");
        Python::with_gil(|py| Ok(py.None()))
    })
}

/// A Python module implemented in Rust.
#[pymodule]
fn trade_rs(py: Python, m: &PyModule) -> PyResult<()> {
    pyo3_asyncio::try_init(py);
    m.add_function(wrap_pyfunction!(call_rust_sleep, m)?)?;
    Ok(())
}
1
2
3
4
5
6
7
8
9
import trade_rs
import asyncio


async def run():
    await trade_rs.call_rust_sleep()

loop = asyncio.get_event_loop()
loop.run_until_complete(run())

输出结果:

1
2
await sleep 10s
finish

在 python 包中使用

可以将其复制到与根 __init__.py 同一目录
假设项目名称为 my_test, rust 包名称为 rust_test,使用示例:

将 rust_test.so 放到 my_test 目录下

1
from my_test import rust_test

rust 异步返回 json 字符串

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#[derive(Deserialize, Serialize)]
pub struct MyClass {
    pub id: i32
}

pub fn get_msg(&self, py: Python) -> PyResult<PyObject> {
    let rx = self.send2py_rx.clone();
    pyo3_asyncio::async_std::into_coroutine(py, async move {
        if let Some(rx) = rx {
            let mut raw_rx = rx.lock().await;
            let msg = raw_rx.next().await;
            let myclass = MyClass { id: 1 };
            let json_class = serde_json::json!(&myclass).to_string();
            Python::with_gil(|py| Ok(PyObject::from(PyString::new(py, &json_class))))
        } else {
            let myclass = MyClass { id: 2 };
            let json_class = serde_json::json!(myclass).to_string();
            Python::with_gil(|py| Ok(PyObject::from(PyString::new(py, &json_class))))
        }
    })
}

如果导出的class中有异步方法,需要对其进行一个同步封装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyclass]
struct SyncClass {
    pub myclass: MyClass,
}

#[pymethods]
impl SyncClass {
    #[new]
    pub fn new(num: i32) -> Self {
        SyncClass { myclass: MyClass { num } }
    }

    pub fn add(&self, a: i32, b: i32) -> i32 {
        self.myclass.num + a + b
    }

    pub fn my_num(&self) -> i32 {
        self.myclass.num
    }
}

struct MyClass {
    num: i32,
}

impl MyClass {
    fn new(num: i32) -> Self {
        MyClass { num }
    }
    pub fn add(&self, a: i32, b: i32) -> PyResult<i32> {
        Ok(self.num + a + b)
    }

    pub async fn async_test(&self) {

    }
}

#[pymodule]
fn hhh(py: Python, m: &PyModule) -> PyResult<()> {
    m.add_class::<SyncClass>()?;
    Ok(())
}
1
2
3
4
5
import hhh

c = hhh.SyncClass(1)
print(c.my_num())
print(c.add(1, 2))

输出:

1
2
1
4