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 
#include 

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;
}

参考

  • C/C++ 调用Python
  • Python官方文档:c-api
  • https://docs.python.org/2/extending/embedding.html

你可能感兴趣的:(C++陷阱,C++葵花宝典,c++,python,c++调用python,c调用python3,c++调用python3)