如何使C、C++程序支持Python脚本

C/C++比较底层,可以开发高性能、高可靠的程序,但是不如Python简单易学。所以,如果我们使用C/C++开发的程序,却可以给用户一个Python语言的脚本接口,就比较舒服了。

其实实现这个功能,一点儿不难。Python的C语言API,可以完美地支持这个功能。

只要我们的程序中连接了Python的运行库,就可以使用Python的API,支持用户写的Python脚本执行。

以下我们就一步一步地在C/C++项目中,添加上Python3脚本的支持。

(注:因为Python2已经过时,所以以下所有代码均按照Python3来演示)。

CMake取得Python3开发环境

如果我们的项目是用CMake来构建的,可以有两种方法来在CMakeLists.txt中自动添加Python3的支持。

pkg-config

如:

FIND_PACKAGE (PkgConfig)
PKG_CHECK_MODULES (Python3 python3.10 REQUIRED)

MESSAGE(STATUS "Python3_INCLUDE_DIRS = ${Python3_INCLUDE_DIRS} ")
MESSAGE(STATUS "Python3_LIBRARIES = ${Python3_LIBRARIES} ")

INCLUDE_DIRECTORIES(${Python3_INCLUDE_DIRS})

...
TARGET_LINK_LIBRARIES(test_program ${Python3_LIBRARIES})

但是,这种方法有个弊端(可能较新的版本已经修复),只能取得Python开发环境的头文件目录,不能取得库文件。

FIND_PACKAGE()

还可以使用FIND_PACKAGE()函数,参数参数为Python3,组件需要Interpreter与Development。

其中,Interpreter是解释器,Development是开发环境。

如:

FIND_PACKAGE(Python3 COMPONENTS Interpreter Development)
IF (!${Python3_FOUND})
    MESSAGE(FATAL_ERROR "Python3 not found, please install it.")
ENDIF ()

MESSAGE(STATUS "Python3_INCLUDE_DIRS = ${Python3_INCLUDE_DIRS} ")
MESSAGE(STATUS "Python3_LIBRARIES = ${Python3_LIBRARIES} ")

INCLUDE_DIRECTORIES(${Python3_INCLUDE_DIRS})

...
TARGET_LINK_LIBRARIES(test_program ${Python3_LIBRARIES})

在CMakeLists.txt中添加了Python3开发的头文件以及库以后,就可以在我们的C/C++代码中使用Python3了。

初始化

在使用Python之前,先要初始化。

初始化Python运行环境的函数为Py_Initialize(),初始化之后就可以使用PyRun_SimpleString来执行Python语句。

如:

bool init(const std::string &scriptdir) {
    Py_Initialize();
    auto ret = Py_IsInitialized();
    if (ret == 0)
        return false;

    PyRun_SimpleString("import sys");
    auto pathappend = "sys.path.append('" + scriptdir + "')";
    PyRun_SimpleString(pathappend.c_str());

    return true;
}

以上代码,在初始化完成以后,执行了

import sys
sys.path.append(scriptdir)

其实就是把自定义的脚本目录,加入了sys.path列表,这样我们在后续的代码中就可以直接import我们用Python3写的脚本了。

清理Python运行环境:

 Py_Finalize();

导入Python模块

在C/C++中直接导入Python模块的函数为:PyImport_ImportModule()

如以下代码,导入了前面我们加入的scriptdir目录中的helloworld.py模块。

    // 去掉模块结尾的.py
    dname = "helloworld.py";
    auto pos = dname.find_last_of('.');
    auto package = dname.substr(0, pos);
    auto pyModule = PyImport_ImportModule(package.c_str());
    if (!pyModule) {
        PyErr_Print();
        throw std::exception();
    }

取得模块中的变量与函数

导入模块以后,模块中的所有变量与函数,都可以通过字典取得。

具体函数为PyModule_GetDict(),参数为上面返回的pyModule。

