Python C语言API系列教程(六、C语言API灵魂——PyBuffer)
在上一篇文章中,我们了解了Python虚拟机对模块和方法的实现,以及它们的API用法。同时,我们也完善了我们的datetimecpy模块。在这篇文章中,我们会接触到一个重要的概念PyBuffer。
本系列代码datetimecpy对应的代码详见repo。
前置知识
早在第一篇文章中,我们就了解到Python的C语言API的用处,一方面是加速,另一方面是调用C开发的模块(本地化适配)。而通过C语言API调用其他模块必须通过一个协议——Buffer协议。
这个协议本质上是生产者-消费者模型,功能提供方(C开发的模块)是生产者,用户使用的Python的API是消费者。官方文档是这样描述的——
- 在生产者方面是一个可以提供buffer给外界使用的接口的类型。
- 在消费者方面是有不同的方法可以获取到原始buffer指针的模型。
这么说有点费解,我的理解是这样的——
- 首先不管生产者还是消费者都是一个
PyObject*。 - 其次,作为生产者,它必须包含一个指针指向原始Buffer,且提供相应的方法可以获取这个Buffer。
- 作为消费者,它必须调用上面的方法来获取Buffer内容给我们最终用户。
举个例子就好理解了,PIL模块是常见的Python绘图库,常见于数据分析、可视化等领域。但是Python本身是没有能力直接绘图的,因此它必须通过操作系统提供的API进行绘图,比如GDI、OpenGL等。而提供这些能力的PyObject*就是生产者。反之,则是消费者。
要点详解
生产者模型
生产者不是自己随意定义的,它必须遵守C语言API的规定的方式,也就是遵守Buffer协议。这个作为生产者的PyObject*的type必须填写tp_as_buffer插槽。这个插槽是一个PyBufferProcs对象,它包含两个函数指针类型的成员,一个是bf_getbuffer函数指针,其签名为int (PyObject *exporter, Py_buffer *view, int flags),另一个是bf_releasebuffer函数指针,其签名为void (PyObject *exporter, Py_buffer *view)。
具体说说这两个函数指针。顾名思义,bf_getbuffer是为了获取buffer。第一个参数exporter即是作为生产者的PyObject*,第二个view是生产者与消费者之间传递的对象,即请求,flags是标志位,暂时不用去管。在实现这个方法的时候也不是乱来的,它经过以下几个步骤——
- 检查请求是否被满足,即检查
view参数是否符合要求。如果符合则继续,否则抛出PyExc_BufferError异常,并将view->obj赋值NULL,函数返回-1。 - 填写
view字段 - 增加
exporter中的计数器 - 将
view->obj指向exporter - 返回0
同理,bf_releasebuffer是释放buffer。第一个参数还是exporter,第二个是需要被释放的view。它的过程只有两步。
- 减少
exporter中的计数器 - 当计数器为0则释放内存
消费者模型
相比而言,消费者模型比较随意点。用户可以以任意的方式消费生产者产生的PyBuffer对象,一般生产者会封装一些原生buffer的操作方法。
PyBuffer及其相关API
在生产者和消费者之间,Python虚拟机是通过Py_buffer传递的。它的结构如下——
typedef struct {
void *buf;
PyObject *obj;
Py_ssize_t len;
Py_ssize_t itemsize;
int readonly;
int ndim;
...
} Py_buffer;
来自CPython 3.9
buf指针指向原生的buffer对象,是我们需要直接使用的对象。obj指向当前Py_buffer的拥有者(有点像rust的概念),一般指向生产者对象。len是总长度,一般用来分配内存。也就是说如果存放一个数组的话就是itemsize* size。itemsize是单个元素的内存,如果不涉及数组则为内存大小。readonly是只读标识,代表这个buffer是否是只读的。可以填PyBUF_WRITABLE代表可写,否则不可写。ndim代表数组的维度,只有当这个buffer是一个数组才用到,比如三维数组的话这个值为3。
与之相关的API也有很多,选取部分有用的API介绍一下——
PyObject_GetBuffer:获取一个buffer对象,它包含三个参数exporter、view和flags。exporter是生产者对象,view是Py_Buffer类型的请求,一般是未经初始化的Py_Buffer对象,flags是标识获取的buffer对象类型,和上述的readonly、ndim等密切相关。这个方法调用成功以后,用户就可以根据view获取对应的buffer对象。PyBuffer_Release:是上述操作的逆操作。接受一个参数,即需要被释放的view。PyBuffer_FillInfo:填写一个Py_Buffer一般用在生产者模型里,即bf_getbuffer函数内。它包含一个exporter,对应被填充的view以及用来填充的buf。
操作实践
在上一篇文章中我们实现了datetimecpy的time模块,这次我们实现timedelta对象,它是用来表示两个时间之间的差。
代码仓库在这里,本章对应的代码在Ch-6分支上。
模拟第三方C库
由于本章内容是PyBuffer,涉及到第三方C语言库的调用。这里我简单实现了一个C语言类作为被调用的接口,用来存放时间差。
这个时间差用三个单位来刻画,分别是日、秒和微秒。然而为了节约内存空间,在自定义的结构体对象中我用一个定长unsigned char数组实现。考虑到日的范围为[-999999999, 999999999],以一个字节8位来算,只需要四个字节就可以表示这么大范围。同理,秒的范围为[0, 86399]占三个字节,微秒的范围为[0, 999999]也占三个字节,所以一共10个字节。

