This answer provides a prototype for Python3 (which can be easily adapted for Python2) and shows how several cython-modules can be bundled into single extension/shared-library/pyd-file.
I keep it around for historical/didactical reasons - a more concise recipe is given in this answer, which present a good alternative to @Mylin's proposal of putting everything into the same pyx-file.
The question of multiple modules in the same shared object is also discussed in PEP489, where two solutions are proposed:
- one similar to this and to the already above referred answer with extending Finders with proper functionality
- second solution is to introduce symlink with "right" names, which would show to the common module (but here the advantages of having one common module are somehow negated).
Preliminary note: Since Cython 0.29, Cython uses multi-phase initialization for Python>=3.5. One needs to switch multi-phase initialization off (otherwise PyInit_xxx
isn't sufficient, see this SO-post), which can be done by passing -DCYTHON_PEP489_MULTI_PHASE_INIT=0
to gcc/other compiler.
When bundling multiple Cython-extension (let's call them bar_a
and bar_b
) into one single shared object (let's call it foo
), the main problem is the import bar_a
operation, because of the way the loading of modules works in Python (obviously simplified, this SO-post has more info):
- Look for
bar_a.so
(or similar), use ldopen
for loading the shared library and call PyInit_bar_a
which would initialize/register the module, if not successful
- Look for
bar_a.py
and load it, if not successful...
- Look for
bar_a.pyc
and load it, if not successful - error.
The steps 2. and 3. will obviously fail. Now, the issue is that there is no bar_a.so
to be found and albeit the initialization function PyInit_bar_a
can be found in foo.so
, Python doesn't know where to look and gives up on searching.
Luckily, there are hooks available, so we can teach Python to look in the right places.
When importing a module, Python utilizes finders from sys.meta_path
, which return the right loader for a module (for simplicity I'm using the legacy workflow with loaders and not module-spec). The default finders return None
, i.e. no loader and it results in the import error.
That means we need to add a custom finder to sys.meta_path
, which would recognize our bundled modules and return loaders, which in their turn would call the right PyInit_xxx
-function.
The missing part: How should the custom finder finds its way into the sys.meta_path
? It would be pretty inconvenient if the user would have to do it manually.
When a submodule of a package is imported, first the package's __init__.py
-module is loaded and this is the place where we can inject our custom finder.
After calling python setup.py build_ext install
for the setup presented further below, there is a single shared library installed and the submodules can be loaded as usual:
>>> import foo.bar_a as a
>>> a.print_me()
I'm bar_a
>>> from foo.bar_b import print_me as b_print
>>> b_print()
I'm bar_b
###Putting it all together:
Folder structure:
../
|-- setup.py
|-- foo/
|-- __init__.py
|-- bar_a.pyx
|-- bar_b.pyx
|-- bootstrap.pyx
init.py:
# bootstrap is the only module which
# can be loaded with default Python-machinery
# because the resulting extension is called `bootstrap`:
from . import bootstrap
# injecting our finders into sys.meta_path
# after that all other submodules can be loaded
bootstrap.bootstrap_cython_submodules()
bootstrap.pyx:
import sys
import importlib
# custom loader is just a wrapper around the right init-function
class CythonPackageLoader(importlib.abc.Loader):
def __init__(self, init_function):
super(CythonPackageLoader, self).__init__()
self.init_module = init_function
def load_module(self, fullname):
if fullname not in sys.modules:
sys.modules[fullname] = self.init_module()
return sys.modules[fullname]
# custom finder just maps the module name to init-function
class CythonPackageMetaPathFinder(importlib.abc.MetaPathFinder):
def __init__(self, init_dict):
super(CythonPackageMetaPathFinder, self).__init__()
self.init_dict=init_dict
def find_module(self, fullname, path):
try:
return CythonPackageLoader(self.init_dict[fullname])
except KeyError:
return None
# making init-function from other modules accessible:
cdef extern from *:
"""
PyObject *PyInit_bar_a(void);
PyObject *PyInit_bar_b(void);
"""
object PyInit_bar_a()
object PyInit_bar_b()
# wrapping C-functions as Python-callables:
def init_module_bar_a():
return PyInit_bar_a()
def init_module_bar_b():
return PyInit_bar_b()
# injecting custom finder/loaders into sys.meta_path:
def bootstrap_cython_submodules():
init_dict={"foo.bar_a" : init_module_bar_a,
"foo.bar_b" : init_module_bar_b}
sys.meta_path.append(CythonPackageMetaPathFinder(init_dict))
bar_a.pyx:
def print_me():
print("I'm bar_a")
bar_b.pyx:
def print_me():
print("I'm bar_b")
setup.py:
from setuptools import setup, find_packages, Extension
from Cython.Build import cythonize
sourcefiles = ['foo/bootstrap.pyx', 'foo/bar_a.pyx', 'foo/bar_b.pyx']
extensions = cythonize(Extension(
name="foo.bootstrap",
sources = sourcefiles,
))
kwargs = {
'name':'foo',
'packages':find_packages(),
'ext_modules': extensions,
}
setup(**kwargs)
NB: This answer was the starting point for my experiments, however it uses PyImport_AppendInittab
and I cannot see a way how can this be plugged in into the normal python.