python与c/c++相互调用

最近的项目使用python语言,其中一个功能需要对接c++的sdk。于是学习了下python与c/c++的相互调用方法,这里做下笔记,方便以后查找。

python里面调用c/c++代码基本上有三种方式: ctypes库、cffi库和c/c++拓展模块。这篇笔记主要讲的是拓展模块,不过ctypes和cffi也会稍微介绍一下:

ctypes

使用ctypes模块十分简单,这里直接上demo。我们的c代码如下:

// demo.c
int add(int a, int b) {
    return a + b;
}

使用下面命令编译demo.so

gcc demo.c -shared -fPIC -o demo.so

然后python里面只需要用ctypes.cdll.LoadLibrary方法加载so库,就可以通过方法名去调用c的函数了:

import ctypes
lib = ctypes.cdll.LoadLibrary("./demo.so")
print(lib.add(1, 3))

基本上不需要过多的介绍,不过有个坑是如果写的是c++,那么需要用extern "C"包裹下给python调用的函数:

class Utils {
public:
        static int add(int a, int b) {
                return a + b;
        }
};

extern "C" {

int add(int a, int b) {
        return Utils::add(a, b);
}

}

这么做的原因在于c++编译之后会修改函数的名字,add函数在编译之后变成了__ZN5Utils3addEii,而且不同编译器的修改规则还不一样,所以在python里面用add找不到对应的函数。

加上extern "C"包裹之后能让编译器按照c的方式去编译这个函数,不对函数名做额外的修改,这样python里面才能通过函数名去调用它。

cffi

cffi和ctypes类似,但是稍微复杂一些。

cffi的功能其实是在python里面写c代码,我们可以通过python里面写的c代码去调用第三库的c代码。

这么说可能有点抽象,我举个例子大家可能就好理解了:

我们在c里面实现了一个foo方法,它的作用是打印传入的字符串,并且返回字符串的长度:

#include <stdio.h>
#include <string.h>

int foo(char* str) {
    printf("%s\n", str);
    return strlen(str);
}

我们将上面的c代码编译成ffidemo.so,然后用下面的python代码去调用这个foo方法:

from cffi import FFI

ffi = FFI()
lib = ffi.dlopen("./ffidemo.so")           # 导入so
ffi.cdef("int foo(char* str);")            # 声明foo方法
param = ffi.new("char[]", b"hello world!") # 创建char数组
print(lib.foo(param))                      # 调用之前声明的foo方法,它的实现在ffidemo.so

c/c++拓展模块

使用ctypes的方式虽然简便,但是在使用上能明显的感觉出来是在调用so库的代码。

更不用说使用cffi会在python代码里面嵌入c的语句,总有种莫名的不协调感。

而且c/c++编码规范里一般方法名会用驼峰,但是python编码规范里建议方法名用下划线分割单词,上面的两种方法都会造成python里面调用so和python脚本的方法有两种命名规范,逼死强迫症。

有没有一种方法可能让python无感调用c/c++代码,就像调用普通的python代码一样呢?

答案就是使用c/c++为Python编写扩展模块。虽然有官方文档可以参考,但这个文档其实讲的不是很全,当初也遇到了不少问题,这里也整理下。

我们希望Python里面像这样去调用c/c++:

import demo
demo.foo()

c/c++的完整代码如下:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <iostream>
#include <string>

using namespace std;

PyObject* Foo(PyObject* self, PyObject* args) {
    cout<<"Foo"<<endl;
    return Py_BuildValue("");
}

static PyMethodDef g_moduleMethods[] = {
        {"foo", Foo, METH_NOARGS, "function Foo"},
        {NULL, NULL, 0, NULL}
};

static PyModuleDef g_moduleDef = {
        PyModuleDef_HEAD_INIT,
        "ExtendedDemo",                /* name of module */
        "C/C++ Python extension demo", /* module documentation, may be NULL */
        -1,                            /* size of per-interpreter state of the module, or -1 
                                          if the module keeps state in global variables. */
        g_moduleMethods
};

PyMODINIT_FUNC PyInit_demo(void) {
        return PyModule_Create(&g_moduleDef);
}

我们用下面命令将这个代码编译成demo.so (mac系统下):

g++ demo.cpp -shared -fPIC -o demo.so -I /usr/local/Frameworks/Python.framework/Versions/3.7/include/python3.7m -L /usr/local/Frameworks/Python.framework/Versions/3.7/lib -lpython3.7m

python的import demo语句就会去动态链接这个demo.so,并且调用PyInit_demo方法。也就是说so的名字要和PyInit_XXX这个方法名对应,要不然python里面会报找不到init方法的异常。

