如何使用 PyO3 从 Python 访问 Rust 迭代器?

the*_*eth 8 python iterator rust pyo3

我对 Rust 很陌生,我的第一个“严肃”项目涉及使用 PyO3 为小型 Rust 库编写 Python 包装器。这主要是非常轻松的,但我正在努力研究如何将 Rust 上Vec的惰性迭代器暴露给 Python 代码。

到目前为止,我一直在收集迭代器产生的值并返回一个列表,这显然不是最好的解决方案。这是一些说明我的问题的代码:

use pyo3::prelude::*;

// The Rust Iterator, from the library I'm wrapping.
pub struct RustIterator<'a> {
    position: usize,
    view: &'a Vec<isize>
}

impl<'a> Iterator for RustIterator<'a> {
    type Item = &'a isize;

    fn next(&mut self) -> Option<Self::Item> {
        let result = self.view.get(self.position);
        if let Some(_) = result { self.position += 1 };
        result
    }
}

// The Rust struct, from the library I'm wrapping.
struct RustStruct {
    v: Vec<isize>
}

impl RustStruct {
    fn iter(&self) -> RustIterator {
        RustIterator{ position: 0, view: &self.v }
    }
}

// The Python wrapper class, which exposes the 
// functions of RustStruct in a Python-friendly way.
#[pyclass]
struct PyClass {
    rust_struct: RustStruct,
}

#[pymethods]
impl PyClass {
    #[new]
    fn new(v: Vec<isize>) -> Self {
        let rust_struct = RustStruct { v };
        Self{ rust_struct }
    }

    // This is what I'm doing so far, which works
    // but doesn't iterate lazily.
    fn iter(&self) -> Vec<isize> {
        let mut output_v = Vec::new();
        for item in self.rust_struct.iter() {
            output_v.push(*item);
        }
        output_v
    }
}
Run Code Online (Sandbox Code Playgroud)

我试图RustIterator用 Python 包装器包装类,但我不能使用 PyO3 的#[pyclass]proc。带有生命周期参数的宏。我研究了一下,pyo3::types::PyIterator但这看起来像是一种从 Rust 访问 Python 迭代器的方法,而不是相反。

如何RustStruct.v在 Python 中访问惰性迭代器?可以安全地假设Vec始终派生CopyClone,以及需要 Python 端的一些代码的答案中包含的类型是可以的(但不太理想)。

小智 0

我的建议是,正如您所指出的,PyO3 并不是为处理PyClass实现者的泛型而设计的。在这种情况下,它会阻止您做潜在危险的事情,因为RustIterator您尝试包装的生命周期通用。rustc无法分析 FFI 边界上的生命周期,例如 PyO3 试图跨越的 Rust/Python 边界。因此,您只能传递包装器'static + Send + Sync'static引用不需要通用生命周期;它是内置关键字)。

这意味着(假设您有权访问 的内部RustIterator)您可以使用不安全代码来更改 的生命周期&'a Vec<T>。当您取消引用原始指针时,编译器不会假定生命周期。它相信生命周期就是你告诉它的任何东西。这意味着您可以执行类似于下面示例的操作,只要您可以确保传递给 Python 运行时的迭代器永远不会比RustIterator您从正在包装的库中获取的迭代器的寿命长。需要明确的是,这可能会对您的图书馆造成不好的影响。

struct StaticRustIterator {
    position: usize,
    view: &'static Vec<isize>
}

fn make_iter_static<'a>(iter: RustIterator<'a>) -> StaticRustIterator {
    let RustIterator { position, view } = iter;
    let static_iter = StaticRustIterator {
        position,
        view: unsafe { view.as_ptr() as &'static Vec<isize> }
    };
    static_iter
}
Run Code Online (Sandbox Code Playgroud)

如果您想使用 PyO3,您的另一个选择是克隆view中的字段RustIterator并以这种方式创建'static迭代器。这是昂贵的,但安全。

struct StaticRustIterator {
    position: usize,
    view: Vec<isize>
}

fn make_iter_static<'a>(iter: RustIterator<'a>) -> StaticRustIterator {
    let RustIterator { position, view } = iter;
    let static_iter = StaticRustIterator { position, view: view.clone() };
    static_iter
}
Run Code Online (Sandbox Code Playgroud)

如果可以的话,我个人不会选择这两种方法中的任何一种。您可以看看是否有一种方法可以获取指向该视图的原子引用计数指针,而不是获取RustIterator. 如果您可以对库中一些更深、更坚韧的部分进行逆向工程,以便它们可以安全地越过 FFI 边界,我建议您这样做。或者也许其他人会在这里发帖并揭示一些将引用变成弱指针或其他东西的技巧。这不是一个很好的答案,但希望它能给您一些想法。