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)
/// 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表示对象类型的实际值。在我看来,MyClass从producer模块构造 的实例时,类型对象是从符号返回的,这似乎是合理的type_object_raw::h115c96004643f7df。但是当函数 likeconsumer::print_data尝试转换传递的实例时MyClass,它们使用符号type_object_raw::h0e4c5c91a2345444来获取对象的类型。想必这些是不一样的。
所以现在我的问题是,为什么有两个不同的符号用于返回 的实例的类型MyClass?
我有一个类似的问题,它将为同一个 pyclass 生成两个具有不同类型信息的符号。就我而言,我将 pyclass 模块设为独立的板条箱,并将其标记为dylib确保它仅编译一次,然后从其他板条箱引用它。这将确保你的 pyclass 只编译一次。
由于 rust 的编译模型会在不同的翻译单元中多次编译相同的库,每次编译发生在 pyclass 上时,它都会生成不同的 python 类型(具有相同的名称),当你让 pyo3 抱怨你的 PyABC 对象时,它变得非常混乱无法转换为 PyABC 对象!