这个init方法很简单,就是创建了一个module。这个module的定义在g_moduleDef这个全局变量里面,它定义了module的name、documentation等,这里的name可以和so的名字不一样,它在python里module的__name__、__doc__里面体现:

import demo
print(demo.__name__)  # ExtendedDemo
print(demo.__doc__)   # C/C++ Python extension demo

g_moduleDef里面最重要的是最后一个成员g_moduleMethods,它定义的module里面的方法。这货是个PyMethodDef结构体数组,定义了方法名字,方法的指针,参数类型,和文档描述:

struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;    /* The C function that implements it */
    int         ml_flags;   /* Combination of METH_xxx flags, which mostly
                               describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};
typedef struct PyMethodDef PyMethodDef;

ml_name、ml_meth和ml_doc都很好理解,ml_flags有点小坑。它可以是下面几种类型:

  • METH_NOARGS 没有参数
  • METH_VARARGS 可变参数
  • METH_VARARGS | METH_KEYWORDS 可变参数+关键字参数

METH_NOARGS

我们上面的demo里foo方法就是没有参数的,这里可能有同学会说怎么就没有参数了?明明它有两个参数:

PyObject* Foo(PyObject* self, PyObject* args) {
    cout<<"Foo"<<endl;
    return Py_BuildValue("");
}

是的,虽然在c/c++这里的声明它是有两个参数的,但是由于我们在g_moduleMethods里面给它的声明是METH_NOARGS,在python里面如果给它传参就会出现异常:

import demo
demo.foo(1)

# 出现异常
# Traceback (most recent call last):
#   File "test.py", line 2, in <module>
#     demo.foo(1)
#  TypeError: foo() takes no arguments (1 given)

所以对于METH_NOARGS类型的方法来说,c/c++里面的args参数其实是没有意义的,它总是NULL。

self 参数,对模块级函数指向模块对象,对于对象实例则指向方法。

METH_VARARGS

当我们将一个方法声明成METH_VARARGS,这个函数的args就会变成一个元组,我们可以通过PyArg_Parse方法解析出里面的值,例如下面的add方法:

PyObject* Add(PyObject* self, PyObject* args) {
    int a,b;
    PyArg_Parse(args, "(ii)", &a, &b);
    return Py_BuildValue("i", a+b);
}

static PyMethodDef g_moduleMethods[] = {
        ...
        {"add", Add, METH_VARARGS, "function Add"},
        ...
}

这个方法接收两个int的参数,然后返回a+b的值:

import demo
print(demo.add(1,2)) # 3

我们看到PyArg_Parse和Py_BuildValue都有个字符串去配置数据类型,它们很相似,只不过一个是解析PyObejct*一个是生成PyObejct*,这里用Py_BuildValue举例(左侧是调用,右侧是Python值结果):

Py_BuildValue("")                        None
Py_BuildValue("i", 123)                  123
Py_BuildValue("iii", 123, 456, 789)      (123, 456, 789)
Py_BuildValue("s", "hello")              'hello'
Py_BuildValue("y", "hello")              b'hello'
Py_BuildValue("ss", "hello", "world")    ('hello', 'world')
Py_BuildValue("s#", "hello", 4)          'hell'
Py_BuildValue("y#", "hello", 4)          b'hell'
Py_BuildValue("()")                      ()
Py_BuildValue("(i)", 123)                (123,)
Py_BuildValue("(ii)", 123, 456)          (123, 456)
Py_BuildValue("(i,i)", 123, 456)         (123, 456)
Py_BuildValue("[i,i]", 123, 456)         [123, 456]
Py_BuildValue("{s:i,s:i}",
              "abc", 123, "def", 456)    {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
              1, 2, 3, 4, 5, 6)          (((1, 2), (3, 4)), (5, 6))

不过需要注意的是,虽然Py_BuildValue不用加括号也能自动解析成元组,但是如果要用PyArg_Parse解析元组的话必须加上括号,当然你也可以直接用PyArg_ParseTuple去元组,这样的话就不需要带括号。

METH_KEYWORDS

关于METH_KEYWORDS,文档里面有这样一句话(好像漏了METH_NOARGS,我测试验证这个也是可以用的):

这个标志指定会使用C的调用惯例。可选值有 METH_VARARGSMETH_VARARGS | METH_KEYWORDS

也就是说METH_KEYWORDS是不能单独使用的,必须要和METH_VARARGS一起。我一开始没有注意,单独使用之后一直报错。

这样配置的方法参数类似python里面的func(*args, **kwargs),而c/c++里面的函数声明和METH_NOARGS、METH_VARARGS不一样,有三个参数。可以看下下面的demo:

PyObject* Subtract(PyObject* self, PyObject* args, PyObject* keywds) {
    int a,b;
    char *kwlist[] = {"a", "b", NULL};
    PyArg_ParseTupleAndKeywords(args, keywds, "ii", kwlist, &a, &b);
    return Py_BuildValue("i", a-b);
}
static PyMethodDef g_moduleMethods[] = {
        ...
        {"subtract", (PyCFunction)(void(*)(void))Subtract, METH_VARARGS|METH_KEYWORDS, "function Subtract"},
        ...
};

python里面就能用可变参数和关键字参数的方式传参:

import demo
print(demo.subtract(1, 2))     # -1
print(demo.subtract(1, b=2))   # -1
print(demo.subtract(b=1, a=2)) # 1

c/c++回调python

通过上面的讲解我们可以轻松实现python对c/c++函数的调用。但是我们的项目还出现了python往c/c++里面设置回调函数的需求,我们接下来就来看看这个需求要怎么实现。

下面是c++部分的代码,它注册了个方法,参数是一个PyObject*,实际上它是个回调函数,我把可以用PyEval_CallObject去调用它计算两个字符串的字符总数,得到一个PyObject*的返回值。我们可以用PyLong_AsLong将它解析成c的long类型:

PyObject* SetCountCallback(PyObject* self, PyObject* args) {
    PyObject* callback;
    PyArg_Parse(args, "(O)", &callback);

    PyGILState_STATE state = PyGILState_Ensure();
    PyObject* callbackArgs = Py_BuildValue("(ss)", "hello ", "world");
    PyObject* result = PyEval_CallObject(callback, callbackArgs);
    cout<<"count result : "<<PyLong_AsLong(result)<<endl;

    Py_DECREF(callbackArgs);
    PyGILState_Release(state);

    return Py_BuildValue("");
}

static PyMethodDef g_moduleMethods[] = {
        ...
        {"setCountCallback", SetCountCallback, METH_VARARGS, "function SetCountCallback"},
        ...
};

python的代码如下:

import demo
def count(str1, str2):
    return len(str1) + len(str2)
demo.setCountCallback(count) # c++会打印count result : 11

类似的我们可以用PyFloat_AsDouble从PyObject*解析出double类型的数据。但是如果是字符串类型的话解析比较麻烦需要先转换成bytes类型的数据再转成char*,可以用下面这个方法转换:

string GetStringFromPyObject(PyObject* pObject) {
    PyObject* bytes = PyUnicode_AsUTF8String(pObject);
    string str = PyBytes_AsString(bytes);
    Py_DECREF(bytes);
    return str;
}

如果是返回的元组的话可以用遍历的方法去读取,用PyTuple_Size读取数量然后用PyTuple_GetItem读取item,然后再用上面的转换方法转换:

PyObject* SetSplicingAndCountCallback(PyObject* self, PyObject* args) {
    PyObject* callback;
    PyArg_Parse(args, "(O)", &callback);

    PyGILState_STATE state = PyGILState_Ensure();
    PyObject* callbackArgs = Py_BuildValue("(ss)", "hello ", "world");
    PyObject* result = PyEval_CallObject(callback, callbackArgs);
    cout<<"result size : "<<PyTuple_Size(result)<<endl;
    cout<<"item0 : "<<GetStringFromPyObject(PyTuple_GetItem(result, 0))<<endl;
    cout<<"item1 : "<<PyLong_AsLong(PyTuple_GetItem(result, 1))<<endl;
    Py_DECREF(callbackArgs);
    PyGILState_Release(state);

    return Py_BuildValue("");
}

Python里面这么调用:

import demo

def splicing_and_count(str1, str2):
    return str1+str2, len(str1)+len(str2)
    
demo.setSplicingAndCountCallback(splicing_and_count)

不过实际调试的时候使用PyArg_Parse也能解析出返回值元组的数据,但是这个方法的名字用在解析返回值这里总感觉怪怪的,说不好有什么坑,这块文档里面也没有讲。

char* s;
int i;
PyArg_Parse(result, "(si)", &s, &i);

完整demo

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <iostream>
#include <string>

using namespace std;

PyObject* Foo(PyObject* self, PyObject* args) {
    cout<<self<<endl;
    cout<<args<<endl;
    cout<<"Foo"<<endl;
    return Py_BuildValue("");
}

PyObject* Add(PyObject* self, PyObject* args) {
    int a,b;
    PyArg_Parse(args, "(ii)", &a, &b);
    return Py_BuildValue("i", a+b);
}

PyObject* Subtract(PyObject* self, PyObject* args, PyObject* keywds) {
    int a,b;
    char *kwlist[] = {"a", "b", NULL};
    PyArg_ParseTupleAndKeywords(args, keywds, "ii", kwlist, &a, &b);
    return Py_BuildValue("i", a-b);
}

PyObject* SetCountCallback(PyObject* self, PyObject* args) {
    PyObject* callback;
    PyArg_Parse(args, "(O)", &callback);

    PyGILState_STATE state = PyGILState_Ensure();
    PyObject* callbackArgs = Py_BuildValue("(ss)", "hello ", "world");
    PyObject* result = PyEval_CallObject(callback, callbackArgs);
    cout<<"count result : "<<PyLong_AsLong(result)<<endl;

    Py_DECREF(callbackArgs);
    PyGILState_Release(state);

    return Py_BuildValue("");
}

string GetStringFromPyObject(PyObject* pObject) {
    PyObject* bytes = PyUnicode_AsUTF8String(pObject);
    string str = PyBytes_AsString(bytes);
    Py_DECREF(bytes);
    return str;
}

PyObject* SetSplicingCallback(PyObject* self, PyObject* args) {
    PyObject* callback;
    PyArg_Parse(args, "(O)", &callback);

    PyGILState_STATE state = PyGILState_Ensure();
    PyObject* callbackArgs = Py_BuildValue("(ss)", "hello ", "world");
    PyObject* result = PyEval_CallObject(callback, callbackArgs);
    cout<<"splicing result : "<<GetStringFromPyObject(result)<<endl;

    Py_DECREF(callbackArgs);
    PyGILState_Release(state);

    return Py_BuildValue("");
}

PyObject* SetSplicingAndCountCallback(PyObject* self, PyObject* args) {
    PyObject* callback;
    PyArg_Parse(args, "(O)", &callback);

    PyGILState_STATE state = PyGILState_Ensure();
    PyObject* callbackArgs = Py_BuildValue("(ss)", "hello ", "world");
    PyObject* result = PyEval_CallObject(callback, callbackArgs);
    cout<<"result size : "<<PyTuple_Size(result)<<endl;
    cout<<"item0 : "<<GetStringFromPyObject(PyTuple_GetItem(result, 0))<<endl;
    cout<<"item1 : "<<PyLong_AsLong(PyTuple_GetItem(result, 1))<<endl;

    char* s;
    int i;
    PyArg_Parse(result, "(si)", &s, &i);
    cout<<"s="<<s<<endl;
    cout<<"i="<<i<<endl;

    Py_DECREF(callbackArgs);
    PyGILState_Release(state);

    return Py_BuildValue("");
}

static PyMethodDef g_moduleMethods[] = {
        {"foo", Foo, METH_NOARGS, "function Foo"},
        {"add", Add, METH_VARARGS, "function Add"},
        {"subtract", (PyCFunction)(void(*)(void))Subtract, METH_VARARGS|METH_KEYWORDS, "function Subtract"},
        {"setCountCallback", SetCountCallback, METH_VARARGS, "function SetCountCallback"},
        {"setSplicingCallback", SetSplicingCallback, METH_VARARGS, "function SetSplicingCallback"},
        {"setSplicingAndCountCallback", SetSplicingAndCountCallback, METH_VARARGS, "function SetSplicingAndCountCallback"},
        {NULL, NULL, 0, NULL}
};

static PyModuleDef g_moduleDef = {
        PyModuleDef_HEAD_INIT,
        "ExtendedDemo",
        "C/C++ Python extension demo",
        -1,
        g_moduleMethods
};

PyMODINIT_FUNC PyInit_demo(void) {
        return PyModule_Create(&g_moduleDef);
}
import demo

print(demo.__name__)
print(demo.__doc__)


print(demo.add.__name__)
print(demo.add.__doc__)

def splicing(str1, str2):
    return str1+str2

def count(str1, str2):
    return len(str1) + len(str2)

def splicing_and_count(str1, str2):
    return splicing(str1, str2), count(str1, str2)

demo.foo()
print(demo.add(1,2))
print(demo.subtract(1, 2))
print(demo.subtract(1, b=2))
print(demo.subtract(b=1, a=2))

demo.setCountCallback(count)
demo.setSplicingCallback(splicing)
demo.setSplicingAndCountCallback(splicing_and_count)

def foo(a,b):
    return a-b
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342