具体代码如下:

    auto dict = PyModule_GetDict(pyModule);

    auto py_protocol = PyDict_GetItemString(dict, "protocol");
    if (PyUnicode_CompareWithASCIIString(py_protocol, "UDP") == 0) {
        protocol = UDP;

    pyInit = PyDict_GetItemString(dict, "init");
    if (!PyCallable_Check(pyInit)) {
        PyErr_Print();
        throw std::exception();
    }

变量在使用时需要进行一些转换,如:

  • Long
port = (int) PyLong_AsLong(py_port);
  • const char *
auto str_host = PyUnicode_AsASCIIString(py_host);
auto host = PyBytes_AsString(str_host);

获取异常

有两种获取异常的方法,一种是直接打印错误,使用

PyAPI_FUNC(void) PyErr_Print(void);

还有一种,可以通过PyErr_Fetch函数,获得详细错误,以及栈信息。

其中,traceback为一个链表,next指针将获得下一层调用栈。

如:

        PyObject *type, *value, *traceback;
        Py_ssize_t size;

        PyErr_Fetch(&type, &value, &traceback);

        std::string filename;
        int lineno = 0;
        if (traceback) {
            auto *tb = (PyTracebackObject *) traceback;
            auto code = PyFrame_GetCode(tb->tb_frame);
            auto co_filename = PyUnicode_AsUTF8AndSize(code->co_filename, &size);
            filename = co_filename;
            lineno = tb->tb_lineno;
        }

        std::string what;
        if (type) {
            what += std::string(PyExceptionClass_Name(type)) + ": ";
        }

        if (value) {
            PyObject *line = PyObject_Str(value);

            const char *str = PyUnicode_AsUTF8AndSize(line, &size);
            if (line && (PyUnicode_Check(line)))
                what += std::string(str, size);
        }

        logF(facility, level, filename.c_str(), lineno, "%s", what.c_str());

使用以上函数,可以非常方便快捷地定位用户写的Python脚本哪里有问题。

Python调用C/C++中的API

使用C/C++开发应用程序,给用户提供Python接口,还可以提供一些API给用户,使得用户可以在Python文件里,进行调用。

而实现这个功能,其实并不复杂。

只需要在C/C++实现中,使用特定的参数(PyObject)定义C/C++函数,之后导入Python脚本取得PyModule对象之后,使用 PyModule_AddFunctions把函数加入进去。

定义Python可以使用的C/C++函数

Python执行函数,传递到C/C++里之后,都是PyObject,所以要在这种函数里,做参数转换,具体方法为使用PyArg_ParseTuple。

如:

PyObject *
py_listen(PyObject * self, PyObject * args) {
    char *host, *port;

    if (!PyArg_ParseTuple(args, "ss", &host, &port)) {
        return nullptr;
    }

    std::cout << __func__ << "listen " << host << ":" << port << std::endl;

    return (PyObject *) Py_BuildValue("s", "OK");
}

,使用PyArg_ParseTuple函数,把Python的args转换成了C/C++中的变量。

如果希望Python中可以使用keyword方式调用函数,还可以使用PyArg_ParseTupleAndKeyWords函数解析参数。

具体用法为:

PyObject *
py_listen(PyObject * self, PyObject * args, PyObject *kwargs) {
    char *host, *port;
    char *kwlist[] = {"host", "port", NULL};
    PyArg_ParseTupleAndKeywords(args, kwargs, "ss", kwlist, &host, &port);

使用 PyModule_AddFunctions把第一步的函数加入进去

自从Python3.5以后,有了PyModule_AddFunctions,可以方便地给导入进来的Module添加函数。

只要把函数加入PyMethodDef结构的数组即可。

PyMethodDef api_methods[] = {
                {"listen_tcp", _PyCFunction_Cast(py_listen), METH_VARARGS | METH_KEYWORDS, "listen tcp endpoint"},
                {nullptr, nullptr, 0, nullptr}
        };
PyModule_AddFunctions(pyModule, api_methods);

此时,在我们使用Python3实现的脚本里面,就可以使用listen_tcp函数。程序加载了脚本之后,遇到listen_tcp的调用,会自动执行py_listen。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值