尝试rust与python的混合编程(一)

前言

笔者想试试rust与python的混合编程,笔者去搜索了一下

PyO3 教程:连接 Python 与 Rust 的桥梁PyO3 是一个 Rust 库,它提供了 Rust 和 Pyth - 掘金https://juejin.cn/post/7486766400281231414(35 封私信 / 38 条消息) Python+Rust混合编程 - 知乎https://zhuanlan.zhihu.com/p/1929138818348487898Rust 与 Python 混合编程 | 鸭梨公共文档https://docs.alexsun.top/public/blog/2024-12/rust-maturin.html感觉很好玩,笔者决定尝试一下,官网参考

FAQ and troubleshooting - PyO3 user guidehttps://pyo3.rs/Mapping of Rust types to Python types - PyO3 user guidehttps://pyo3.rs/v0.27.1/conversions/tables.html

正文

初始化项目

笔者初始化项目,笔者python的包管理工具是Rye,至于什么是Rye以及怎么使用这样,笔者直接跳过,而Rye可以初始化rust-python混合编程的项目

rye init rye-rp --build-system maturin

rye sync

就可以初始化一个rust-python混合编程的项目,项目结构如下

可以安装一个关键的依赖maturin

rye add --dev maturin

笔者对cargo.toml文件的依赖做出修改

rust 复制代码
[package]
name = "rye-rp"
version = "0.1.0"
edition = "2024"

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

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

截止到目前最新版是0.27.1。

那么需要修改一下lib.rs里面的内容,修改更新一下,即

rust 复制代码
use pyo3::prelude::*;

#[pyfunction]
fn hello() -> PyResult<String> {
    Ok("Hello from rye-rp!".into())
}

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

在python/rye-rp目录下,新建一个**main.py**文件,或者使用如下代码

python 复制代码
if __name__ == "__main__":
    print(hello())

两种都行,笔者直接在**init.py**写打印语句,直接打印了两次

笔者还懵逼了一下,后面明白了

当直接运行 init.py 时,Python 加载器会让同一个文件被执行 两次,但以不同模块名加载:

第一次:作为 main 模块(直接运行)
第二次:作为 rye_rp.init 模块(被导入时)

总之,结果如下

没问题。

尝试打包

笔者添加一个简单的函数

rust 复制代码
#[pyfunction]
fn add(x: i32, y: i32) -> PyResult<i32> {
    Ok(x + y)
}

需要添加到模块里面

rust 复制代码
#[pymodule]
fn rust_python(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(hello, m)?)?;
    m.add_function(wrap_pyfunction!(add, m)?)?;
    Ok(())
}

笔者顺序修改一下名字。在项目根目录,运行如下命令

maturin develop --release

有个警告

Warning: Couldn't find the symbol `PyInit__lowlevel` in the native library. Python will fail to import this module. If you're using pyo3, check that `#[pymodule]` uses `_lowlevel` as module name 📦 Built wheel for CPython 3.13 to F:\code\Python\rye-rp\target\wheels\rye_rp-0.1.0-cp313-cp313-win_amd64.whl

好像是笔者修改了模块的名字,说导入失败了**_lowlevel**这个模块

修改在pyproject.toml文件中的内容

[tool.maturin]
python-source = "python"
module-name = "rye_rp.rust_python"
features = ["pyo3/extension-module"]

修改了一下模块的名字,再次运行,结果如下

没问题,可以发现,在python/rye-rp目录下出现了一个新的文件pyd文件

原来的文件被笔者删除了。修改__init__.py文件,内容如下

rust 复制代码
from rust_python import hello,add
if __name__ == '__main__':
    print(hello())
    print(add(1,2))

虽然说Pycharm有报错

但是运行没问题。

如何让pycharm不再报错,很简单,在rye-rp目录下,新建一个rust_python.pyi文件

如下大佬的文章

Python 存根文件(.pyi)详解:类型提示的高级指南-CSDN博客https://blog.csdn.net/weixin_63779518/article/details/148560726

其中内容如下

rust 复制代码
def hello() -> str:
    """返回问候语"""
    ...

def add(a: int, b: int) -> int:
    """两数相加"""
    ...

这个就不会报错了。


=========就这样,明天再说===========


导出类

简单的导出

导出函数使用的是pyfunction,那导出类自然使用的是pyclass。

简单的一个类student,先新建一个student文件

其中内容如下

rust 复制代码
use pyo3::prelude::*;
#[pyclass]
pub struct Student{
    #[pyo3(get,set)]
    pub id:u32,
    #[pyo3(get,set)]
    pub name:String,
}

