saenai no heroine

尝试在C++项目中嵌入Python脚本

字数统计: 1,608阅读时长: 7 min
2020/08/22 Share

尝试在C++项目中嵌入Python脚本

Hello World

首先写一个在C++代码中调用Python脚本输出 Hello World 的代码吧。

1
2
3
4
5
6
7
8
9
10
#include "python3.6m/Python.h"

int
main()
{
Py_Initialize();
PyRun_SimpleString("print('hello world from python')");
Py_Finalize();
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cmake_minimum_required(VERSION 3.17)
project(CppCallPython)

set(CMAKE_CXX_STANDARD 11)

include_directories(/Users/USERNAME/.pyenv/versions/anaconda3-2020.02/envs/OpenAI-Gym/include)
link_directories(/Users/USERNAME/.pyenv/versions/anaconda3-2020.02/envs/OpenAI-Gym/lib)

#include_directories(/usr/local/opt/python@3.8/Frameworks/Python.framework/Versions/3.8/include)
#link_directories(/usr/local/opt/python@3.8/Frameworks/Python.framework/Versions/3.8/lib)

add_executable(CppCallPython main.cpp)
target_link_libraries(CppCallPython python3.6m)
#target_link_libraries(CppCallPython python3.8)

遇到的问题

在编写这一个 Hello World 程序时,网上的教程大多数都是使用的 Python 2.7 版本或者是直接从官网下载安装的或者是通过各种包管理器安装的 Python 3.x,对于这些情况,我们很容易就能找到对应的 Python dynamic link library。对于 mac os 来说,通过 brew install python 安装的 Python 的话,Python 的 dylib 就存在于该路径下 `/usr/local/opt/python@3.8/Frameworks/Python.framework/Versions/3.8/lib` (注意替换上述路径中的版本号为使用的版本号,本文中使用的是使用 brew 安装的 3.8.3 版本的 Python)。
当前,我的 MBP 上一共存在有三种不同的 Python:

1. Mac os 系统自带的 Python 2.7,用于各种系统用途
2. brew package manager 安装的 Python 3.8,用作一些使用 brew 安装的包的依赖
3. 使用 pyenv 安装的 Anaconda-2020.02,以及一系列的 conda environments。

从现在起,假设当前使用的Python是来自于 Anaconda的某一个 environment - OpenAI-Gym
一开始写好了C++部分之后,就开始编写相应的CMake文件了。这时候就犯了难,Python 的 include path 很好找,直接去这个 environment 下面找到 include 文件夹就好了,这个文件夹的路径就是我们需要的 include path。而 Python 的 dylib 则让我犯了难。理论上来说 library 应该是放在类似名为 lib 的文件夹下的。然后就去找了路径 environment/lib 下面寻找有没有 Python 的 dylib。一开始的时候不知道是不是看错了还是怎么的,以为没有(后面发现其实是有的)。之后就发现,诶,在 lib 文件夹里还存在一个叫做 python3.6 的文件夹。进去之后用 ls | grep python 搜索了一下发现没有任何结果。这就让我纳闷了。这时突然想起之前看的 Python 官方的关于将Python脚本嵌入到C++程序的文档,在那个文档的最后[1]有提到过C++编译需要设置的一些参数。其中就提到了可以使用一个 Python 自带的shell脚本 pythonX.Y-config --ldflags 来获取链接的参数。我执行之后得到的结果是这样的:-L/Users/USERNAME/.pyenv/versions/anaconda3-2020.02/lib/python3.7/config-3.7m-darwin -lpython3.7m -ldl -framework CoreFoundation。虽然也不是很懂这些参数的意思,不过我猜测应该是在这个路径( /Users/USERNAME/.pyenv/versions/anaconda3-2020.02/lib/python3.7/config-3.7m-darwin )下面就能找到 Python 的 dylib了。可惜事情并没有这么简单。这个路径下面与 library 有关的就只有一个:libpython3.7m.a 。通过 file 命令可以知道,这个文件是一个静态库。

1
2
$ file libpython3.7m.a
libpython3.7m.a: current ar archive random library

然后我就傻乎乎地直接把这个静态库当作动态库(dylib)写进了 CMake文件里。然后 build 就输出了超长一串各种看不懂的错误。
。。。
中间也找了各种办法,但是都不管用。中途想过会不会是因为我当时使用的是 Anaconda 发行版而不是普通从官网下载或者从包管理器安装的 Python。于是就去测试了一下用 brew 安装的 Python 3.8.3。

1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.17)
project(CppCallPython)

