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。