工程腳本插件方案 - c集成Python基礎篇

序: 爲何要集成腳本,怎麼在工程中集成Python腳本。html

在作比較大型的工程時,通常都會分核心層和業務層。核心層要求實現高效和穩定的基礎功能,並提供調用接口供業務層調用的一種標準的框架劃分。在實際中根據需求會拆分的更細。外部的表現形式就是一個核心動態庫,帶着一堆業務業務動態庫。經過一個調度程序把這些連接起來,外加一堆配置文件,就造成一個完成的項目。python

這種模式在一個團隊開發中,工做職責比較容易劃分。制定API接口後,開發工做基本能夠並行實現,包括後期的功能測試(白盒、黑盒)。無論工程使用什麼語言,基本都是如此。c++

c語言無疑是很強大而又靈活的,可是開發比較複雜,開發工期比較長。所有使用c/c++ 進行開發的話,編譯調試整合發佈,就須要大量時間,包括運營維護的話,呵呵~ 。整個產品的生命週期須要投入大量的人力來維持這個產品。特別是對作定製的最終用戶的話,那就可能就是一場曠日持久戰。git

剛纔說的通常工程都會包括2方面,核心和擴展。是公司級產品的話,必定會出於盈利問題,會對核心作保密(商業機密)。擴展是在覈心基礎上實現的,很大程度上保密等級就沒那麼高,甚至能夠是開放式的。方便有必定能力的用戶直接擴展。這是最理想的一種狀況。github

若是業務層仍是使用c/c++的話,估計用戶沒有幾個有能力或者說不太願意去作擴展。只能公司團隊進行維護和擴展。固然開源項目除外,活躍的開源項目仍是有不少大俠們願意去擴展的。當下降擴展的難度,不只能夠縮短開發週期下降成本,下降運營維護成本。若是產品夠優秀,還能吸引有必定能力的客戶來幫忙作擴展。擴展簡單方便,用戶本身都能擴展,項目運營成本一定下降。這就皆大歡喜。windows

腳本是一個很是方便的東西,不須要編譯直接運行就能看到結果。不用考慮大量系統相關的開發技巧,更貼近實際業務的描述,修正問題不須要從新編譯發佈,維護很是方便。能大大提供生產效率,這就是咱們所需要的。如今流行的腳本語言有不少Ruby、Perl、Python、Lua、Javascript等等。。。,反正不少。api

腳本語言比較:數組

一、python: 簡單易學,有大量擴展可使用。
二、Ruby: 魔幻型純面嚮對象語言,很是靈活,學習相對其餘語言有必定難度。
三、Lua: 超輕量級,性能高效。不少遊戲使用Lua提供擴展。框架

使用那種腳本做爲擴展,要看實際項目和現有資源的狀況而定。本文只介紹python的集成。函數

廢話這麼多,主要的目的就下面幾個。

擴展目的:

一、高可配性。 解決一些簡單配置不能實現的組織、回調功能,避免改動重編重發布。
二、爲了使用已有的庫。 如原來你有不少積累,寫了一些適合工程開發的庫。
三、優化程序提高性能。 對腳本程序使用庫的一種狀況。

集成: 就是經過簡單、直接和快速的在不一樣語言直接調度切換控制,屬於無縫鏈接。就像使用同一種語言在不一樣的動態庫之間調度。而不是那種使用套接字和管道等的間接調度。

混合開發中,無論python或C均可以做爲「上層」。所以兩方面都要提供入口提供對方調度。這個其實和正常的同一語言編寫的插件模式是一致的。核心提供接口和註冊入口,擴展註冊入口並調用接口。

嵌入接口: C程序中運行Python的代碼
擴展接口: Python程序中運行C的代碼庫

測試環境:
window 7
vs2015
python 3.5

一、準備環境

使用的Python是3.5版本的。

首先確定是需要個python的運行環境,能夠直接從官網python.org下載Python3.x 進行安裝。建議直接下源代碼編譯,由於裏面有不少代碼能夠參考。

windows配置

python安裝路徑如下爲例d:\python,在系統的用戶環境變量中添加。

一、 增長python搜索路徑,方便代碼運行調試

path=d:\python;d:\python\pcbuild\win32

二、 增長python環境路徑。加載模塊時默認會從配置路徑中搜索。

PYTHONPATH=.;d:\python\lib;d:\python\pcbuild\win32;D:\Python\Lib\site-packages;d:\python

三、 增長編譯路徑。

PyInc=d:\python\include;d:\python\pc
PyLib=d:\python\pcbuild\win32

方便VS搜索路徑配置。在工程中,只需引用配置變量路徑,能夠直接使用$(PyInc) 和 $(PyLib)。

提示: 因爲是本身編譯的,一些環境參數沒有需要本身加。注意大小寫

2.一、嵌入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對象或模板之類的全部操做。

流程:
一、初始化Python解析器
二、執行Python代碼,字符串,對象或模塊。
三、關閉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.二、使用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_ntVS示例工程做爲參考。

2.三、集成Python,實現雙工

先前的的兩個示例都是單方面調用,c調用Python 和 Python調用c的擴展模型。並無交互。在實際工程中不太可能有這種狀況,必定是相互交叉調用。

建立duplex工程

在VS中建立一個空的控制檯程序,並設置Python代碼搜索路徑,參照2.1。

建立duplex.c 文件

這個原文件包括了腳本調用和膠水代碼的實現。和2.一、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實現打印輸出。

三、後續

經過上述簡單的三個實例實現了c語言和Python腳本的集成。簡單、直接和快速的在不一樣語言直接調度切換控制。

因爲Python開始時自己就是基於C寫的,全部對c的支持是很是好的。能在c/c++中很方便的進行集成。不過要想更好的實現腳本和C進行交互,那就需要熟悉並使用提供的API。

後續將會使用Python腳本做爲插件擴展一種模式,在實際工程中實現業務的一些方案。

相關參考:
一、官方幫助 https://docs.python.org/3/
二、源代碼:https://github.com/cmacro/simple/tree/master/pythoninc

相關文章
相關標籤/搜索