C++調用Python3實戰,和PyImport_ImportModule返回NULL問題解決

Linux C++調用Python3

入門

準備

以下面的目錄結構演示如何在Linux C/C++調用python3。

|--hello.py
|--main.cpp
|--CMakeLists.txt

 

  • hello.py:python的腳本,裏面有2個函數
  • main.cpp:c++函數
  • CMakeLists.txt:Cmake文件,生成makefile

python腳本

示例python腳本hello.py內容如下,有2個函數:

def add(a,b):
    return a+b

def get_name(first):
    return "your name is {} alice".format(first)

 

c++調用python3

#include <iostream>
#include <Python.h>

using namespace std;

const int kError = -1;
const int kSuccess = 0;

int pythonInit() {
    //初始化python
    Py_Initialize();
    int ret = Py_IsInitialized();
    if (ret == 0) {
        cout << "ocr_card_init Py_Initialize error" << endl;
        return kError;
    }
    return kSuccess;
}

void pythonCleanup() {
    Py_Finalize();
}

PyObject *pythonImportModule(const char *pyDir, const char *name) {
    //引入當前路徑,否則下面模塊不能正常導入
    char tempPath[256] = {};
    sprintf(tempPath, "sys.path.append('%s')", pyDir);
    PyRun_SimpleString("import sys");
    //PyRun_SimpleString("sys.path.append('./')");
    PyRun_SimpleString(tempPath);
    PyRun_SimpleString("print('curr sys.path', sys.path)");

    //引入模塊, hello.py
    PyObject *module = PyImport_ImportModule(name);
    if (module == nullptr) {
        PyErr_Print(); // print stack
        cout << "PyImport_ImportModule 'hello.py' not found" << endl;
        return nullptr;
    }

    return module;
}

int callPythonAdd(PyObject *module, int a, int b) {
    //獲取模塊字典屬性
    PyObject *pDict = PyModule_GetDict(module);
    if (pDict == nullptr) {
        PyErr_Print();
        std::cout << "PyModule_GetDict error" << std::endl;
        return kError;
    }

    //直接獲取模塊中的函數
    PyObject *addFunc = PyDict_GetItemString(pDict, "add");
    if (addFunc == nullptr) {
        std::cout << "PyDict_GetItemString 'add' not found" << std::endl;
        return kError;
    }

    // 構造python 函數入參, 接收2
    // see: https://docs.python.org/zh-cn/3.7/c-api/arg.html?highlight=pyarg_parse#c.PyArg_Parse
    PyObject *pArg = Py_BuildValue("(i,i)", a, b);

    //調用函數,並得到python類型的返回值
    PyObject *result = PyEval_CallObject(addFunc, pArg);

    int ret = 0;
    //將python類型的返回值轉換爲c/c++類型
    PyArg_Parse(result, "i", &ret);
    return ret;
}

int callPythonGetName(PyObject *module, std::string firstName, std::string &outName) {
    //獲取模塊字典屬性
    PyObject *pDict = PyModule_GetDict(module);
    if (pDict == nullptr) {
        PyErr_Print();
        std::cout << "PyModule_GetDict error" << std::endl;
        return kError;
    }

    //直接獲取模塊中的函數
    PyObject *addFunc = PyDict_GetItemString(pDict, "get_name");
    if (addFunc == nullptr) {
        std::cout << "PyDict_GetItemString 'add' not found" << std::endl;
        return kError;
    }

    // 構造python 函數入參, 接收2
    // see: https://docs.python.org/zh-cn/3.7/c-api/arg.html?highlight=pyarg_parse#c.PyArg_Parse
    PyObject *pArg = Py_BuildValue("(s)", firstName.c_str());

    //調用函數,並得到python類型的返回值
    PyObject *result = PyEval_CallObject(addFunc, pArg);

    char *name = nullptr;
    //將python類型的返回值轉換爲c/c++類型
    PyArg_Parse(result, "s", &name);

    char tempStr[256] = {};
    int strLen = strlen(name);
    if (strLen > 256) {
        return kError;
    }
    strcpy(tempStr, name);
    outName = tempStr;

    return kSuccess;
}

通過C/C++ 封裝好之後,即可使用了:

int main() {
    pythonInit();

    //int argc = 1;
    //wchar_t *argv[1] = {L"ocrTest"};
    //PySys_SetArgv(argc, argv);

    //直接運行python代碼
    PyRun_SimpleString("print('----------hello Python form C/C++')");

    PyObject *helloModule = pythonImportModule("../", "hello");
    if (helloModule == nullptr) {
        return -1;
    }

    // call python add function
    int result = callPythonAdd(helloModule, 1, 3);
    cout << "1 + 3 = " << result << endl;

    // call python get_name function
    std::string fullName;
    callPythonGetName(helloModule, "summer", fullName);
    cout << fullName << endl;

    pythonCleanup();
}

執行後輸出:

----------hello Python form C/C++
curr sys.path ['/usr/lib64/python36.zip', '/usr/lib64/python3.6', '/usr/lib64/python3.6/lib-dynload', '/home/tengqing/.local/lib/python3.6/site-packages', '/usr/local/lib64/python3.6/site-packages', '/usr/local/lib/python3.6/site-packages', '/usr/lib64/python3.6/site-packages', '/usr/lib/python3.6/site-packages', '../']
1 + 3 = 4
your name is summer alice

Process finished with exit code 0

 

依賴安裝

環境是CentOS 7,gcc是4.8.5默認的,這裏要注意是使用python2還是3。

$ sudo yum install libstdc++ #  version `CXXABI_1.3.8' not found 問題
$ sudo yum install binutils  # 升級ld版本
$ sudo yum install python3         # python3 --version
$ sudo yum install python3-devel   # c/c++調用python需要python.so

 