set(CMAKE_CXX_STANDARD 11)

include_directories(/usr/local/opt/python@3.8/Frameworks/Python.framework/Versions/3.8/include)
link_directories(/usr/local/opt/python@3.8/Frameworks/Python.framework/Versions/3.8/lib)

add_executable(CppCallPython main.cpp)
target_link_libraries(CppCallPython python3.8)

于是,直接成功啦。
当然到这里故事并没有结束,归根结底我还是想用 Anaconda 的。所以还是要解决一开始的问题。从这里开始我知道了一开始看到的那个 libpython3.7m.a 文件其实是一个静态库。那么我就想会不会是 Anaconda 版本的 Python 没有带 Python 的动态库呢。网上找了一圈没什么收获。只能自己折腾啦。
这时我又回到了 libpython3.7m.a 这个静态库所在的文件夹,执行了 ls -lh 命令,有了惊奇的发现:

1
2
3
4
5
6
$ pwd
/Users/USERNAME/.pyenv/versions/anaconda3-2020.02/envs/OpenAI-Gym/lib/python3.6/config-3.6m-darwin
$ ls -lh
...
lrwxr-xr-x 1 USERNAME staff 21B Jul 23 16:30 libpython3.6m.a -> ../../libpython3.6m.a
...

咦,这个静态库原来是个软链接。于是就去到了它链接的文件所在的文件夹。然后用 ls | grep python 搜索了一下 (其实应该用 find . -name '*python*' )。意外收获

1
2
3
4
5
6
7
$ pwd
/Users/USERNAME/.pyenv/versions/anaconda3-2020.02/envs/OpenAI-Gym/lib
$ ls | grep python
libpython3.6m.a
libpython3.6m.dylib
libpython3.6m.nolto.a
python3.6

libpython3.6m.dylib 不就是我一直苦苦寻找的动态链接库吗!
在CMake设置好了链接的目录以及链接的动态库的名字之后,尝试着build,结果又又又出问题了。输出如下:

1
2
3
4
5
6
7
8
9
Could not find platform independent libraries <prefix>
Could not find platform dependent libraries <exec_prefix>
Consider setting $PYTHONHOME to <prefix>[:<exec_prefix>]
Fatal Python error: Py_Initialize: unable to load the file system codec
ModuleNotFoundError: No module named 'encodings'

Current thread 0x000000011454bdc0 (most recent call first):

Process finished with exit code 6

拿着 Fatal Python error: Py_Initialize: unable to load the file system codec 这段话上网查了一下,问题大概出在系统里有多个 Python 版本,结果没找到正确的那个。
解决方案很简单,设置一下环境变量 PYTHONHOME ,将其指向正确的Python目录即可。我这里就把它设置为了 Anaconda 的 OpenAI-Gym environment 的目录( /Users/USERNAME/.pyenv/versions/anaconda3-2020.02/envs/OpenAI-Gym )。然后,终于输出了正确的结果啦!

碎碎念,这一下就折腾了三个多小时,最后半个小时是在敲这个事件记录。

References:
1. Embedding Python in Another Application — Python 3.8.5 documentation
c++ - Py_Initialize fails - unable to load the file system codec - Stack Overflow

CATALOG
  1. 1. 尝试在C++项目中嵌入Python脚本
    1. 1.1. Hello World
      1. 1.1.1. 遇到的问题