从Python传递结构体数组到C

9 投票
2 回答
3900 浏览
提问于 2025-04-15 19:32

[更新:问题解决了!请看帖子底部]

我需要让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 个回答

1

我看到最简单的做法就是直接避开这个问题,提供一个叫做Device_ReadVertex的功能,它需要输入x、y、z、u、v和颜色这些参数。这样做有明显的缺点,比如Python程序员需要一个一个地输入顶点。

如果这样做不够好(看起来可能确实不够好),那么你可以尝试定义一个新的Python类型,具体方法可以参考这里。这需要写更多的代码,但我觉得这是“更合理的架构”方法,因为这样可以确保你的Python开发者和你在C代码中使用的是同一个类型定义。这个方法比简单的结构体更灵活(实际上它是一个类,可以添加方法等),虽然我不确定你是否真的需要这样,但将来可能会派上用场。

2

虽然没有测试过,但你可以试试这个方法,然后告诉我们它是否满足你的需求。

在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字符串就是一串字节/单词/浮点数,应该就是你想要的东西。

祝你好运!

撰写回答