实现了get和set。打包并运行,导入并使用

rust 复制代码
from rust_python import hello, add,Student
if __name__ == '__main__':
    print(hello())
    print(add(1,2))
    s=Student(1,"sasd")
    print(s)

然后就报错了

Hello from rye-rp!
3
Traceback (most recent call last):
File "F:\code\Python\rye-rp\python\rye_rp\init.py", line 5, in <module>
s=Student(1,"sasd")
TypeError: cannot create 'builtins.Student' instances

什么叫没有不能创建"builtins.Student"实例,啊

笔者去pyo3的官网看了看

好像需要实现一个new方法,应该是,行

rust 复制代码
#[pymethods]
impl Student{
     #[new] 
    fn new(id: u32, name: String) -> Self {
        Self { id, name }
    }
}

再次打包运行。结果如下

没问题。

当然,还是没有代码提示,写一个类

python 复制代码
class Student:
    id: int
    name: str
    def __init__(self, id: int, name: str) -> None: ...

作为提示。

实现魔术方法

比如__str__,__len__等之类的,因此,笔者重新定义一个类Vector,搞点好玩的,当然,还是在一个新的rust文件里面写。

当然Vector可以类似于Python的列表,那么应该比较灵活,除了能存放数字,还应该能存放其他东西,考虑PyAny。

必然需要使用Rust里面的Vec,

PyAny in pyo3 - Rusthttps://jonhue.github.io/soco/doc/pyo3/struct.PyAny.html如果还需要实现切片,这里就非常值得考虑了。

那么经过多次的修改,最终rust的代码如下,

具体细节难以细说,主要是因为还是比较麻烦。


=====算了,明天再说=======


直接给出代码

rust 复制代码
use pyo3::exceptions::PyIndexError;
use pyo3::prelude::*;
use pyo3::types::{PyAny, PySlice};

#[pyclass]
pub struct Vector {
    data: Vec<Py<PyAny>>,
}

#[pymethods]
impl Vector {
    #[new]
    fn new(data: Vec<Py<PyAny>>) -> Self {
        Self { data }
    }

    fn __len__(&self) -> usize {
        self.data.len()
    }

    fn __getitem__<'py>(&self, py: Python<'py>, idx: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
        if let Ok(i) = idx.extract::<isize>() {
            let len = self.data.len() as isize;
            let adjusted_i = if i < 0 { len + i } else { i };

            if adjusted_i < 0 || adjusted_i >= len {
                return Err(PyIndexError::new_err("Index out of bounds"));
            }

            return self.data.get(adjusted_i as usize)
                .map(|obj| obj.bind(py).clone())
                .ok_or_else(|| PyIndexError::new_err("Index out of bounds"));
        }
    
        if let Ok(slice) = idx.cast::<PySlice>() {
            let indices = slice.indices(self.data.len() as isize)?;
            let mut items = Vec::with_capacity(indices.slicelength as usize);

            let mut current = indices.start;
            for _ in 0..indices.slicelength {
                if let Some(obj) = self.data.get(current as usize) {
                    items.push(obj.clone_ref(py));
                }
                current += indices.step;
            }

            let new_vector = Vector { data: items };
            let py_vector = Py::new(py, new_vector)?;
            Ok(py_vector.bind(py).clone().into_any()) 
        } else {
            Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
                "Index must be int or slice"
            ))
        }
    }
     fn __str__(&self, py: Python<'_>) -> PyResult<String> {
        let mut parts = Vec::with_capacity(self.data.len());
        for obj in &self.data {
            let s = obj.bind(py).str()?.to_string();
            parts.push(s);
        }
        Ok(format!("Vector([{}])", parts.join(", ")))
    }
    fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
        let mut parts = Vec::with_capacity(self.data.len());
        for obj in &self.data {
            let s = obj.bind(py).repr()?.to_string();
            parts.push(s);
        }
        Ok(format!("Vector([{}])", parts.join(", ")))
    }
}
python 复制代码
from typing import Any, overload, Union
class Vector:
    def __init__(self, data: list[Any]) -> None:...

    def __len__(self) -> int:        ...

    def __str__(self) -> str:...

    def __repr__(self) -> str:...

    @overload
    def __getitem__(self, idx: int) -> Any:...

    @overload
    def __getitem__(self, idx: slice) -> "Vector":...

    def __getitem__(self, idx: Union[int, slice]) -> Union[Any, "Vector"]:...

