C++ 調用Python3


作為一種膠水語言,Python 能夠很容易地調用 C 、 C++ 等語言,也能夠通過其他語言調用 Python 的模塊。

Python 提供了 C++ 庫,使得開發者能很方便地從 C++ 程序中調用 Python 模塊。

具體操作可以參考: 官方文檔

在調用Python模塊時需要如下步驟:

  1. 初始化Python調用環境
  2. 加載對應的Python模塊
  3. 加載對應的Python函數
  4. 將參數轉化為Python元組類型
  5. 調用Python函數並傳入參數元組
  6. 獲取返回值
  7. 根據Python函數的定義解析返回值

初始化

在調用Python模塊時需要首先包含Python.h頭文件,這個頭文件一般在安裝的Python目錄中的 include文件中,所在VS中首先需要將這個路徑加入到項目中

包含完成之后可能會抱一個錯誤:找不到 inttypes.h文件,在個錯誤在Windows平台上很常見,如果報這個錯誤,需要去網上下載對應的inttypes.h文件然后放入到對應的目錄中即可,我這放到VC的include目錄中

在包含這些文件完成之后可能還會抱一個錯誤,未找到Python36_d.lib 在Python環境中確實找不到這個文件,這個時候可以修改pyconfig.h文件,將這個lib改為python36.lib,具體操作請參考這個鏈接: https://blog.csdn.net/Chris_zhangrx/article/details/78947526

還有一點要注意,下載的Python環境必須的與目標程序的類型相同,比如你在VS 中新建一個Win32項目,在引用Python環境的時候就需要引用32位版本的Python

這些准備工作做完后在調用Python前先調用Py_Initialize 函數來初始化Python環境,之后我們可以調用Py_IsInitialized來檢測Python環境是否初始化成功
下面是一個初始化Python環境的例子

BOOL Init()
{
	Py_Initialize();

	return Py_IsInitialized();
}

調用Python模塊

調用Python模塊可以簡單的調用Python語句也可以調用Python模塊中的函數。

簡單調用Python語句

針對簡單的Python語句(就好像我們在Python的交互式環境中輸入的一條語句那樣),可以直接調用 PyRun_SimpleString 函數來執行, 這個函數需要一個Python語句的ANSI字符串作為參數,返回int型的值。如果為0表示執行成功否則為失敗

void ChangePyWorkPath(LPCTSTR lpWorkPath)
{
	TCHAR szWorkPath[MAX_PATH + 64] = _T("");
	StringCchCopy(szWorkPath, MAX_PATH + 64, _T("sys.path.append(\""));
	StringCchCat(szWorkPath, MAX_PATH + 64, lpWorkPath);
	StringCchCat(szWorkPath, MAX_PATH + 64, _T("\")"));

	PyRun_SimpleString("import sys");
	USES_CONVERSION;

	int nRet = PyRun_SimpleString(T2A(szWorkPath));
	if (nRet != 0)
	{
		return;
	}
}

這個函數主要用來將傳入的路徑加入到當前Python的執行環境中,以便可以很方便的導入我們的自定義模塊
函數首先通過字符串拼接的方式組織了一個 "sys.path.append('path')" 這樣的字符串,其中path是我們傳進來的參數,然后調用PyRun_SimpleString執行Python的"import sys"語句來導入sys模塊,接着執行之前拼接的語句,將對應路徑加入到Python環境中

調用Python模塊中的函數

調用Python模塊中的函數需要執行之前說的2~7的步驟

  1. 加載Python模塊(自定義模塊)

加載Python的模塊需要調用 PyImport_ImportModule 這個函數需要傳入一個模塊的名稱作為參數,注意:這里需要傳入的是模塊的名稱也就是py文件的名稱,不能帶.py后綴。

這個函數會返回一個Python對象的指針,在C++中表示為PyObject。這里返回模塊的對象指針

  1. 然后調用 PyObject_GetAttrString 函數來加載對應的Python模塊中的方法,這個函數需要兩個參數,第一個是之前獲取到的對應模塊的指針,第二個參數是函數名稱的ANSI字符串。這個函數會返回一個對應Python函數的對象指針。后面需要利用這個指針來調用Python函數

