Boost.Python是Boost库家族中一个非常实用的组件,旨在帮助C++开发者更便捷地为Python编写扩展模块。坦白说,以往使用纯Python C API编写扩展相当繁琐,涉及大量引用计数和类型转换,稍有不慎就会导致内存泄漏。Boost.Python将这些复杂细节封装起来,显著提升了开发效率。目前,它对将Python嵌入C++的支持尚不全面,但仅其提供的工具集就已经能节省大量时间。此外,华宇煜编写的《Boost.Python简明教程》内容扎实,可作为补充阅读材料。
1 Boost.Python 安装简介
在正式开始编写代码之前,需要先编译Boost库。首先从Boost官网下载源码包,并将其解压到合适的目录。需要注意的是,在安装Boost.Python之前,请确保Python已正确安装,这是必要的前提条件。
1.1 Linux 下的编译
首先进入Boost源码目录,运行 ./configure 脚本。配置时需要传递Python相关参数,例如Python解释器路径、版本号和根目录:
./configure --with-python=/usr/bin/python --with-python-version=2.4 --with-python-root=/usr
随后,如同大多数Linux程序一样,执行 make 开始编译。编译完成后,切换到root权限并运行 make install,Boost的头文件和库文件将自动复制到系统指定目录,之后即可直接使用。
1.2 使用 MinGW + MSys 在 Windows 下的编译
在Windows环境下,编译步骤稍多一些。首先需要编译Boost自带的构建工具bjam。进入Boost源码目录下的 tools/build/jam_src,执行 build.bat mingw,稍等片刻即可获得bjam.exe。将其放置于 %PATH% 可访问的目录中,例如 C:\Windows\System32,或自行创建一个工具目录并添加到环境变量。
然后返回Boost源码根目录,运行bjam进行编译。需要指定Python相关变量:PYTHON_ROOT 指向Python安装目录(例如 E:\Python),PYTHON_VERSION 填入版本号(例如 2.4)。具体命令如下:
bjam.exe "-sTOOLS=mingw" "-sPYTHON_ROOT=E:\Python" "-sPYTHON_VERSION=2.4"
编译完成后,头文件和库文件默认存放在 C:\Boost 目录下,您可以根据需要将其移动到其他位置备用。
2 使用 Boost.Python 将 Python 模块嵌入 C++
Boost.Python目前尚未提供完整的Python嵌入C++的封装库,因此许多底层操作仍需借助Python C API来完成。然而,借助Boost.Python提供的几个工具模块,至少可以在很大程度上简化繁琐的引用计数和类型转换工作。
2.1 修改模块加载路径,装入 Python 模块
与所有嵌入Python的C/C++程序相同,第一步是在第一个 #include 之前引入 Python.h,然后在程序开始时调用 Py_Initialize(),在程序结束时调用 Py_Finalize()。
接下来需要准备加载Python模块。为了使Python解释器能够找到模块文件,需要将模块所在路径添加到搜索路径中。对应的Python语句大致如下:
import sys
if not '/module/path' in sys.path:
sys.path.append('/module/path')
使用Python C API执行类似的代码即可。路径添加完成后,通过 PyImport_ImportModule 函数加载模块,该函数返回一个 PyObject * 指针。为避免手动处理引用计数的麻烦,可以使用Boost.Python提供的 handle 来包装该指针:
#include
...
boost::python::handle<>* _module; // Module handle.
std::string path; // Path of the Python module.
std::string module; // Module name.
...
try
{
PyRun_SimpleString("import sys");
PyRun_SimpleString((std::string("if not '") + path
+ "' in sys.path: sys.path.append('" + path + "')").c_str());
_module = new boost::python::handle<>(
PyImport_ImportModule((char *) module));
...
}
catch (...)
{
PyErr_Print();
PyErr_Clear();
delete _module;
_module = NULL;
return false;
}
...
需要特别注意的是:通过Python C API初始化后的解释器不会自动将当前目录加入搜索路径。因此,即使Python模块位于当前目录,也必须使用上述代码手动添加当前路径,否则 PyImport_ImportModule 将无法找到模块。
当Python模块使用完毕或程序退出时,务必使用 delete 释放 _module 指针。handle 在释放时会自动处理底层的Python模块引用计数和资源回收。
2.2 调用 Python 函数
导入模块后,调用Python函数变得非常便捷。Boost.Python提供了一个实用的模板函数 boost::python::call_method,它封装了调用过程中的所有细节:无需手动将C++参数打包成 PyObject *、构造元组、传递参数以及解包返回值。只需如下编写:
boost::python::call_method<返回值类型>(模块指针, "Python 函数名",
参数 1, 参数 2, ...);
模块指针可以通过之前提到的 _module 对象的 get() 方法获取,例如:
...
bool result;
std::string config_file;
...
try
{
return boost::python::call_method(_module->get(), "initialize",
config_file);
}
catch (...)
{
PyErr_Print();
PyErr_Clear();
...
}
...
2.3 使用 Python 类对象
通过Python C API调用Python类对象与调用普通函数的思路基本相同。只需调用类的构造方法获得一个对象,然后将该对象的指针视为模块指针,用相同的方式调用其成员方法。以下代码演示了如何从 _module 创建一个 YukiSession 对象、构造一个Python列表、调用其成员方法,以及从列表中提取元素:
...
boost::python::handle<> _yukisession;
...
// Retrieve the module handle and namespace handle.
boost::python::object main_module(*_module);
boost::python::object main_namespace = main_module.attr("__dict__");
// Call the method and get the object handle.
_yukisession = boost::python::handle<>((PyRun_String(
"YukiSession()", Py_eval_input,
main_namespace.ptr(), main_namespace.ptr())));
...
// Compose a list.
boost::python::list param;
param.append(boost::python::str(_addr.get_host_addr()));
param.append(boost::python::str());
// Call the method and retrieve the result.
// Method is equivalent to:
// "bool __thiscall YukiSession::on_welcome(list param);"
result = boost::python::call_method
(_yukisession.get(), "on_welcome", param);
// Extract an item from a list.
str = boost::python::call_method
(param.ptr(), "__getitem__", 1);
...
3 在嵌入的 Python 模块中调用 C++ 程序
通过动态链接库方式使用Boost.Python将C++模块导出到Python,与直接在C++可执行程序中导出模块供嵌入的Python解释器使用,编写方式几乎完全相同。此处仅简要介绍导出普通函数的方法。如需了解导出C++类、导出可被Python重载的类等高级功能,建议参考华宇煜的《Boost.Python简明教程》或官方文档。
3.1 导出 C++ 函数
使用 BOOST_PYTHON_MODULE 宏定义要导出的Python模块,然后在宏内部使用 boost::python::def 声明导出的函数、参数名和文档字符串。例如,下面的例子导出了C++函数 yukigettext,并在Python中将其重命名为 gettext:
const char *yukigettext(const char *id);
BOOST_PYTHON_MODULE(yuki)
{
boost::python::def("gettext", yukigettext,
boost::python::args("id"), "Translate message.");
}
3.2 为 Python 初始化 C++ 模块
使用 BOOST_PYTHON_MODULE(name) 宏后,会自动生成一个名为 initname 的函数。我们需要在 Py_Initialize() 之后调用该函数来初始化导出的模块。例如,刚才导出的模块名为 yuki,则初始化时调用 inityuki():
...
Py_Initialize();
inityuki();
...
3.3 在 Python 模块中调用 C++ 模块
现在,在Python代码中就可以像导入普通模块一样导入该C++模块,并直接调用其中的函数:
import yuki
...
print yuki.gettext("This is a test!")