打包、安装、运行

python 复制代码
    v=Vector([1,2,3,4,5])
    print(len(v))
    print(v[-2])

一些关键的东西

Bound和Py

Bound in pyo3 - Rusthttps://pyo3.rs/main/doc/pyo3/struct.bound

看看Bound的定义

rust 复制代码
#[repr(transparent)]
pub struct Bound<'py, T>(Python<'py>, ManuallyDrop<Py<T>>);

这是一个**元组结构体,**如果初始化需要传两个参数,可以写一个测试,来看看到底怎么使用的

使用官网的例子

rust 复制代码
use pyo3::prelude::*;
use std::any::type_name_of_val;
#[pyclass]
struct  Foo{
    #[pyo3(get, set)]
    name: String,
}
#[pyclass]
struct  Foo{
    name: String,
}
#[pymethods]
impl Foo{
    #[new]
    fn new(name: String) -> Self {
        Self{
            name,
        }
    }
}
#[cfg(test)]
mod tests{
    use super::*;
    use pyo3::Python;

    #[test]
    fn test_bound(){
        Python::attach(|py|{
            let foo_bound=Bound::new(py,Foo::new("foo".to_string()));
            println!("  类型: {}", type_name_of_val(&foo_bound))
        })


    }
}

笔者运行测试,没想到报错了

error: test failed, to rerun pass `-p rye-rp --lib`

Caused by:
process didn't exit successfully: `C:\Users\26644\AppData\Local\Programs\RustRover 2025.2.4.1\bin\native-helper\intellij-rust-native-helper.exe F:\code\Python\rye-rp\target\debug\deps\rye_rp-2c705f678bff614e.exe simple::tests::test_bound --format=json --exact -Z unstable-options --show-output` (exit code: 0xc0000135, STATUS_DLL_NOT_FOUND)
note: test exited abnormally; to see the full output pass --no-capture to the harness.
error: 1 target failed:
`-p rye-rp --lib`

这里面有个关键的东西------STATUS_DLL_NOT_FOUND,说明是dll文件没有发现,那么显示是指的python.dll文件了,看来测试环境和部署环境还不一样,获取不了dll文件

笔者进入虚拟环境里面的Scripts看看

笔者使用的rye,笔者使用的是python313,这里面确实是没有python311.dll文件,有点意思

因此,笔者直接把rye管理的python313.dll文件复制到这个Scripts目录下。然后再运行

thread 'simple::tests::test_bound' (51684) panicked at C:\Users\26644\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\pyo3-0.27.1\src\interpreter_lifecycle.rs:117:13:
assertion `left != right` failed: The Python interpreter is not initialized and the `auto-initialize` feature is not enabled.

Consider calling `Python::initialize()` before attempting to use Python APIs.
left: 0
right: 0
stack backtrace:

.....

需要先调用**Python::initialize(),**可以,修改一下代码

rust 复制代码
    #[test]
    fn test_bound(){
        Python::initialize();
        Python::attach(|py|{
            let foo_bound=Bound::new(py,Foo::new("foo".to_string())).unwrap();
            println!("  类型: {}", type_name_of_val(&foo_bound));
            let foo:Py<Foo>=foo_bound.into();
            println!("  类型: {}", type_name_of_val(&foo));
        })

    }

结果如下


=========就这样,明天再说============


简单的测试了一下,

这里使用了两个关键的东西Bound和Py,先给出Py的定义

rust 复制代码
#[repr(transparent)]
pub struct Py<T>(NonNull<ffi::PyObject>, PhantomData<T>);

Bound是有生命周期参数,而Py没有这个参数,这就非常关键了

直接复制官网的原文

bound

A Python thread-attached equivalent to Py<T>.