獲取到函數的指針之后我們可以調用 PyCallable_Check 來檢測一下對應的對象是否可以被調用,如果能被調用這個函數會返回true否則返回false

  1. 接着就是傳入參數了,Python中函數的參數以元組的方式傳入的,所以這里需要先將要傳入的參數轉化為元組,然后調用 PyObject_CallObject 函數來執行對應的Python函數。這個函數需要兩個參數第一個是上面Python函數對象的指針,第二個參數是需要傳入Python函數中的參數組成的元組。函數會返回Python的元組對象,這個元組就是Python函數的返回值
  2. 獲取到返回值之后就是解析參數了,我們可以使用對應的函數將Python元組轉化為C++中的變量
  3. 最后需要調用 Py_DECREF 來解除Python對象的引用,以便Python的垃圾回收器能正常的回收這些對象的內存

下面是一個傳入空參數的例子

void GetModuleInformation(IN LPCTSTR lpPyFileName, OUT LPTSTR lpVulName, OUT long& level)
{
	USES_CONVERSION;

	PyObject *pModule = PyImport_ImportModule(T2A(lpPyFileName)); //加載模塊
	if (NULL == pModule)
	{
		g_OutputString(_T("加載模塊[%s]失敗"), lpPyFileName);
		goto __CLEAN_UP;
	}

	PyObject *pGetInformationFunc = PyObject_GetAttrString(pModule, "getInformation"); // 加載模塊中的函數
	if (NULL == pGetInformationFunc || !PyCallable_Check(pGetInformationFunc))
	{
		g_OutputString(_T("加載函數[%s]失敗"), _T("getInformation"));
		goto __CLEAN_UP;
	}

	PyObject *PyResult = PyObject_CallObject(pGetInformationFunc, NULL);
	if (NULL != PyResult)
	{
		PyObject *pVulNameObj = PyTuple_GetItem(PyResult, 0);
		PyObject *pVulLevelObj = PyTuple_GetItem(PyResult, 1);

		//獲取漏洞的名稱信息
		int nStrSize = 0;
		LPTSTR pVulName = PyUnicode_AsWideCharString(pVulNameObj, &nStrSize);
		StringCchCopy(lpVulName, MAX_PATH, pVulName);
		PyMem_Free(pVulName);

		//獲取漏洞的危險等級
		level = PyLong_AsLong(pVulLevelObj);

		Py_DECREF(pVulNameObj);
		Py_DECREF(pVulLevelObj);
	}

	//解除Python對象的引用, 以便Python進行垃圾回收
__CLEAN_UP:
	Py_DECREF(pModule);
	Py_DECREF(pGetInformationFunc);
	Py_DECREF(PyResult);

}

在示例中調用了一個叫 getInformation 的函數,這個函數的定義如下:

def getInformation():
    return "測試腳本", 1

下面是一個需要傳入參數的函數調用

BOOL CallScanMethod(IN LPPYTHON_MODULES_DATA pPyModule, IN LPCTSTR lpUrl, IN LPCTSTR lpRequestMethod, OUT LPTSTR lpHasVulUrl, int BuffSize)
{
	USES_CONVERSION;
	//加載模塊
	PyObject* pModule = PyImport_ImportModule(T2A(pPyModule->szModuleName));
	if (NULL == pModule)
	{
		g_OutputString(_T("加載模塊[%s]失敗!!!"), pPyModule->szModuleName);
		return FALSE;
	}

	//加載模塊
	PyObject *pyScanMethod = PyObject_GetAttrString(pModule, "startScan");
	if (NULL == pyScanMethod || !PyCallable_Check(pyScanMethod))
	{
		Py_DECREF(pModule);
		g_OutputString(_T("加載函數[%s]失敗!!!"), _T("startScan"));
		return FALSE;
	}

	//加載參數
	PyObject* pArgs = Py_BuildValue("ss", T2A(lpUrl), T2A(lpRequestMethod));

	PyObject *pRes = PyObject_CallObject(pyScanMethod, pArgs);
	Py_DECREF(pArgs);

	if (NULL == pRes)
	{
		g_OutputString(_T("調用函數[%s]失敗!!!!"), _T("startScan"));
		return FALSE;
	}

	//如果是元組,那么Python腳本返回的是兩個參數,證明發現漏洞
	if (PyTuple_Check(pRes))
	{
		PyObject* pHasVul = PyTuple_GetItem(pRes, 0);
		long bHasVul = PyLong_AsLong(pHasVul);
		Py_DECREF(pHasVul);

		if (bHasVul != 0)
		{
			PyObject* pyUrl = PyTuple_GetItem(pRes, 1);
			int nSize = 0;
			LPWSTR pszUrl = PyUnicode_AsWideCharString(pyUrl, &nSize);
			Py_DECREF(pyUrl);

			StringCchCopy(lpHasVulUrl, BuffSize, pszUrl);
			PyMem_Free(pszUrl);

			return TRUE;
		}
	}

	Py_DECREF(pRes);
	return FALSE;
}