所以这样定义——
struct timedelta_buf {
unsigned char buf[10];
};
那么怎么获取对应的数据呢?比如说取timedelta的days呢?这个很简单,只要获取对应下标的数据再根据位置偏移就好了,比如这样获取days数据——
long timedelta_buf_get_days(struct timedelta_buf* td_buf) {
return (long)(td_buf->buf[0] << 24 | td_buf->buf[1] << 16 | td_buf->buf[2] << 8 | td_buf->buf[3]);
}
如果要写数据呢?也很简单,只要“与”上对应位置的数据并右移相应的位数就好了——
int timedelta_buf_set_days(struct timedelta_buf* td_buf, long days) {
if (td_buf == NULL) {
return -1;
}
td_buf->buf[0] = (days & 0xff000000) >> 24;
td_buf->buf[1] = (days & 0x00ff0000) >> 16;
td_buf->buf[2] = (days & 0x0000ff00) >> 8;
td_buf->buf[3] = (days & 0x000000ff) >> 0;
return 0;
}
其他两个单位也按照这种方法读取即可。另外,需要注意一下这个unsigned char的初始化和释放,必须要用\0来填充——
struct timedelta_buf* timedelta_buf_new() {
struct timedelta_buf* td_buf = (struct timedelta_buf*)PyMem_RawMalloc(10 * sizeof(char));
if (td_buf == NULL) {
return NULL;
}
memset(td_buf->buf, '\0', 10 * sizeof(char));
return td_buf;
}
void timedelta_buf_delete(struct timedelta_buf* td_buf) {
if (td_buf == NULL) {
return;
}
PyMem_RawFree(td_buf);
}
这里面用到了Python内存相关API(
PyMem_RawMalloc和PyMem_RawFree),下一章就会讲解。
好了,有了这部分基础知识就可以往下走了。
生产者对象设计
上文一直提到的exporter就是生产者对象,按照要求其包含一个原生buffer和计数器——
typedef struct {
PyObject_HEAD
struct timedelta_buf* timedelta;
Py_ssize_t exports;
} TimedeltaExporter;
并且在实现type时需要填写tp_as_buffer来表明它是一个生产者对象——
int TimedeltaExporter_getbuffer(TimedeltaExporter* exporter, Py_buffer* view, int flag);
void TimedeltaExporter_releasebuffer(TimedeltaExporter* exporter, Py_buffer* view);
static PyBufferProcs TimedeltaExporter_as_buffer = {
(getbufferproc) TimedeltaExporter_getbuffer,
(releasebufferproc) TimedeltaExporter_releasebuffer
};
static PyTypeObject TimedeltaExporter_type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "datetimecpy._timedelta_exporter",
.tp_as_buffer = &TimedeltaExporter_as_buffer
...
};
然后实现PyObject_GetBuffer和PyBuffer_Release两个方法,注意前文提供的步骤——
int TimedeltaExporter_getbuffer(TimedeltaExporter* exporter, Py_buffer* view, int flag) {
struct timedelta_buf* buf = (struct timedelta_buf*)exporter->timedelta;
if (buf == NULL || exporter->exports == 0) {
buf = timedelta_buf_new();
if (buf == NULL) {
return -1;
}
}
if (view == NULL) {
return -1;
}
PyBuffer_FillInfo(view, exporter, buf, sizeof(buf), 0, flag);
if (PyErr_Occurred()) {
PyErr_Print();
}
exporter->exports ++;
return 0;
}
void TimedeltaExporter_releasebuffer(TimedeltaExporter* exporter, Py_buffer* view) {
timedelta_buf_delete(exporter->timedelta);
exporter->exports --;
}
到此为止,生产者对象写好了。
消费者对象设计
消费者对象比较随意,只要能消费上述的exporter即可。在本例中,消费者对象就是timedelta对象。为了方便,我直接将exporter定义在消费者对象里面——
typedef struct {
PyObject_HEAD
PyObject* exporter;
} Timedelta;
其成员方法即可通过Buffer协议(PyObject_GetBuffer方法)获取struct timedelta_buf*进行操作。比如它的__repr__方法——
PyObject* Timedelta_repr(PyObject* self) {
TimedeltaExporter* exporter = ((Timedelta*)self)->exporter;
Py_buffer buffer = {NULL, NULL};
if (PyObject_GetBuffer(exporter, &buffer, PyBUF_WRITABLE) < 0) {
return NULL;
}
struct timedelta_buf* _buf = (struct timedelta_buf*)buffer.buf;
long days = timedelta_buf_get_days(_buf);
long seconds = timedelta_buf_get_seconds(_buf);
long microseconds = timedelta_buf_get_microseconds(_buf);
return PyUnicode_FromFormat("%s(days=%ld, seconds=%ld, microseconds=%ld)",
Py_TYPE(self)->tp_name, days, seconds, microseconds);
}
现在测试一下,运行项目并初始化timedelta如果出现下图则说明成功——

小结
本章内容讲述了Python的C语言API最重要的部分之一PyBuffer,并完善了datetimecpy项目。至此,datetimecpy项目的开发就告一段落了,它是完全参考官方的datetime模块开发的。如果学有余力可以直接阅读CPython项目中对应的C版本的datetime模块源码。这个版本的datetime并不是我们平常用Python开发时候所用的datetime模块,它是C语言版本写的实现,需要在构建时修改同级的SetUp文件,将下面一行去掉注释再按照文档编译CPython项目才可以使用。
#_datetime _datetimemodule.c
好了,接下来我会开始讲一些遗留的问题,比如这章提到的内存分配机制和GIL。
转载自:https://juejin.cn/post/7245316146337136701