如何让pybind11正确适配Python ctypes类型与C++函数参数?
问题
我从设备采集的数据在Python中直接以ctypes.c_uint16的ctypes.Array类型存储,为加速迭代后处理任务,用C++实现处理逻辑并通过pybind11生成绑定,最终需返回torch::Tensor。
C++函数签名:
torch::Tensor foo(uint16_t* a, int b, int c, int d, uint64_t e, uint64_t* f);
pybind11绑定代码:
namespace py = pybind11; PYBIND11_MODULE(foo_lib, m){ m.def("foo", &foo, py::arg("a"), py::arg("b"), py::arg("c"), py::arg("d"), py::arg("e"), py::arg("f") ); }
Python调用代码:
import torch import foo_lib from ctypes import Array, c_int, c_uint16, c_uint64 a: Array[c_uint16] = ... # 输入数据数组 b: c_int = c_int(0) c: c_int = c_int(13) d: c_int = c_int(16) e: c_uint64 = c_uint64(1000) f: Array[c_uint64] = ... # 所需元数据数组 output: torch.Tensor = foo_lib.foo(a, b, c, d, e, f)
运行时出现类型错误:
Traceback (most recent call last): File "~/test.py", line 14, in <module> output: torch.Tensor = foo_lib.foo(a, b, c, d, e, f) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: foo(): incompatible function arguments. The following argument types are supported: 1. (a: int, b: int, c: int, d: int, e: int, f: int) -> torch.Tensor Invoked with: <__main__.c_ushort_Array_1000 object at 0x7ad66ff620d0>, c_int(0), c_int(13), c_int(16), c_ulong(1000), <__main__.c_ulong_Array_10 object at 0x7ad66ff62250>
需求:不想将ctypes数组转为numpy.ndarray(会降低速度),也不能用纯ctypes绑定(需要返回torch.Tensor),希望pybind11能正确识别ctypes类型并转换为对应C++类型。
环境:Python 3.12.3、Ubuntu 24.04.4、torch 2.7.1、pybind11 2.11.2
解决方案
pybind11默认不支持直接转换ctypes类型,最直接且易维护的方式是写一个C++包装函数,在包装中手动提取ctypes对象的底层数据和数值,再调用原处理函数。
实现步骤
1. 编写包装函数
在绑定代码中添加一个包装函数,负责转换Python传入的ctypes参数到C++类型:
#include <pybind11/pybind11.h> #include <torch/torch.h> namespace py = pybind11; // 原处理函数(已存在) torch::Tensor foo(uint16_t* a, int b, int c, int d, uint64_t e, uint64_t* f); // 包装函数 torch::Tensor foo_wrapper(py::object a_obj, py::object b_obj, py::object c_obj, py::object d_obj, py::object e_obj, py::object f_obj) { // 导入ctypes模块,用于获取指针和数值 py::object ctypes = py::module::import("ctypes"); py::object addressof = ctypes.attr("addressof"); // 转换ctypes Array为C++指针(零拷贝) uintptr_t a_addr = py::cast<uintptr_t>(addressof(a_obj)); uint16_t* a = reinterpret_cast<uint16_t*>(a_addr); uintptr_t f_addr = py::cast<uintptr_t>(addressof(f_obj)); uint64_t* f = reinterpret_cast<uint64_t*>(f_addr); // 转换ctypes数值类型为C++基本类型 int b = py::cast<int>(b_obj.attr("value")); int c = py::cast<int>(c_obj.attr("value")); int d = py::cast<int>(d_obj.attr("value")); uint64_t e = py::cast<uint64_t>(e_obj.attr("value")); // 调用原函数 return foo(a, b, c, d, e, f); } // 绑定模块 PYBIND11_MODULE(foo_lib, m){ m.def("foo", &foo_wrapper, py::arg("a"), py::arg("b"), py::arg("c"), py::arg("d"), py::arg("e"), py::arg("f") ); }
2. 编译并测试
用原有的编译流程重新编译pybind11模块,之后Python端的调用代码无需修改,直接运行即可。
原理说明
- ctypes的Array对象可以通过
ctypes.addressof()获取底层内存地址,再通过reinterpret_cast转换为对应C++类型的指针,完全零拷贝,不会影响性能。 - ctypes的数值类型(如
c_int、c_uint64)都有value属性,直接提取即可得到对应的Python数值,再转换为C++基本类型。 - 包装函数相当于在pybind11和原函数之间做了一层类型适配,既保留了原C++函数的性能,又兼容了Python端的ctypes类型输入。
内容的提问来源于stack exchange,提问作者inventi
相关产品推荐
相关产品推荐

