无法通过 PyO3 创建的 Python 绑定在多个 Rust 库之间使用类型

bna*_*ker 8 rust python-3.x pyo3

我正在编写一个带有多个库的 Rust 项目。工作区中的其他库使用的一些库导出类型。除了 Rust crate,我还想将一些库暴露给 Python,使用pyo3crate生成 Python 绑定,这就是我遇到麻烦的地方。

问题如下。

假设我们有两个 Rust 库包producer, 和consumer。在 中producer,我们有一个简单的类型,MyClass它是公开可用的,并且是 Python 模块的一部分。在consumercrate 中,我有一些函数接受类型为 的对象MyClass,并对它们执行一些操作。这些函数在 Rust 中可用,并且还绑定到第二个 Python 模块中。

我可以MyClass在 Python 和 Rust 中创建对象。我可以正确调用 Rust 代码(例如,从另一个应用程序)中接受MyClass. 但是我不能从 Python调用consumer模块中接受类型对象的函数。换句话说,虽然我可以在 Rust 或 Python 中创建类型的对象并在 Rust crate 中使用它们,但我无法将对象从Python 模块传递到Python 模块。这样做会生成 a ,尽管对象将自己宣传为具有 type 。为什么?MyClassMyClassconsumerproducerconsumerTypeErrorMyClass

编辑:请参阅问题的底部以进行进一步调查。

我制作了一个 MCVE,可从GitHub 获取。Rust 和 Python 代码也包含在下面。

再现:

克隆 repo 后,您可以生成我得到的输出:

$ cargo build
$ python3 runme.py
Run Code Online (Sandbox Code Playgroud)

你应该看到:

Object is of type: <class 'MyClass'>
isinstance(obj, MyClass): true
Could not convert object! PyErr { type: Py(0x10d79e5b0, PhantomData) }
Traceback (most recent call last):
  File "./runme.py", line 32, in <module>
    consumer.print_data(obj)
TypeError
Run Code Online (Sandbox Code Playgroud)

平台详情:

  • macOS 10.14.6
  • 货物 1.44.0 (05d080faa 2020-05-06)
  • rustc 1.44.0 (49cae5576 2020-06-01)
  • 蟒蛇 3.7.7
  • pyo3 v0.11.1

代码:

/// producer.rs
use pyo3::prelude::*;

#[pyclass]
#[derive(Debug, Clone)]
pub struct MyClass {
    data: u64,
}

#[pymethods]
impl MyClass {
    #[new]
    fn new(data: u64) -> Self {
        MyClass { data }
    }

    pub fn get_data(&self) -> u64 {
        self.data
    }
}

#[pymodule]
fn producer(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_class::<MyClass>()?;
    Ok(())
}
Run Code Online (Sandbox Code Playgroud)
/// consumer.rs
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

use producer::MyClass;

#[pyfunction]
fn print_data(cls: &MyClass) {
    println!("{}", cls.get_data());
}

#[pyfunction]
fn convert_to_myclass(obj: &PyAny) -> PyResult<()> {
    match obj.extract::<MyClass>() {
        Ok(_) => println!("Converted to MyClass successfully"),
        Err(err) => println!("Could not convert object! {:?}", err),
    }
    Ok(())
}

#[pyfunction]
fn print_type_info(obj: &PyAny) {
    let typ = obj.get_type();
    println!("Object is of type: {}", typ);
    println!("isinstance(obj, MyClass): {}", typ.is_instance(obj).unwrap());
}

#[pymodule]
fn consumer(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_wrapped(wrap_pyfunction!(print_data))?;
    m.add_wrapped(wrap_pyfunction!(print_type_info))?;
    m.add_wrapped(wrap_pyfunction!(convert_to_myclass))?;
    Ok(())
}
Run Code Online (Sandbox Code Playgroud)

这个小的 Python 脚本演示了这个问题。第一个功能是确保构建的 crate 可以被脚本导入。

/// consumer.rs
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