C/C++調用Python需要藉助 Python.hPython.so,即上面 python3-devel 執行後能在 /usr/include/usr/lib64下找到。

CMakeLists

所以,在CMakeLists.txt中,我們需要配置python3-devel包:

cmake_minimum_required(VERSION 3.19)
project(test)

set(CMAKE_CXX_STANDARD 14)

# 這裏需要配置python3的頭文件和python.so的庫位置,在centos7中,通過如下方式查看
#[tengqing@localhost card]$ ls /usr/include/python
#python2.7/  python3.6m/
set(PYTHON3_VERSION 3.6m)
if (UNIX)
    set(PYTHON3_INCLUDE /usr/include/python${PYTHON3_VERSION}/)
    set(PYTHON3_LIBS /usr/bin/python3.6m-x86_64-config)
endif()

# 指定生成可執行程序
add_executable(test main.cpp)

# 頭文件搜索目錄
target_include_directories(${PROJECT_NAME}
        PUBLIC
        ${PYTHON3_INCLUDE} # 這樣外部就能找到頭文件所在目錄了
        )
# 動態庫搜索目錄
target_link_directories(${PROJECT_NAME}
        PUBLIC
        ${PYTHON3_LIBS})
# 鏈接動態庫
target_link_libraries(${PROJECT_NAME}
        PRIVATE
        python${PYTHON3_VERSION})

 

多線程環境下調用Python3

GIL簡介

來自:https://zhuanlan.zhihu.com/p/146874652

在使用python解釋器時,要注意GIL(全局解釋鎖)的工作原理以及對性能的影響。GIL保證在任意時刻只有一個線程在解釋器中運行。在多線程環境中,python解釋器工作原理如下:

  1. 設置GIL
  2. 切換到一個線程去運行
  3. 運行:
    a. 指定數量的字節碼指令,或者
    b. 線程主動讓出控制(可以調用time.sleep(0))
  4. 把線程設置爲睡眠狀態
  5. 解鎖GIL
  6. 再次重複以上所有步驟

對性能的影響: 假如有一段兩個線程的python代碼,運行在一個兩核CPU上。由於GIL的存在,兩個線程無法真正並行執行,CPU佔用率總是低於50%。

GIL是一個歷史遺留問題,導致CPython多線程不能利用多個CPU內核的計算能力。爲了利用多核,通常使用多進程的方法,或是通過Python調用C代碼,由C來實現多線程。

注意,當在C/C++創建的線程中調用Python時,GIL需要通過函數PyGILState_Ensure()和PyGILState_Release()手動獲取、釋放。 C++

PyGILState_STATE gstate;
gstate = PyGILState_Ensure();

/* Perform Python actions here. */
result = CallSomeFunction();
/* evaluate result or handle exception */

/* Release the thread. No Python API allowed beyond this point. */
PyGILState_Release(gstate);

C++封裝GIL和正確的使用方式

封裝GIL

有點類似mutex互斥鎖,爲了方便使用,我們封裝成自動釋放的鎖,如下:

// https://docs.python.org/zh-cn/3/c-api/init.html?highlight=pygilstate_ensure
class PyThreadStateLock {
public:
    PyThreadStateLock() {
        cout << "----------------PyGILState_Ensure start" << endl;
        state = PyGILState_Ensure();
        cout << "----------------PyGILState_Ensure end" << endl;
    }

    ~PyThreadStateLock() {
        cout << "----------------PyGILState_Release start" << endl;
        PyGILState_Release(state);
        cout << "----------------PyGILState_Release end" << endl;
    }

private:
    PyGILState_STATE state;
};
初始化python3修改

在主線程中初始化python後,需要調用 PyEval_SaveThread() 釋放全局解釋器。

static PyThreadState *g_mainThreadState = nullptr;

int ocr_card_init(){
	//初始化python
	Py_Initialize();
	
	// 初始化線程支持
	PyEval_InitThreads();
	
	// release the GIL, store thread state, set the current thread state to NULL
	g_mainThreadState = PyEval_SaveThread();
}
每次調用python申請GIL

每次調用python函數之前,獲取一下全局解釋器即可。

int ocr_card_detect(const char *image_path, char *out_buffer, int *buffer_len) {
    PyThreadStateLock autoLock;
    
    // call python function
    // ...
   
    return kSuccess;
}
退出前釋放

程序結束前釋放,別忘記了恢復線程狀態,獲取全局解釋器。

void ocr_card_cleanup() {
    if (g_mainThreadState != nullptr) {
        //re-acquire the GIL (re-initialize the current thread state)
        PyEval_RestoreThread(g_mainThreadState);
        Py_Finalize();
    }
}

遇到問題

version `CXXABI_1.3.8’ not found 問題

升級一下libstdc++庫的版本即可。

sudo yum install libstdc++ #  version `CXXABI_1.3.8' not found 問題

PyImport_ImportModule返回NULL

原因一:找不到*.py腳本,或者沒有引入腳本所在路徑

找不到python腳本,或者目錄不正確,引入一下即可:

PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./')");
原因二:沒有安裝依賴

執行之前,需要安裝一下相關依賴,比如:

$ pip3 install -r requirements.txt
原因三:python腳本有異常

沒有正確安裝,或者依賴出錯,我們可以通過堆棧查看具體的錯誤,然後解決。

//引入模塊, hello.py
PyObject *module = PyImport_ImportModule(name);
if (module == nullptr) {
    // print stack, 看看具體是什麼錯誤
    PyErr_Print(); 
    cout << "PyImport_ImportModule 'hello.py' not found" << endl;
    return nullptr;
}

參考

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章