序: 為什么要集成腳本,怎么在工程中集成Python腳本。
在做比較大型的工程時,一般都會分核心層和業務層。核心層要求實現高效和穩定的基礎功能,並提供調用接口供業務層調用的一種標准的框架划分。在實際中根據需求會拆分的更細。外部的表現形式就是一個核心動態庫,帶着一堆業務業務動態庫。通過一個調度程序把這些鏈接起來,外加一堆配置文件,就形成一個完成的項目。
這種模式在一個團隊開發中,工作職責比較容易划分。制定API接口后,開發工作基本可以並行實現,包括后期的功能測試(白盒、黑盒)。不管工程使用什么語言,基本都是如此。
c語言無疑是很強大而又靈活的,但是開發比較復雜,開發工期比較長。全部使用c/c++ 進行開發的話,編譯調試整合發布,就需要大量時間,包括運營維護的話,呵呵~ 。整個產品的生命周期需要投入大量的人力來維持這個產品。特別是對做定制的最終用戶的話,那就可能就是一場曠日持久戰。
剛才說的一般工程都會包括2方面,核心和擴展。是公司級產品的話,一定會出於盈利問題,會對核心做保密(商業機密)。擴展是在核心基礎上實現的,很大程度上保密等級就沒那么高,甚至可以是開放式的。方便有一定能力的用戶直接擴展。這是最理想的一種情況。
如果業務層還是使用c/c++的話,估計用戶沒有幾個有能力或者說不太願意去做擴展。只能公司團隊進行維護和擴展。當然開源項目除外,活躍的開源項目還是有很多大俠們願意去擴展的。當降低擴展的難度,不僅可以縮短開發周期降低成本,降低運營維護成本。如果產品夠優秀,還能吸引有一定能力的客戶來幫忙做擴展。擴展簡單方便,用戶自己都能擴展,項目運營成本必定降低。這就皆大歡喜。
腳本是一個非常方便的東西,不需要編譯直接運行就能看到結果。不用考慮大量系統相關的開發技巧,更貼近實際業務的描述,修正問題不需要重新編譯發布,維護非常方便。能大大提供生產效率,這就是我們所須要的。現在流行的腳本語言有很多Ruby、Perl、Python、Lua、Javascript等等。。。,反正很多。
腳本語言比較:
1、python: 簡單易學,有大量擴展可以使用。
2、Ruby: 魔幻型純面向對象語言,非常靈活,學習相對其他語言有一定難度。
3、Lua: 超輕量級,性能高效。很多游戲使用Lua提供擴展。
使用那種腳本作為擴展,要看實際項目和現有資源的情況而定。本文只介紹python的集成。
廢話這么多,主要的目的就下面幾個。
擴展目的:
1、高可配性。 解決一些簡單配置不能實現的組織、回調功能,避免改動重編重發布。
2、為了使用已有的庫。 如原來你有很多積累,寫了一些適合工程開發的庫。
3、優化程序提升性能。 對腳本程序使用庫的一種情況。
集成: 就是通過簡單、直接和快速的在不同語言直接調度切換控制,屬於無縫連接。就像使用同一種語言在不同的動態庫之間調度。而不是那種使用套接字和管道等的間接調度。
混合開發中,不管python或C都可以作為“上層”。因此兩方面都要提供入口提供對方調度。這個其實和正常的同一語言編寫的插件模式是一致的。核心提供接口和注冊入口,擴展注冊入口並調用接口。
嵌入接口: C程序中運行Python的代碼
擴展接口: Python程序中運行C的代碼庫
測試環境:
window 7
vs2015
python 3.5
1、准備環境
使用的Python是3.5版本的。
首先肯定是須要個python的運行環境,可以直接從官網python.org下載Python3.x 進行安裝。建議直接下源代碼編譯,因為里面有很多代碼可以參考。
windows配置
python安裝路徑以下為例d:\python
,在系統的用戶環境變量中添加。
1、 增加python搜索路徑,方便代碼運行調試
path=d:\python;d:\python\pcbuild\win32
2、 增加python環境路徑。加載模塊時默認會從配置路徑中搜索。
PYTHONPATH=.;d:\python\lib;d:\python\pcbuild\win32;D:\Python\Lib\site-packages;d:\python
3、 增加編譯路徑。
PyInc=d:\python\include;d:\python\pc
PyLib=d:\python\pcbuild\win32
方便VS搜索路徑配置。在工程中,只需引用配置變量路徑,可以直接使用$(PyInc) 和 $(PyLib)。
提示: 由於是自己編譯的,一些環境參數沒有須要自己加。注意大小寫
2.1、嵌入Python第一個簡單工程
創建測試工程
在VS中創建一個空的VC++控制台程序。在工程選擇的搜索目錄中加入$(PyInc)
。
選擇菜單:Project->Properties...
在VC++ Directories
分類的Include Directories
中加入先前定義的環境變量$(PyInc)
。確保Python的頭文件能搜索到。
使用環境變量的一個好處是,方便不同機器不同環境的切換,不需要修改工程配置。
提示: 可以不建工程,直接使用makefile方式進行編譯。
創建script.py腳本
在工程中創建一個新文件,直接改名為script.py
,復制下面內容。
注意: 編碼設置為UTF-8
"""
在C中調用Python模塊運行。
調用時須要把文件放在程序當前運行目錄(保證在搜索目錄中)。
www.moguf.com 2016-05-28
"""
message = 'hello life...'
def transform(input):
input = input.replace('life', 'Python')
return input.upper()
在Python環境中運行這個腳本,可以得到下面結果。可以看到打印出 hello life...
和轉換后的字符串 HELLO PYTHON...
Microsoft Windows [版本 6.1.7601]
版權所有 (c) 2009 Microsoft Corporation。保留所有權利。
C:\Users\CrystalIce>cd /d D:\Dev\MySimple\pythoninc\embedsimple
D:\Dev\MySimple\pythoninc\embedsimple>python
Running Release|Win32 interpreter...
Python 3.5.0 (default, Nov 4 2015, 21:58:28) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import script
>>> print(script.message)
hello life...
>>> x = script.message
>>> print(script.transform(x))
HELLO PYTHON...
>>>
創建hello.c
在工程中添加一個新文件,命名為hello.c
。復制下面內容。
//
// C code runs Python code in this module in embedded mode.
// print hello string
//
// www.moguf.com 2016-05-28
//
#include <python.h>
int main()
{
Py_Initialize();
PyRun_SimpleString("print('run python ...')");
PyRun_SimpleString("import script");
PyRun_SimpleString("print(script.message)");
PyRun_SimpleString("x = script.message");
PyRun_SimpleString("print(script.transform(x))");
Py_Finalize();
}
運行程序
在程序最后下個斷點,方便查看運行結果。直接運行程序可以看到下面結果。
run python ...
hello life...
HELLO PYTHON...
基本調用流程解析
使用C程序運行Python腳本代碼,可以通過使用Python字符串,調用Python對象或模板之類的所有操作。
流程:
1、初始化Python解析器
2、執行Python代碼,字符串,對象或模塊。
3、關閉Python解析器。
上面代碼嵌入過程很容易。但在實際使用中想要更好的整合,須要了解提供的API和不同語言之間的轉換。
Python嵌入C的基礎API
下面幾個基礎API,在C中能很容易的執行Python腳本中的代碼。包括字典、數組和對象。當然想要更好的混合交互須要熟悉所有的API。
C API 調用 | Python 對應 |
---|---|
PyImport_ImportModel | import module |
PyImport_ReloadModule | reload(module) |
PyImport_GetModuleDict | module.__dict__ |
PyDict_GetItemString | dict[key] |
PyDict_SetItemString | dict[key] = value |
PyDict_New | dict = {} |
PyObject_GetAttrString | getattr(obj, attr) |
PyObject_SetAttrString | setattr(obj, attr, val) |
PyObject_CallObject | funcobj(*argstuple) |
PyEval_CallObject | funcobj(*argstuple) |
PyRun_String | eval(exprstr) , exec(stmtstr) |
PyRun_File | exec(open(filename().read()) |
建議: 去官網下載一個手冊,方便查看API。https://docs.python.org/3/download.html
2.2、使用C擴展Python
2.1的內容只是通過C程序調用Python腳本,要讓Python腳本能調用C代碼,就須要擴展。用C擴展Python功能那就簡單很多。有很多實例可以參考。Python源代碼就是寶庫。
創建擴展工程 hello
在VS中創建一個空的動態庫hello
工程(先不要改名)。在工程配置中增加搜索路徑。
在VS生成時有些特殊。生成的后綴選擇.pyd主要是為防止和系統.dll產生沖突。
在工程選項界面中設置工程輸出名稱為$(ProjectName)_d
,輸出擴展名稱為.pyd
。
並在Linker頁面Input組中設置庫依賴為python35_d.lib
不同的編譯模式的設置:
Release 下使用的依賴庫為pythonXY.lib
Debug 下使用依賴庫為pythonXY_d.lib
注意:
debug 模式生成應為 hello_d.pyd
release 模式生成應為 hello.pyd
創建hello.c擴展代碼
在工程中新建hello.c文件,復制下面內容。
//
// A simple C extension module for python, called "hello"
//
// www.moguf.com 2016-05-28
//
#include <python.h>
#include <string.h>
//
// module functions
//
static PyObject * // returns object
message(PyObject *self, PyObject *args)
{
char *fromPython, result[1024];
if (!PyArg_Parse(args, "(s)", &fromPython)) // convert Python -> C
return NULL; // exception null = raise
else {
strcpy(result, "Hello , "); // build up C string
strcat(result, fromPython); // add passed Python string
return Py_BuildValue("s", result); // convert C -> Python
}
}
//
// registration methods table
static PyMethodDef hello_methods[] = {
{ "message", message, METH_VARARGS, "func doc" }, // format: name, &func, fmt, doc
{ NULL, NULL, 0, NULL } // end
};
// module definition structure
static struct PyModuleDef hellomodule = {
PyModuleDef_HEAD_INIT,
"hello", // module name
"mod doc", // module documentation,
-1,
hello_methods // methods table
};
//
// module initializer
PyMODINIT_FUNC
PyInit_hello()
{
return PyModule_Create(&hellomodule);
}
上面的代碼主要分為四塊。
第一塊: 模塊功能實現函數
第二塊: 注冊功能函數
第三塊: 定義模塊申明
第四塊: 初始化模塊。動態加載就不需要這塊內容,集成時會使用動態加載。
通過上述定義為Python腳本調用提供訪問入口,這就是通常所說的膠水代碼。具體定義直接看代碼注釋,就不啰嗦了。
這里須要注意的是定義中的名稱hello。在第三塊模型注冊的時候是名稱為hello、第四塊中函數的初始化名稱PyInit_hello()
。在python3中的名稱規定比較嚴格,初始化函數名稱格式為PyInit_xxx
, xxx為注冊的模塊名稱。
即對Python擴展工程中的工程名稱
,注冊名稱
和初始化名稱
須要保持一致。
編譯測試運行
編譯hello工程(debug版本),在Python調試版本下運行。python調試環境使用python_d
命令進入。
可以看到下面結果,就說明OK了
Microsoft Windows [版本 6.1.7601]
版權所有 (c) 2009 Microsoft Corporation。保留所有權利。
C:\Users\CrystalIce>cd /d D:\Dev\MySimple\pythoninc\Debug
D:\Dev\MySimple\pythoninc\Debug>dir
驅動器 D 中的卷是 Docs
卷的序列號是 0002-2203
D:\Dev\MySimple\pythoninc\Debug 的目錄
2016-05-28 23:10 <dir> .
2016-05-28 23:10 <dir> ..
2016-05-28 23:10 639 hello_d.exp
2016-05-28 23:10 247,520 hello_d.ilk
2016-05-28 23:10 1,718 hello_d.lib
2016-05-28 23:10 503,808 hello_d.pdb
2016-05-28 23:10 35,840 hello_d.pyd
5 個文件 789,525 字節
2 個目錄 27,394,551,808 可用字節
D:\Dev\MySimple\pythoninc\Debug>python_d
Python 3.5.0 (default, Nov 4 2015, 21:57:44) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello
>>> print(hello.message('C'))
Hello , C
>>> print(hello.message('module ' + hello.__file__))
Hello , module D:\Dev\MySimple\pythoninc\Debug\hello_d.pyd
>>>
如果在運行調試中出現下面情況,是Python找不到hello模塊導致的。
>>> import hello
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named 'hello'
大致原因:
編譯的模塊名稱有問題,加載不到模塊。Python在debug環境下會調用 xxx_d.pyd,release下會調用 xxx.pyd。
創建hellouse.py 調用hello擴展模塊
在工程中新建一個hellouse.py,用於調度hello擴展模塊。
注意: 編碼設置為UTF-8
內容如下:
"""
import and use a C extension library module
www.moguf.com 2016-05-28
"""
import hello
print(hello.message('C'))
print(hello.message('module ' + hello.__file__))
把這個腳本復制到hello_d.pyd
擴展庫所在目錄,並執行 。可以看到和剛才測試輸出的結果是一致的。
D:\Dev\MySimple\pythoninc\Debug>python_d hellouse.py
Hello , C
Hello , module D:\Dev\MySimple\pythoninc\Debug\hello_d.pyd
相關編譯問題:
如果自己建的工程編譯或調試,老出現狀況。可以直接使用Python提供的PC\example_nt
VS示例工程作為參考。
2.3、集成Python,實現雙工
先前的的兩個示例都是單方面調用,c調用Python 和 Python調用c的擴展模型。並沒有交互。在實際工程中不太可能有這種情況,一定是相互交叉調用。
創建duplex工程
在VS中創建一個空的控制台程序,並設置Python代碼搜索路徑,參照2.1。
創建duplex.c 文件
這個原文件包括了腳本調用和膠水代碼的實現。和2.1、2.2的內容基本一致。其中主要的差異在Python模塊的注冊上
PyImport_AppendInittab("hello_api", &PyInit_hello_api);
實際對外注冊的模塊在程序啟動時執行,並沒有作導出。
文件內容如下。
//
// c API module, test c embedding and extending
//
// www.moguf.com 2016-05-29
//
#include <python.h>
#include <string.h>
void helloWorld(char *param)
{
if (param)
printf("It's c, hello %s", param);
else
printf("It's c, hello ");
}
static PyObject *
message(PyObject *self, PyObject *args)
{
char *fromPython;
if (!PyArg_Parse(args, "(s)", &fromPython))
helloWorld(NULL);
else
helloWorld(fromPython);
return Py_BuildValue("");
}
static PyMethodDef hello_methods[] = {
{ "message", message, METH_VARARGS, "func doc" },
{ NULL, NULL, 0, NULL } // end
};
static struct PyModuleDef hello_api = {
PyModuleDef_HEAD_INIT,
"hello_api",
"mod doc",
-1,
hello_methods
};
static PyObject*
PyInit_hello_api(void)
{
return PyModule_Create(&hello_api);
}
int main(int argc, char** argv)
{
PyObject* module;
PyObject* func;
// add c api to modules
PyImport_AppendInittab("hello_api", &PyInit_hello_api);
Py_Initialize();
if (!Py_IsInitialized()) {
PyErr_Print();
printf("Couldn't init python");
return -1;
}
module = PyImport_ImportModule("plugins");
if (module) {
func = PyObject_GetAttrString(module, "helloWorld");
if (func && PyCallable_Check(func)) {
PyObject* pArgs = NULL;
PyObject* pReturnVal = PyObject_CallObject(func, pArgs);
}
else {
PyErr_Print();
printf("error: no func\n");
}
Py_XDECREF(func);
Py_DECREF(module);
}
else {
PyErr_Print();
printf("err: no module");
}
Py_Finalize();
return 0;
}
創建腳本plugins.py
內容如下
"""
Module to test c embedding and extending
www.moguf.com 2016-05-29
"""
import hello_api
def helloWorld():
print("it's Python, Hello C")
hello_api.message('python')
return
運行測試
在 main函數結束的位置設置斷點,這用方便查看結果。運行程序。
it's Python, Hello C
It's c, hello python
可以看到,第一行打印是由Python腳本實現輸出,第二行是由python調用程序的API實現打印輸出。
3、后續
通過上述簡單的三個實例實現了c語言和Python腳本的集成。簡單、直接和快速的在不同語言直接調度切換控制。
由於Python開始時本身就是基於C寫的,所有對c的支持是非常好的。能在c/c++中很方便的進行集成。不過要想更好的實現腳本和C進行交互,那就須要熟悉並使用提供的API。
后續將會使用Python腳本作為插件擴展一種模式,在實際工程中實現業務的一些方案。
相關參考:
1、官方幫助 https://docs.python.org/3/
2、源代碼:https://github.com/cmacro/simple/tree/master/pythoninc