對應的Python函數如下:

def startScan(url, method):
    if(method == "GET"):
	    response = requests.get(url)
    else:
        response = requests.post(url)

    if response.status_code == 200:
        return True, url
    else:
        return False

C++數據類型與Python對象的相互轉化

Python與C++結合的一個關鍵的內容就是C++與Python數據類型的相互轉化,針對這個問題Python提供了一系列的函數。

這些函數的格式為PyXXX_AsXXX 或者PyXXX_FromXXX,一般帶有As的是將Python對象轉化為C++數據類型的,而帶有From的是將C++對象轉化為Python,Py前面的XXX表示的是Python中的數據類型。比如 PyUnicode_AsWideCharString 是將Python中的字符串轉化為C++中寬字符,而 Pyunicode_FromWideChar 是將C++的字符串轉化為Python中的字符串。這里需要注意一個問題就是Python3廢除了在2中的普通的字符串,它將所有字符串都當做Unicode了,所以在調用3的時候需要將所有字符串轉化為Unicode的形式而不是像之前那樣轉化為String。具體的轉化類型請參考Python官方的說明。

上面介紹了基本數據類型的轉化,除了這些Python中也有一些容器類型的數據,比如元組,字典等等。下面主要說說元組的操作。元組算是比較重要的操作,因為在調用函數的時候需要元組傳參並且需要解析以便獲取元組中的值。

  1. 創建Python的元組對象

創建元組對象可以使用 PyTuple_New 來創建一個元組的對象,這個函數需要一個參數用來表示元組中對象的個數。

之后需要創建對應的Python對象,可以使用前面說的那些轉化函數來創建普通Python對象,然后調用 PyTuple_SetItem 來設置元組中數據的內容,函數需要三個參數,分別是元組對象的指針,元組中的索引和對應的數據

示例:

  PyObject* args = PyTuple_New(2);   // 2個參數
  PyObject* arg1 = PyInt_FromLong(4);    // 參數一設為4
  PyObject* arg2 = PyInt_FromLong(3);    // 參數二設為3
  PyTuple_SetItem(args, 0, arg1);
  PyTuple_SetItem(args, 1, arg2);

或者如果元組中都是簡單數據類型,可以直接使用 PyObject* args = Py_BuildValue(4, 3); 這種方式來創建元組

  1. 解析元組

Python 函數返回的是元組,在C++中需要進行對應的解析,我們可以使用 PyTuple_GetItem 來獲取元組中的數據成員,這個函數返回PyObject 的指針,之后再使用對應的轉化函數將Python對象轉化成C++數據類型即可

PyObject *pVulNameObj = PyTuple_GetItem(PyResult, 0);
PyObject *pVulLevelObj = PyTuple_GetItem(PyResult, 1);

//獲取漏洞的名稱信息
int nStrSize = 0;
LPTSTR pVulName = PyUnicode_AsWideCharString(pVulNameObj, &nStrSize);
StringCchCopy(lpVulName, MAX_PATH, pVulName);
PyMem_Free(pVulName); //釋放由PyUnicode_AsWideCharString分配出來的內存

//獲取漏洞的危險等級
level = PyLong_AsLong(pVulLevelObj);

//最后別忘了將Python對象解引用
Py_DECREF(pVulNameObj);
Py_DECREF(pVulLevelObj);
Py_DECREF(PyResult);

Python中針對具體數據類型操作的函數一般是以Py開頭,后面跟上具體的數據類型的名稱,比如操作元組的PyTuple系列函數和操作列表的PyList系列函數,后面如果想操作對應的數據類型只需要去官網搜索對應的名稱即可。

這些代碼實例都是我之前寫的一個Demo中的代碼,Demo放到了Github上: PyScanner



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM