4

I have a python project that I want to call from a c++ application. I would like to bundle all python sources together in a single shared library and link the c++ application to that library. Right now my cython setup.py creates one *.so per python source, which is very inconvenient.

Here is the setup.py file:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

sourcefiles = ['project_interface.pyx', 'impl_file1.pyx']

setup(
    ext_modules = cythonize(sourcefiles)
)

project_interface.pyx :

# distutils: language = c++

import impl_file1

cdef public void HelloWorld():
    print "Calling Hello World"
    impl_file1.helloworld_func()

impl_file1.pyx :

def helloworld_func():
    print 'Hello World'

I tried to modify setup.py to bundle all python code in a single library like this:

setup(
      ext_modules = cythonize([Extension("my_project", sourcefiles, language='c++')])
)

Unfortunately, when executing void HelloWorld(), the application cannot file impl_file1 anymore. I get :

Calling Hello World
NameError: name 'impl_file1' is not defined
Exception NameError: "name 'impl_file1' is not defined" in 'project_interface.HelloWorld' ignored

The c++ program driving this is:

#include <Python.h>
#include "project_interface.h"

int main(int argc, const char** argv){
    Py_Initialize();
    initproject_interface();
    HelloWorld();
    Py_Finalize();


    return 0;
}

This application works correctly when compiling with multiple *.so files.

Compilation is very straightforward in either cases:

python setup.py build_ext --inplace
mv my_project.so libmy_project.so
g++ main.cpp -o main `python2-config --cflags --ldflags` -L. -lmy_project

Is there any way to get the single shared library solution to work?

sophros
  • 14,672
  • 11
  • 46
  • 75
Eric
  • 19,525
  • 19
  • 84
  • 147

2 Answers2

2

There's a number of similar looking questions about bundling multiple Cython modules together (e.g. 1, 2) which isn't really viable because Python uses file paths to handle modules. However, this question isn't quite the same because you're calling it from C++, which gives you an extra option.

You need to use the C API function PyImport_AppendInittab to Python to treat impl_file1 as a builtin module so it doesn't search the path for a file to import. Start by providing a declaration of the import function (since you won't get that from your header file):

extern "C" {
// PyObject* PyInit_impl_file1(); // Python 3
void initimpl_file1(); // Python 2
}

Then, in main, before Py_Initialize, add:

PyImport_AppendInittab("impl_file1", initimpl_file1); // change the name for Python 3
DavidW
  • 29,336
  • 6
  • 55
  • 86
  • Your answer seems to be the "official" way: https://cython.readthedocs.io/en/latest/src/userguide/source_files_and_compilation.html#integrating-multiple-modules. But does it really work? I get `ImportError: 'xxxx' is not a built-in module` if I try it with Python3, due to this sanity check here https://github.com/python/cpython/blob/e42b705188271da108de42b55d9344642170aa2b/Lib/importlib/_bootstrap.py#L233. I'm adding the module-name to `sys.builtin_module_names` to work around it, unsure this is the best way. – ead Oct 08 '18 at 08:07
  • @ead I did test my answer with Python 3 (on Linux) so I can confirm that it works for me. (I haven't tested it with Python 2 though, just because I don't have the headers etc installed). The line of code you link to looks to be 10 years old, so I can't imagine this is happening because you're running a different version than me – DavidW Oct 08 '18 at 08:36
  • I have added my minimal example as answer (https://stackoverflow.com/a/52698794/5769463) would be interesting to know, why the difference. – ead Oct 08 '18 at 09:05
  • I noticed that adding a single `cdef public void func()` function in impl_file1.pyx will generate a impl_file1.h header containing the initimpl_file1() call, reducing the amount of duct tape required – Eric Oct 08 '18 at 09:36
1

For me (in a slightly different scenario, but I didn't expect a difference) the solution of @DavidW needs some tweaking. Here is my setup:

foo.pyx:

cdef extern from "Python.h":
    int PyImport_AppendInittab(const char *name, object (*initfunc)())


cdef extern from *:
    """
    PyObject *PyInit_bar(void);
    """
    object PyInit_bar()

PyImport_AppendInittab("bar", PyInit_bar)

import bar  # HERE The error happens

bar.pyx:

print("bar imported")

setup.py:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

sourcefiles = ['foo.pyx', 'bar.pyx']


setup(
    ext_modules = cythonize([Extension("foo", sourcefiles)])
)

Now after building with python setup.py build_ext -i, result in an error:

import foo
ImportError: 'bar' is not a built-in module

coming from here. To work around I have to add the name "bar" to sys.builtin_module_names:

...
import sys
sys.builtin_module_names = list(sys.builtin_module_names)+["bar"]
import bar
ead
  • 32,758
  • 6
  • 90
  • 153
  • If I understand correctly, you moved the PyImport_AppendInitTab call to foo.pyx. In that case, foo clients won't have to call it manually. – Eric Oct 08 '18 at 09:12
  • I believe AppendInitTab needs to be called before Python is initialised, which means you can't really put it in a Cython module. Thus my solution works with embedding Python into C(++) but not as a general approach to bundling Cython modules together. – DavidW Oct 08 '18 at 10:06
  • @Eric That is exactly what I tried to achieve, but it doesn't seem to be that straight forward. – ead Oct 08 '18 at 10:52
  • I found some workaround. You can put all the `PyImport_AppendInittab` calls inside a cpp file and add that file to the `sourcefiles` in setup.py. You then call that function once to register everything. The idea is that the client doesn't have to know about the details of the resulting library at all – Eric Oct 08 '18 at 13:38
  • @Eric probably even better: you could initialize a dummy-global variable via call to your common-init-function (obviously, side effects is what you are interested in). Then this function will be called automatically prior to main (as soon as the so is loaded) without the need to call it explicitly. – ead Oct 09 '18 at 12:21
  • I get into an infinite loop with this. I'll try to dig further – Eric Oct 09 '18 at 20:31
  • @Eric You get into the infinite loop with my answer but not with DavidW's? Please keep me updated if you will find something out. Btw. The ongoing discussion here might be intersting for you: https://stackoverflow.com/q/30157363/5769463 – ead Oct 10 '18 at 04:08