• 隐藏侧边栏
  • 展开分类目录
  • 关注微信公众号
  • 我的GitHub
  • QQ:1753970025
Chen Jiehua

使用C/C++扩展Python 

在Python中,我们可以很容易使用各种内建模块。不过,如果你需要某个新的功能或者对某些逻辑有比较高的性能要求,那么就可以考虑使用C/C++来实现一个Python模块。

使用C/C++来写扩展模块,可以实现Python无法直接完成的功能,比如:

  • 实现一个新的内建对象类型;
  • 调用C/C++库函数和系统调用;

环境配置

我们使用CLion作为开发环境,采用CMake格式来构建项目。

因为我们开发的是一个Python扩展,所以必需include Python的头文件 <Python.h>。

在CMakeLists.txt中,可以使用 find_package 来自动寻找python的头文件路径。

cmake_minimum_required(VERSION 3.10)
project(demo)
set(CMAKE_CXX_STANDARD 11)
find_package(PythonLibs 2.7 REQUIRED)
message(STATUS "Python Include = ${PYTHON_INCLUDE_DIRS}")
include_directories(${PYTHON_INCLUDE_DIRS})
add_library(${PROJECT_NAME} SHARED library.cpp)

默认情况下,编译出来的库文件以 lib前缀开头,我们可以将其去掉:

set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")

如果提示找不到Python,可以检查一下是否安装了 python-dev(比如cygwin或者linux环境下)。

远程编译

Windows和Mac环境下进行编译可能会碰到各种奇怪的问题,而且最后编译出来的库文件一般也是在Linux下使用,因为建议直接在Linux下进行编译。

CLion支持远程编译,在 Setting->Build,Execution,Deployment->Toolchains 中添加一个Remote Host,填写对应的Linux服务器信息(可以用虚拟机安装Linux,并安装好相关的软件)。比如用虚拟机安装一个Ubuntu,然后安装软件:

$ sudo apt install openssh-server
$ sudo apt install python python-dev
$ sudo apt install cmake make gcc g++ gdb

扩展实现

我们接下来开发一个最简单的 add 函数。

头文件

首先,需要在代码中包含Python头文件:

#include <Python.h>

Python头文件中包含了Python的C API,具体可以查看文档

注意:由于Python可能会定义一些预处理,并影响某些系统的标准头文件,因为必需将 #include <Python.h> 放在最前面。

函数体

接着实现我们的 add 函数:

static PyObject* add(PyObject* self, PyObject* args){
    int a, b;
    if (!PyArg_ParseTuple(args, "ii", &a, &b)){
        return nullptr;
    }
    int sum = a + b;
    PyObject* ret = PyLong_FromLong(sum);
    return ret;
}

函数add的参数和返回值都是 PyObject*类型,参数 args 表示从Python中传进来的参数列表,可以用 PyArg_ParseTuple 转换为 C/C++的数据类型,最后再用 PyLong_FromLong 将计算结果转为PyObject*类型返回。

方法列表

方法列表定义了我们这个模块中可以给外部调用的接口,比如:

static PyMethodDef MyDemoMethods[] = {
        {"addx", add, METH_VARARGS, "add two integers"},
        {nullptr, nullptr, 0, nullptr},
};

定义了一个 addx, 之后使用这个模块的时候就可以用 demo.addx(123, 456) 来进行调用了。

每一个方法定义由四个元素组成 {函数名称,函数实体,flag,函数描述},其中flag一般是:METH_VARARGS 或者 METH_VARARGS|METH_KEYWORDS。

  • METH_VARARGS: 表示函数期望python传进来的参数是一个可以被 PyArg_ParseTuple解析的元组;
  • METH_KEYWORDS: 设置这个标志位表示传经来的参数支持keyword,这时候函数实现需要支持第三个参数 PyObject *,并用PyArg_ParseTupleAndKeywords 来解析参数,比如:
static PyObject *hello3(PyObject *self, PyObject *args, PyObject *kwdict){
    int voltage;
    char *state = "a stiff";
    char *action = "voom";
    char *type = "Norwegian Blue";
    static char *kwlist[] = {"voltage", "state", "action", "type", nullptr};
    if (!PyArg_ParseTupleAndKeywords(args, kwdict, "i|sss", kwlist, &voltage, &state, &action, &type)){
        return nullptr;
    }
    printf("-- This parrot wouldn't %s if you put %i Volts throught it.\n", action, voltage);
    printf("-- Lovely plumage, the %s -- It's %s\n", type, state);
    Py_RETURN_NONE;
}

初始化函数

在模块的初始化函数中,我们需要将方法列表传递给python解释器。初始化函数必须以 initname() 命名,name是模块名称,并且不需要定义为 static。

PyMODINIT_FUNC initdemo(void){
    (void) Py_InitModule("demo", MyDemoMethods);
}

编译链接

如果采用远程编译的方法,那么我们就可以直接在CLion中进行编译了。编译完成后,可以在Linux服务器中找到一个 demo.so 的库文件。

====================[ Build | demo | Debug-Remote ]=============================
/usr/bin/cmake --build /home/jachua/learncpp/pymodule/demo/cmake-build-debug-remote --target demo -- -j 6
-- Python Include = /usr/include/python2.7
-- Configuring done
-- Generating done
-- Build files have been written to: /home/jachua/learncpp/pymodule/demo/cmake-build-debug-remote
Scanning dependencies of target demo
[ 50%] Building CXX object CMakeFiles/demo.dir/library.cpp.o
[100%] Linking CXX shared library demo.so
[100%] Built target demo
Build finished

模块调用

最后,就可以在Python脚本中调用我们写的扩展模块了。

# -*- coding: utf-8 -*-
import sys
sys.path.append("/home/jachua/learncpp/pymodule/demo/cmake-build-debug-remote")
import demo

def main():
    print demo.addx(123, 456)

if __name__ == "__main__":
    main()

至此,我们用C/C++来实现一个Python模块就完成了。

参考:

码字很辛苦,转载请注明来自ChenJiehua《使用C/C++扩展Python》

评论