7

使用C/C++扩展Python

 3 years ago
source link: https://chenjiehua.me/python/extending-python-with-cpp.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

使用C/C++扩展Python 

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

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

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

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

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

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

Default
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前缀开头,我们可以将其去掉:

Default
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,然后安装软件:

Default
$ 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 的库文件。

Default
====================[ 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脚本中调用我们写的扩展模块了。

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模块就完成了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK