Python、C++ 混合编程环境搭建及简单示例
Python 以其极高的开发效率著称,而 C++ 作为一种编译型语言,在运行效率上鹤立鸡群。开发效率,我所欲也,运行效率,亦我所欲也,二者可得兼乎?可!
近期在复现 DeepMind 的 Alpla Zero 算法时,我面临如下一种需求:既需要快速地把算法实现出来,又要努力保证其运行效率(算法的核心部分:蒙特卡洛树搜索用 python 实现需要消耗巨量时间),因此开始零基础尝试 python、C++ 混合编程,试图用 C++ 实现蒙特卡洛树搜索部分,并在 python 中调用(在此之前本人几乎无任何 C++ 编程经验)
简单了解了下,这里推荐一下自我感觉 API 用户友好程度比较高的一个 C++ 库 ——Boost。但 u1s1,这个库的环境搭建过程中坑实在是多,网上资料也并不是特别多,因此在此记录一下。
由于一般代码都是搬去 Linux 服务器上运行,因此本文将介绍的是 Linux 系统下的环境搭建。
版本说明
- 系统版本:Ubuntu20.04,64 位(低版本应该也没什么关系,不过必须是 64 位,否则需要将后面编译的版本都修改为 32 位)
- gcc,g++ 版本 9.3.0(稍低一些也可,比如 7 + 应该也够用)
- Boost 1.75.0(当前最新)
- Python 3.8.5
- numpy 1.20.1
其中 Python 一般要 3 以上版本,(2 版本的情况,会更好装,不再赘述),numpy 的版本需要做好对应,比如 1.20.1 的版本下编译出来的库,到 1.19 版本的环境里可能会跑不了。
安装基本的软件
首先去 Boost 官网上下载最新版本的 Boost 库,选择 Unix 版本的 boost_1_75_0.tar.gz 下载到 Linux 系统下,解压。
确保环境下已经安装好了 Python3,同时该 python 已安装 numpy,找到该 python 安装目录下的如下两个路径并记住:包含目录,例如 /usr/local/python3/include/python3.8;静态库所在目录,例如 /usr/local/python3/lib/python3.8/config-3.8-x86_64-linux-gnu。(注:Linux 下的静态库即为.a 后缀的文件,例如本例中是 libpython3.8.a)
接下来 cd 进入前面解压出来的 boost_1_75_0 目录,准备进行 boost 静态库文件的编译。
上面两段话多次出现了 “静态库” 这个词,这里插一句为什么要编译 “静态库”,而非 “动态库”,由于我们的开发环境和运行环境通常不相同,且运行环境随时可能发生变化,动态库对系统的环境依赖程度高,也即,换个环境,动态库引用的动态链接一般就会失效。这显然令人很不爽,如果每次换个运行环境都要搭一遍环境,可太难顶了,而静态库则没有这个问题。
在 boost_1_75_0 目录下,运行以下命令:(下面的命令欲知更多参数细节或有个性化的需求请移步官方文档)
./bootstrap.sh --with-libraries=python --with-python-version=3.8 --with-python-root=/usr/local/python3 include=/usr/local/python3/include/python3.8 --with-python=/usr/local/python3/bin/python3.8 --with-toolset=gcc --prefix=/usr/local
其中参数指定安装库为 boost-python
(boost 库有很多模块,这里只安装 python 模块,若不指定,则默认全装,暂时似乎没有必要),指定编译工具为 gcc,且安装路径在 /usr/local
下。中间几个参数分别对应了 python 的版本、根目录、包含目录及解释器所在目录。
上面命令跑完后,当前目录下会出现名为 b2 的可执行文件,接下来执行以下命令:
./b2 cflags='-fPIC' cxxflags='-fPIC' address-model=64
其中比较重要是的 cflags 及 cxxflags,均需要设置为’fPIC’,可以用于生成位置无关的代码,使代码在加载到内存时使用相对地址。若不加这两条参数,后面在编译生成目标动态库时会出问题。address-model 参数默认为 64,可指定生成 64 位或 32 位的库文件。
最后执行
./b2 install
接下来坐等程序运行完即可。运行完后,检查目录 /usr/local/lib 下是否有以下这些文件:
- libboost_numpy38.so
- libboost_python38.a
- libboost_python38.so.1.75.0
- libboost_numpy38.a
- libboost_numpy38.so.1.75.0
- libboost_python38.so
之所以有.so 动态库文件生成是因为前面并没有指定动态或静态,默认参数下动静态库都会编译,可按需使用。
再检查目录 /usr/local/include
下是否存在 boost 文件夹,若存在,一般即安装成功。
最后,需要设置几个永久环境变量,如下:
BOOST_LIB=/usr/local/lib
CPLUS_INCLUDE_PATH=/usr/local/python3/include/python3.8:/usr/local/include/boost
如果你成功进行到了这一步,那么恭喜你,boost 环境应该已经安装成功了,接下来只要写一个简单的 hello world 程序编译一下即可!
一个简单的 demo
编写.cpp 文件并编译为.so 文件
hello.cpp
#include <iostream>
#include <boost/python.hpp>
using namespace std;
using namespace boost::python;
void greet(const char *name) {
cout << "Hello, " << name << endl;
}
BOOST_PYTHON_MODULE_INIT(hello) {
def("greet", greet);
}
在当前路径下运行下面的编译命令:
g++ -shared -fPIC -lm -pthread -O3 -std=c++11 -march=native -Wall -funroll-loops -Wno-unused-result hello.cpp -o hello.so /usr/local/lib/libboost_python38.a /usr/local/lib/libboost_numpy38.a -I/usr/local/python3/lib/python3.8/config-3.8-x86_64-linux-gnu
等待数秒即可在同目录下生成 hello.so 文件,不出意外,该文件可以被 python3 导入使用。
针对上面又臭又长的编译命令,我们其实可以做一个 makefile,如下:
PYTHON_VERSION = 3.8
INPUT = hello.cpp
TARGET= hello
CFLAGS = -shared -fPIC -lm -pthread -O3 -std=c++11 -march=native -Wall -funroll-loops -Wno-unused-result
PYTHON_LIB_PATH = /usr/local/python3/lib/python$(PYTHON_VERSION)/config-3.8-x86_64-linux-gnu
BOOST_PYTHON_STATIC_LIB = $(BOOST_LIB)/libboost_python38.a
BOOST_NUMPY_STATIC_LIB = $(BOOST_LIB)/libboost_numpy38.a
$(TARGET).so: $(INPUT)
g++ $(CFLAGS) $(INPUT) -o $(TARGET).so $(BOOST_PYTHON_STATIC_LIB) $(BOOST_NUMPY_STATIC_LIB) -I$(PYTHON_LIB_PATH)
之后通过 make 命令即可进行编译。文件中的各种路径请自行比对修改。如遇到
makefile:13: *** missing separator (did you mean TAB instead of 8 spaces?).
这样的报错,则只需将最后 g++ 前面的 8 个空格换成 Tab。
在 python 中导入使用
将 hello.so 移到其他无 boost python 环境的 Linux 系统下,安装好与之前相同版本的 python 和 numpy,尝试导入与使用,若未得到以下结果,则意味着前面所有的努力都白费了需要再仔细看一下各项路径的配置及各种环境的版本!
另外 Boost Python 库同样支持在 C++ 程序中调用 python 脚本,鉴于暂时无这方面的需求,我并没有尝试。