2020-11-03

Embedding Python in C++ with boost::python - importing modules

Boost offers quite nice functionality for embedding Python in C++ and exporting C++ to Python: boost python.

I'm not going to discuss the library in depth, but instead I'd like to focus on a specific problem. Let's consider a simple example. We have a Python module in our project that defines a function, which we'd like to execute in our C++ code:

def sample_function(x):
  """Returns twice the x value"""
  return 2.0 * x

It can be done using boost::python in the following way:

#include <iostream>
#include <boost/python.hpp>

using namespace std;
namespace python = boost::python;

int main(int argc, char* argv[]) {
  Py_Initialize(); 
  python::object sample_module = python::import("sample_module");
  python::object sample_function = sample_module.attr("sample_function");
  python::object result = sample_function(5.0);
  float y = python::extract<float>(result);
  cout << y << endl;
  return 0;
}

If you try to run it, you are going to encounter an issue: Python doesn't know where to look for the module! It's not in the PYTHONPATH. It won't even try the current working directory.

A quick Google search suggests several possible solutions.

1. You may try to set PYTHONPATH using setenv():

#include <cstdlib>

...

setenv("PYTHONPATH", ".", 1);

Py_Initialize();

This will tell Python to look for the module in the directory you run the program from. So it will only work if you always launch it from that directory. Furthermore, you discard everything else that may have happened to be in PYTHONPATH, e.g. location of other imported modules. It is also a bit ugly to have to rely on an environment variable.

2. Execute sys.path.append() before importing module. You can do that with '.' or the full path, e.g:

Py_Initialize(); 

PyRun_SimpleString("import sys\nsys.path.append(\"/home/user/path/to/module\")");

That doesn't discard PYTHONPATH. If you use '.', the drawback is that it still won't work if you run your program from a different directory. If you provide the full path - well, hardcoding is ugly.

3. The solution that I found that seems to work is to do the following.

Reading /proc/self/exe symlink gives you a path to the executable file. You can then use this path to build your relative path:

string exe_dir = fs::read_symlink("/proc/self/exe").parent_path().string();

string module_path = exe_dir + "/../scripts";

cout << module_path << endl;

Py_Initialize();

PyRun_SimpleString(("import sys\nsys.path.append(\"" + module_path + "\")").c_str());


The code is available: HERE

No comments:

Post a Comment