
近期笔者在研究python内部部分模块的实现机理,研究着研究着就开始硬刚C源码了。想着先前工作或是日常也没有体验过用C++编写python库,于是就刚好学习了一下。
用C或者C++编写python的扩展库,建议用Visual Studio宇宙第一IDE,一来能够同时支持python跟C,二来调试功能非常强大。入门上手的话,可以参考下面的文档:
编写的库叫做cplayground,只包含一个hack函数tuple_setitem——强行设置tuple的元素(python默认是不支持的)。我们可以来看这样的python扩展用C++该如何实现:
首先参考上面的VS文档Create a C++ extension for Python,部署基础环境,一个Solution里需要包含用于测试的Python Project以及用来编写扩展的C++ Project。环境部署有几个要点需要注意:
值得一提的是,如果VS没有预装python发行版本,VS自带的python环境管理模块也能检测到你以前另外安装的python,所以不用担心python环境方面的问题。
搭好整个框架流程之后,我们可以实现tuple_setitem具体的逻辑了。整个cpp代码如下:
// cplayground.cpp #define PY_SSIZE_T_CLEAN #includePyObject* tuple_setitem(PyObject* self, PyObject* args) { // parse args PyObject *tuple, *value; int idx; if (!PyArg_ParseTuple(args, "OiO", &tuple, &idx, &value)) { Py_RETURN_FALSE; } // check tuple if (!PyTuple_Check(tuple)) { PyErr_Format( PyExc_TypeError, "invalid tuple, %.200s", Py_TYPE(tuple)->tp_name ); Py_RETURN_FALSE; } PyTupleObject* tp = (PyTupleObject*)tuple; // handle index below zero if (idx < 0) { idx += PyTuple_GET_SIZE(tp); } // check index range if ((size_t)idx >= (size_t)Py_SIZE(tp)) { PyErr_SetString( PyExc_IndexError, "tuple index out of range" ); Py_RETURN_FALSE; } // set value by index Py_INCREF(value); Py_SETREF(tp->ob_item[idx], value); Py_RETURN_TRUE; } static PyMethodDef cplayground_methods[] = { { "tuple_setitem", (PyCFunction)tuple_setitem, METH_VARARGS, "a hack method to set value in tuple" }, { nullptr, nullptr, 0, nullptr } }; static PyModuleDef cplayground_module = { PyModuleDef_HEAD_INIT, "cplayground", // module name "a c-python extension for testing", // module desc 0, cplayground_methods }; PyMODINIT_FUNC PyInit_cplayground() { return PyModule_Create(&cplayground_module); }
在模块的cpp实现中,顶头必须要#include
那么怎么实现tuple_setitem的逻辑呢?我们需要预想下python端如何调用:
import cplayground tp = (1, 2, 3) cplayground.tuple_setitem(tp, 0, 'haha') # 设置第一个元素为字符串'haha'
在C层的实现上,tuple_setitem的签名是两个参数:module自己的引用、打包的参数集合,都是PyObject*。
首先第一步是通过PyArg_ParseTuple看是否能用对应的模式解包参数。关于参数的模式,可以参考这个文档。
之后需要对解包的参数再检查,比如检查解出来的PyObject* tuple是否真正是tuple object,以及计算索引是不是有溢出的情况。
最后通过Py_SETREF,就能把传进来的元组的内部元素重置。整个实现过程,其实是参考了list object的相关实现,有兴趣的同学可以深入探索一下(预告:在后面的文章里也会提到这个)。
整个小项目都放到了github上,是一个sln。如果要测试的话,clone之后需要注意重新走一遍环境配置的过程,确认配置无误。