从Python传递结构体数组到C
[更新:问题解决了!请看帖子底部]
我需要让Python开发者能够将一组打包的数据(在这个例子中是顶点)传递给我的API,而这个API是通过Python C API手动暴露的一系列C++接口。我最初的想法是使用ctypes的结构类来实现这样的接口:
class Vertex(Structure):
_fields_ = [
('x', c_float),
('y', c_float),
('z', c_float),
('u', c_float),
('v', c_float),
('color', c_int)
]
verts = (Vertex * 3)()
verts[0] = Vertex(0.0, 0.5, 0.0, 0.0, 0.5, 0xFF0000FF)
verts[1] = Vertex(0.5, -0.5, 0.0, 0.5, -0.5, 0x00FF00FF)
verts[2] = Vertex(-0.5, -0.5, 0.0, -0.5, -0.5, 0x0000FFFF)
device.ReadVertices(verts, 3) # This is the interfaces to the C++ object
我想传递的函数有以下的签名:
void Device::ReadVertices(Vertex* verts, int count);
而Python的包装器看起来像这样:
static PyObject* Device_ReadVertices(Py_Device* self, PyObject* args)
{
PyObject* py_verts;
int count;
if(!PyArg_ParseTuple(args, "Oi", &py_verts, &count))
return NULL;
// This Doesn't Work!
Vertex* verts = static_cast<Vertex*>(PyCObject_AsVoidPtr(py_verts));
self->device->ReadVertices(verts, count);
Py_RETURN_NONE;
}
当然,我面临的最大问题是:我可以获取到这个结构的PyObject,但我不知道该如何将其转换为正确的类型。上面的代码完全失败了。那么,我到底该如何让用户从Python传递这种数据呢?
现在,有几个事情需要考虑:首先,我已经写了相当多的Python/C++层代码,并且对此非常满意(我放弃了SWIG,以便获得更多的灵活性)。我不想重写这些代码,所以我更希望有一个可以原生支持C API的解决方案。其次,我打算在我的C++代码中预定义顶点结构,因此我希望用户不需要在Python中重新定义它(这样可以减少错误),但我不确定如何暴露这样的连续结构。第三,我尝试ctypes结构的原因仅仅是因为我不知道其他方法。欢迎任何建议。最后,由于这(如你们可能猜到的)是一个图形应用程序,我更倾向于使用更快的方法,而不是方便的方法,即使更快的方法需要多花一点功夫。
感谢任何帮助!我仍在摸索Python扩展,所以能得到社区的建议对我来说非常有帮助。
[解决方案]
首先,感谢所有提供想法的人。许多小建议汇聚成了最终的答案。最后,我发现:Sam建议使用struct.pack,结果非常正确。鉴于我使用的是Python 3,我稍微调整了一下,但最终这确实让我在屏幕上显示出了一个三角形:
verts = bytes()
verts += struct.pack("fffffI", 0.0, 0.5, 0.0, 0.0, 0.5, 0xFF0000FF)
verts += struct.pack("fffffI", 0.5, -0.5, 0.0, 0.5, -0.5, 0x00FF00FF)
verts += struct.pack("fffffI", -0.5, -0.5, 0.0, -0.5, -0.5, 0x0000FFFF)
device.ReadVertices(verts, 3)
现在我的元组解析看起来像这样:
static PyObject* Device_ReadVertices(Py_Device* self, PyObject* args)
{
void* py_verts;
int len, count;
if(!PyArg_ParseTuple(args, "y#i", &py_verts, &len, &count))
return NULL;
// Works now!
Vertex* verts = static_cast<Vertex*>(py_verts);
self->device->ReadVertices(verts, count);
Py_RETURN_NONE;
}
请注意,尽管在这个例子中我没有使用len
变量(不过我会在最终产品中使用),我需要用'y#'来解析元组,而不是仅仅用'y',否则它会在第一个NULL处停止(根据文档)。另外要考虑的是:像这样的void*转换是相当危险的,所以请做更多的错误检查,远比我在这里展示的要多!
所以,工作完成,开心的一天,收工回家,是吧?
等等!别急!还有更多内容!
对这一切的结果感到满意,我决定随便试试之前的代码是否还会出错,于是回到了帖子开头的第一段Python代码(当然是使用新的C代码),结果……它成功了!结果和struct.pack版本完全相同!哇!
这意味着你的用户可以选择如何提供这种数据,而你的代码可以无缝处理这两种方式。就我个人而言,我会鼓励使用ctype.Structure方法,因为我觉得这样更易读,但实际上,用户可以选择他们最舒服的方式。(其实,他们如果愿意,也可以手动输入一串十六进制的字节。这样也可以,我试过。)
老实说,我觉得这是最好的结果,所以我非常高兴。再次感谢大家,祝好运给其他遇到这个问题的人!
2 个回答
我看到最简单的做法就是直接避开这个问题,提供一个叫做Device_ReadVertex的功能,它需要输入x、y、z、u、v和颜色这些参数。这样做有明显的缺点,比如Python程序员需要一个一个地输入顶点。
如果这样做不够好(看起来可能确实不够好),那么你可以尝试定义一个新的Python类型,具体方法可以参考这里。这需要写更多的代码,但我觉得这是“更合理的架构”方法,因为这样可以确保你的Python开发者和你在C代码中使用的是同一个类型定义。这个方法比简单的结构体更灵活(实际上它是一个类,可以添加方法等),虽然我不确定你是否真的需要这样,但将来可能会派上用场。
虽然没有测试过,但你可以试试这个方法,然后告诉我们它是否满足你的需求。
在Python这边,把顶点打包成一个字符串,而不是一个对象。
str = "" # byte stream for encoding data
str += struct.pack("5f i", vert1.x, vert1.y, vert1.z, vert1.u, vert1.v, vert1.color) # 5 floats and an int
# same for other vertices
device. ReadVertices( verts, 3) # send vertices to C library
在C库和Python的包装器中,修改你的PyArgs_ParseTuple,使用格式字符串 "si"
。这样可以把你的Python字符串转换成C字符串(char*),然后你可以把它当作指向你的向量结构的指针。到这个时候,C字符串就是一串字节/单词/浮点数,应该就是你想要的东西。
祝你好运!