use producer::MyClass;

#[pyfunction]
fn print_data(cls: &MyClass) {
    println!("{}", cls.get_data());
}

#[pyfunction]
fn convert_to_myclass(obj: &PyAny) -> PyResult<()> {
    match obj.extract::<MyClass>() {
        Ok(_) => println!("Converted to MyClass successfully"),
        Err(err) => println!("Could not convert object! {:?}", err),
    }
    Ok(())
}

#[pyfunction]
fn print_type_info(obj: &PyAny) {
    let typ = obj.get_type();
    println!("Object is of type: {}", typ);
    println!("isinstance(obj, MyClass): {}", typ.is_instance(obj).unwrap());
}

#[pymodule]
fn consumer(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_wrapped(wrap_pyfunction!(print_data))?;
    m.add_wrapped(wrap_pyfunction!(print_type_info))?;
    m.add_wrapped(wrap_pyfunction!(convert_to_myclass))?;
    Ok(())
}
Run Code Online (Sandbox Code Playgroud)

更新:

我一直在深入研究这个问题,我开始怀疑这个问题是由 Rust 库的构建方式引起的。我对一般的库很熟悉,但对任何 Rust 特定的东西不太熟悉。似乎 Rust 将哈希编码到每个损坏的符号名称中。我目前的猜测是这些散列在consumer共享库和之间略有不同producer,因此尽管类型MyClass具有相同的文本表示,但consumer函数中预期的实际类型略有不同。

这里有一些细节可以使这一点具体化。列出每个板条箱中的符号,然后用rustfilt节目来分解它们:

#!/usr/bin/env python3
"""runme.py
MCVE showing showing type weirdness in Python/PyO3.
(C) 2020 Benjamin Naecker
"""

import os
import platform


def link_libraries():
    names = ("libproducer", "libconsumer")
    lib_extension = ".so" if platform.system() == "Linux" else ".dylib"
    base_path = "./target/debug/"
    for name in names:
        source = os.path.join(base_path, f"{name}{lib_extension}")
        new_name = name.replace("lib", "")
        dest = f"./{new_name}.so"
        if os.path.exists(dest):
            os.remove(dest)
        os.symlink(source, dest)


if __name__ == "__main__":
    link_libraries()
    import producer
    import consumer

    obj = producer.MyClass(10)
    consumer.print_type_info(obj)
    consumer.convert_to_myclass(obj)
    consumer.print_data(obj)
Run Code Online (Sandbox Code Playgroud)

您可以看到板条箱type_obect_raw的符号中有一个额外的符号consumer。我不确定如何验证这一点,但我怀疑这是用于转换传递给consumercrate中失败的函数的对象的类型信息。这种类型的对象虽然具有相同的名称,但在某些方面必须有所不同,因为散列是不同的。

查看pyo3docs,该方法type_object_raw用于返回PyTypeObject表示对象类型的实际值。在我看来,MyClassproducer模块构造 的实例时,类型对象是从符号返回的,这似乎是合理的type_object_raw::h115c96004643f7df。但是当函数 likeconsumer::print_data尝试转换传递的实例时MyClass,它们使用符号type_object_raw::h0e4c5c91a2345444来获取对象的类型。想必这些是不一样的。

所以现在我的问题是,为什么有两个不同的符号用于返回 的实例的类型MyClass

Cyb*_*opy 1

我有一个类似的问题,它将为同一个 pyclass 生成两个具有不同类型信息的符号。就我而言,我将 pyclass 模块设为独立的板条箱,并将其标记为dylib确保它仅编译一次,然后从其他板条箱引用它。这将确保你的 pyclass 只编译一次。

由于 rust 的编译模型会在不同的翻译单元中多次编译相同的库,每次编译发生在 pyclass 上时,它都会生成不同的 python 类型(具有相同的名称),当你让 pyo3 抱怨你的 PyABC 对象时,它变得非常混乱无法转换为 PyABC 对象!