最近的項目使用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_VARARGS
、METH_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