以前項目中是C++嵌入Python,開發起來很便利,邏輯業務可以放到python中進行開發,容易修改,以及功能擴展。不過自己沒有詳細的研究過C++嵌入python的細節,這次詳細的研究一下。首先我們簡單的使用C++調用一個Python的py腳本,然后通過Python使用C++中的對象和方法。我們使用的Python是2.7.11
1. 使用C++使用python的功能,比如我們寫一個show.py,代碼如下:
def show(name): return "hello " + name
這個python腳本實在是太簡單了,不需要任何解釋了。然后簡單的寫一個C++函數,來簡單的調用這個show.py中的函數show:
#include <Python.h> #include <iostream> using namespace std; void python_test() { Py_SetPythonHome("D:/Python27"); Py_Initialize(); PyObject* pModule = PyImport_ImportModule("show"); if (!pModule) { cout << "import python module test failed." << endl; return; } PyObject* pFunc = PyObject_GetAttrString(pModule, "show"); if (!pFunc) { cout << "import python func failed." << endl; return; } PyObject* pParam = Py_BuildValue("(s)", "hahaha"); PyObject* pResult = PyObject_CallObject(pFunc, pParam); if (pResult) { char* pBuf = NULL; int nBufSize = 0; //if (PyArg_ParseTuple(pResult, "si", &pBuf, &nBufSize)) //{ // cout << "buf=" << pBuf << endl; // cout << "buf size=" << nBufSize << endl; //} if (PyArg_Parse(pResult, "s", &pBuf)) { cout << "buf=" << pBuf << endl; } } Py_DECREF(pParam); Py_DECREF(pResult); Py_Finalize(); return; } int main() { python_test(); }
這里需要注意幾個地方:
(1) 首先Py_SetPythonHome("D:/Python27");這條語句是用來設置Python腳本的目錄的,如果不設置這個目錄,我們就不知道Python去哪個目錄去讀取我們的腳本了
(2) Py_BuildValue("(s)", "hahaha"); 即使這里只需要一個參數,也需要使用tuple這樣的方式傳遞參數,否則python解析模塊會報錯,它內部做了參數類型判斷。
(3) 如果show函數返回的是一個普通參數,我們使用PyArg_Parse來解析, 如果是一個元組,我們使用PyArg_ParseTuple來解析。不能混用,否則內部也會報錯。
從代碼上看起來也是非常的簡單,不過一開始部署環境不是這么簡單,因為我們默認安裝的Python是沒有python27_d.lib 和python27_d.dll這2個文件的,具體如何解決python環境的問題,看第二節。
2. python代碼編譯
之前說了默認安裝的Python是沒有python27_d.lib 和python27_d.dll這2個文件的,我嘗試過從網上單獨下載這2個文件,不過小版本號不匹配也會出現運行時的crash。所以我選擇了從網上下載了python2.7.11源代碼,然后自己進行編譯。不過這個編譯總的來說是非常簡單的,我是在windows下使用vs2012編譯的,主要需要注意一下幾點:
(1) 使用PCbuild中的pcbuild.sln,直接打開,然后再vs中選中python項目,直接編譯,我的編譯過程是沒有出現任何錯誤的,有些警告,我們直接忽略。在debug模式下,直接生成python27_d.lib 和python27_d.dll,還有python27_d.exe
(2) 雖然我們有了python27_d.lib 和python27_d.dll,然后再在之前測試工程中添加好include和lib的目錄,編譯的時候,會提示找不到pyconfig.h這個文件。 這是因為這個文件是windows下特有的,我們需要在PC目錄中copy這個文件到include目錄下,然后再編譯,就可以正常完成了。
3. python調用C++對象
python調用C++接口,有多種辦法。通常python調用C++接口是一種功能擴展,將C++功能編譯成動態庫,然后python通過ctypes來導入庫文件,在windows下是dll,linux下是so文件,這個使用比較簡單,而且例子非常多,就不再介紹了。在很多復雜項目中,比如游戲中,我們希望python中操作玩家對象,那么如果將這個模塊導成庫文件,非常麻煩,而且不方便操作玩家對象實例。我們采用動態的方式來進行:
示例中我們新建2個c++類,一個Person,一個PersonWrap。 Person對象代表某個人,PersonWrap是針對Person的Python接口擴展,Person.h代碼如下:
#ifndef PERSON_H #define PERSON_H #include <string> using namespace std; class Person { public: Person(){} ~Person(){} void setName(const string& _name) { name = _name; } string& getName() { return name; } private: string name; }; #endif
PersonWrap.h代碼如下:
#ifndef PERSON_WRAP_H #define PERSON_WRAP_H #include <Python.h> #include "Person.h" class PersonWrap { public: static PyObject* SetName(PyObject* self, PyObject* args) { cout << "Wrap setName" << endl; int personAddr = 0; char* pName = NULL; if (!PyArg_ParseTuple(args, "is", &personAddr, &pName)) { return NULL; } Person* p = (Person*)personAddr; p->setName(pName); return Py_BuildValue("i", 0); } static PyObject* GetName(PyObject* self, PyObject* args) { int personAddr = 0; if (!PyArg_ParseTuple(args, "i", &personAddr)) { return NULL; } Person* p = (Person*)personAddr; string& name = p->getName(); return Py_BuildValue("s", name.c_str()); } static PyObject* GetDesc(PyObject* self, PyObject* args) { return Py_BuildValue("s", "whoknows"); } static PyObject* CreatePerson(PyObject* self, PyObject* args) { cout << "Wrap create person" << endl; char* pName = NULL; if (!PyArg_ParseTuple(args, "s", &pName)) { return NULL; } Person* p = new Person(); p->setName(pName); return Py_BuildValue("i", p); } }; PyMethodDef WrapMethods[] = { {"SetName", PersonWrap::SetName, METH_VARARGS, "SetName"}, {"GetName", PersonWrap::GetName, METH_VARARGS, "GetName"}, {"GetDesc", PersonWrap::GetDesc, METH_VARARGS, "GetDesc"}, {"CreatePerson", PersonWrap::CreatePerson, METH_VARARGS, "CreatePerson"}, {NULL, NULL, 0, NULL} }; PyMODINIT_FUNC initPythonWrap() { PyObject* module; module = Py_InitModule("PythonWrap", WrapMethods); } #endif
Person類的代碼非常簡單,不需要解釋了。PersonWrap針對Person的接口進行了封裝,可以看出PersonWrap中都是static函數,這是為了能夠導出接口供Python使用,同時定義一個WrapMethods數組,將這些接口用Python方法的定義方式導出。最后用一個initPythonWrap方法來對這個模塊進行動態初始化。這樣我們的ptyhon腳本就可以調用了。
不過需要在main函數中,新增initPythonWrap();的調用,否則不會生效。我們測試的python代碼如下:
import sys import PythonWrap def show(name): person = PythonWrap.CreatePerson("tom") PythonWrap.SetName(person, "jim") return "hello " + PythonWrap.GetName(person)
從python腳本可以看出,我們import了我們動態生成的PythonWrap模塊,然后就能像其他普通python模塊一樣隨意的調用其接口。需要注意的是,我們的Person對象是一個C++對象,根據文檔查詢,如果不利於第三方插件是不能直接將C++對象傳遞給Python的,所以我們在傳出和傳入的時候,是將Person對象指針通過整數的方式進行傳遞的,然后強制進行指針轉換。
如果在debug模式下,在我們調用python的show函數時,使用到了PythonWrap中的接口時,會進入我們的C++代碼,調試起來還是比較方便的。不過還存在一些問題,就是如果我們的SetName接口寫成這樣:
static PyObject* SetName(PyObject* self, PyObject* args) { cout << "Wrap setName" << endl; int personAddr = 0; char* pName = NULL; if (!PyArg_ParseTuple(args, "is", &personAddr, &pName)) { return NULL; } Person* p = (Person*)personAddr; p->setName(pName); return NULL; }
那么這個接口就只能被調用一次,第二次調用的時候就不會再觸發,應該是針對返回值,python模塊代碼中做了特殊的處理,現在還不知道具體原因。等后面弄明白之后再來補充。