【Python随笔】用C++编写Python的扩展模块

近期笔者在研究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。环境部署有几个要点需要注意:

  • 由于是编写扩展,需要编译为dll,文件后缀名为.pyd
  • 确认下libs有没有debug库,没有的话Preprocessor跟Code Generation的设置都不能带debug
  • python项目的Debug设置里,解释器参数加上-i,且启用native code debugging,这样才能在python跟c代码打断点调试
  • 可以写一个最简的框架先尝试build成功,并且能顺利装到对应python的packages里,跑通整个流程先
    • setup.py中注意模块名字要全部对应上

值得一提的是,如果VS没有预装python发行版本,VS自带的python环境管理模块也能检测到你以前另外安装的python,所以不用担心python环境方面的问题。

搭好整个框架流程之后,我们可以实现tuple_setitem具体的逻辑了。整个cpp代码如下:

// cplayground.cpp

#define PY_SSIZE_T_CLEAN
#include <Python.h>


/* tuple_setitem: a hack method to set item of tuple */
PyObject*
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 <Python.h>,之后我们可以把自己需要暴露出去的函数给实现了(这里可以看到模块的主cpp文件能够起到胶水层的作用,如果此时有其它头文件里定义了一系列接口,就可以在这个文件里把这些接口适配为python可识别的函数模式)。实现完成之后,通过method defmodule def定义接口列表跟模块,然后再在下面定义模块启动函数,一个python的C扩展就诞生了。

那么怎么实现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之后需要注意重新走一遍环境配置的过程,确认配置无误。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

utmhikari

创作不易,共同助力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值