10

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

问题如下。

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

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

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

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

复制:

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

$ cargo build
$ python3 runme.py

你应该看到:

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

平台详情:

  • macOS 10.14.6
  • 货物 1.44.0 (05d080faa 2020-05-06)
  • rustc 1.44.0 (49cae5576 2020-06-01)
  • Python 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(())
}
/// 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(())
}

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

#!/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)

更新:

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

以下是一些具体细节。列出每个 crate 中的符号,然后用rustfilt显示将它们分解:

$ nm producer.so | grep -e "MyClass.*type_object" | rustfilt -h
0000000000085fa8 d _<producer::MyClass as pyo3::type_object::PyTypeInfo>::type_object_raw::TYPE_OBJECT::h215179c585bab4ba
0000000000021810 t _<producer::MyClass as pyo3::type_object::PyTypeInfo>::type_object_raw::h115c96004643f7df
$ nm consumer.so | grep -e "MyClass.*type_object" | rustfilt -h
0000000000091430 d _<producer::MyClass as pyo3::type_object::PyTypeInfo>::type_object_raw::TYPE_OBJECT::h215179c585bab4ba
0000000000004260 t _<producer::MyClass as pyo3::type_object::PyTypeInfo>::type_object_raw::h0e4c5c91a2345444
0000000000027a70 t _<producer::MyClass as pyo3::type_object::PyTypeInfo>::type_object_raw::h115c96004643f7df

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

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

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

4

1 回答 1

1

我有一个类似的问题,它将为同一个 pyclass 生成两个具有不同类型信息的符号。在我的例子中,我将 pyclass 模块设置为一个独立的 crate,并将其标记为dylib确保它只编译一次,然后从其他 crate 中引用它。这将确保您的 pyclass 只编译一次。

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

于 2022-02-03T15:39:02.850 回答