This type can be thought of as equivalent to the tuple (Py<T>, Python<'py>). By having the 'py lifetime of the Python<'py> token, this ties the lifetime of the Bound<'py, T> smart pointer to the lifetime the thread is attached to the Python interpreter and allows PyO3 to call Python APIs at maximum efficiency.

To access the object in situations where the thread is not attached, convert it to Py<T> using .unbind(). This includes, for example, usage in Python::detach's closure.

See the guide for more detail.

Py

A reference to an object allocated on the Python heap.

Py只是一个reference,有python对象的地址,可以知道里面的数据和类型,但是不能安全地操作

Bound是A Python thread-attached equivalent to Py<T>,说白了,二者根本性的区别

就是能不能安全地操作的问题,Py里面有数据和类型,bound里面也有数据和类型,

但是bound是Python thread-attached,而这个 thread-attached的意思

thread-attached = 当前线程持有 GIL=可以CRUD

就这个意思。

总之,二者的根本性区别就是能否操作Python对象,笔者个人的理解

写一个测试------test_alter_name

rust 复制代码
    #[test]
    fn test_bound(){
        Python::initialize();
        Python::attach(|py|{
            let foo_bound=Bound::new(py,Foo::new("foo".to_string())).unwrap();
            println!("  类型: {}", type_name_of_val(&foo_bound));
            foo_bound.borrow_mut().name = "foo".to_string();
            let foo:Py<Foo>=foo_bound.into();
            foo.borrow_mut(py).name = "foo".to_string();
            println!("  类型: {}", type_name_of_val(&foo));
        })
    }

初始化Bound,使用borrow_mut修改内容,变成Py之后,也使用borrow_mut修改内容

看一些Py中borrow_mut的函数签名

rust 复制代码
    #[inline]
    #[track_caller]
    pub fn borrow_mut<'py>(&'py self, py: Python<'py>) -> PyRefMut<'py, T>
    where
        T: PyClass<Frozen = False>,
    {
        self.bind(py).borrow_mut()
    }

可以发现需要传入一个参数py ,这个参数py的类型是Python,

其中函数体,先使用bind,然后调用borrow_mut

那么显然,这个bind是把Py变成了Bound,然后调用Bound的borrow_mut。

可以看一下bind方法

rust 复制代码
    #[inline]
    pub fn bind<'py>(&self, _py: Python<'py>) -> &Bound<'py, T> {
        // SAFETY: `Bound` has the same layout as `Py`
        unsafe { NonNull::from(self).cast().as_ref() }
    }

没问题。

Python是什么

前面看了Py和Bound,发现Py变成Bound需要一个参数,这个参数的类型是Python,这个Python又是什么?

rust 复制代码
#[derive(Copy, Clone)]
pub struct Python<'py>(PhantomData<&'py AttachGuard>, PhantomData<NotSend>);

首先这个是一个元组结构体。
3.10 PhantomData(幽灵数据) | 第三章、所有权 |《Rust 高级编程 2018》| Rust 技术论坛https://learnku.com/docs/nomicon/2018/310-phantom-data/4721PhantomData<T> 是 Rust 标准库中的零大小类型,用于在编译期向 Rust 的类型系统提供类型信息,运行时完全不存在。

总之,注意力放到**&'py AttachGuard,这是什么**

rust 复制代码
/// RAII type that represents thread attachment to the interpreter.
pub(crate) enum AttachGuard {
    /// Indicates the thread was already attached when this AttachGuard was acquired.
    Assumed,
    /// Indicates that we attached when this AttachGuard was acquired
    Ensured { gstate: ffi::PyGILState_STATE },
}

RAII type that represents thread attachment to the interpreter.

翻译一下------一个 RAII 类型,表示线程已附加到解释器

说白了这个Python这个类型就是一个证明、一个Token,

就相当于前端用户登录,后端返回登录信息,一个Token,放到Cookie里面,而这个Python就是这个作用

总之,就是有了这个Python就代表已经是thread-attach

这也说明Bound<'py>=(Py,Python<'py>),或者说Bound<'py, T> ≈ Py<T> + Python<'py>

没问题

相关推荐
云栖梦泽1 天前
鸿蒙应用签名与上架全流程:从开发完成到用户手中
开发语言·鸿蒙系统
神奇的程序员1 天前
从已损坏的备份中拯救数据
运维·后端·前端工程化
哥本哈士奇(aspnetx)1 天前
Streamlit + LangChain 1.0 简单实现智能问答前后端
python·大模型
爱上妖精的尾巴1 天前
6-4 WPS JS宏 不重复随机取值应用
开发语言·前端·javascript
oden1 天前
AI服务商切换太麻烦?一个AI Gateway搞定监控、缓存和故障转移(成本降40%)
后端·openai·api
我一定会有钱1 天前
斐波纳契数列、end关键字
python
李慕婉学姐1 天前
【开题答辩过程】以《基于Android的出租车运行监测系统设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·后端·vue
小鸡吃米…1 天前
Python 列表
开发语言·python
m0_740043731 天前
SpringBoot05-配置文件-热加载/日志框架slf4j/接口文档工具Swagger/Knife4j
java·spring boot·后端·log4j
kaikaile19951 天前
基于C#实现一维码和二维码打印程序
开发语言·c#