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罐栈,而且不同編譯器的修改規(guī)則還不一樣黍衙,所以在python里面用add找不到對應的函數。

加上extern "C"包裹之后能讓編譯器按照c的方式去編譯這個函數荠诬,不對函數名做額外的修改琅翻,這樣python里面才能通過函數名去調用它。

cffi

cffi和ctypes類似柑贞,但是稍微復雜一些方椎。

cffi的功能其實是在python里面寫c代碼,我們可以通過python里面寫的c代碼去調用第三庫的c代碼钧嘶。

這么說可能有點抽象棠众,我舉個例子大家可能就好理解了:

我們在c里面實現(xiàn)了一個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!") # 創(chuàng)建char數組
print(lib.foo(param))                      # 調用之前聲明的foo方法,它的實現(xiàn)在ffidemo.so

c/c++拓展模塊

使用ctypes的方式雖然簡便,但是在使用上能明顯的感覺出來是在調用so庫的代碼书幕。

更不用說使用cffi會在python代碼里面嵌入c的語句新荤,總有種莫名的不協(xié)調感。

而且c/c++編碼規(guī)范里一般方法名會用駝峰按咒,但是python編碼規(guī)范里建議方法名用下劃線分割單詞迟隅,上面的兩種方法都會造成python里面調用so和python腳本的方法有兩種命名規(guī)范但骨,逼死強迫癥。

有沒有一種方法可能讓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系統(tǒng)下):

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語句就會去動態(tài)鏈接這個demo.so闷哆,并且調用PyInit_demo方法。也就是說so的名字要和PyInit_XXX這個方法名對應单起,要不然python里面會報找不到init方法的異常抱怔。

這個init方法很簡單,就是創(chuàng)建了一個module嘀倒。這個module的定義在g_moduleDef這個全局變量里面屈留,它定義了module的name、documentation等测蘑,這里的name可以和so的名字不一樣灌危,它在python里module的__name__、__doc__里面體現(xiàn):

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里面如果給它傳參就會出現(xiàn)異常:

import demo
demo.foo(1)

# 出現(xiàn)異常
# 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

通過上面的講解我們可以輕松實現(xiàn)python對c/c++函數的調用穗熬。但是我們的項目還出現(xiàn)了python往c/c++里面設置回調函數的需求,我們接下來就來看看這個需求要怎么實現(xiàn)丁溅。

下面是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
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市努酸,隨后出現(xiàn)的幾起案子服爷,更是在濱河造成了極大的恐慌,老刑警劉巖获诈,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件举庶,死亡現(xiàn)場離奇詭異,居然都是意外死亡几颜,警方通過查閱死者的電腦和手機获列,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亡嫌,“玉大人嚎于,你說我怎么就攤上這事⌒冢” “怎么了于购?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長知染。 經常有香客問我肋僧,道長,這世上最難降的妖魔是什么控淡? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任嫌吠,我火速辦了婚禮,結果婚禮上掺炭,老公的妹妹穿的比我還像新娘辫诅。我一直安慰自己,他們只是感情好涧狮,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布炕矮。 她就那樣靜靜地躺著,像睡著了一般者冤。 火紅的嫁衣襯著肌膚如雪吧享。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天譬嚣,我揣著相機與錄音钢颂,去河邊找鬼。 笑死拜银,一個胖子當著我的面吹牛殊鞭,可吹牛的內容都是我干的遭垛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼操灿,長吁一口氣:“原來是場噩夢啊……” “哼锯仪!你這毒婦竟也來了?” 一聲冷哼從身側響起趾盐,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤庶喜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后救鲤,有當地人在樹林里發(fā)現(xiàn)了一具尸體久窟,經...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年本缠,在試婚紗的時候發(fā)現(xiàn)自己被綠了斥扛。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡丹锹,死狀恐怖稀颁,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情楣黍,我是刑警寧澤匾灶,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站租漂,受9級特大地震影響粘昨,放射性物質發(fā)生泄漏。R本人自食惡果不足惜窜锯,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芭析。 院中可真熱鬧锚扎,春花似錦、人聲如沸馁启。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惯疙。三九已至翠勉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間霉颠,已是汗流浹背对碌。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蒿偎,地道東北人朽们。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓怀读,卻偏偏與公主長得像,于是被迫代替她去往敵國和親骑脱。 傳聞我的和親對象是個殘疾皇子菜枷,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345