_ctypes.cpython-39-x86_64-linux-gnu.so: undefined symbol: PyFloat_Type in embedded Python loaded with dlopen

When a C-extension is imported in CPython on Linux, dlopen is used under the hood (and per default with RTLD_LOCAL-flag).

A C-extension usually needs functionality from the Python-library (libpythonX.Y.so), like for example PyFloat_Type. However, on Linux the C-extension isn't linked against the libpythonX.Y.so (the situation is different on Windows, see this or this for more details) - the missing function-definition/functionality will be provided by the python-executable.

In order to be able to do so, the executable must be linked with -Xlinker -export-dynamic, otherwise loader will not be able to use the symbols from the executable for shared objects loaded with dlopen.

Now, if the embedded python is not the executable, but a shared object, which loaded with dlopen itself, we need to ensure that its symbols are added to the dynamic table. Building this shared object with -Xlinker -export-dynamic doesn't make much sense (it is not an executable after all) but doesn't break anything - the important part, how dlopen is used.

In order to make symbols from text.python.so visible for shared objects loaded later with dlopen, it should be opened with flag RTLD_GLOBAL:

RTLD_GLOBAL The symbols defined by this shared object will be made available for symbol resolution of subsequently loaded shared objects.

i.e.

shared_lib = dlopen(path_to_so_file, RTLD_GLOBAL | RTLD_NOW);

Warning: RTLD_LAZY should not be used.

The issue with RTLD_LAZY is that C-extensions do not have dependency on the libpython (as can be seen with help of ldd), so once they are loaded and a symbol (e.g. PyFloat_Type) from libpython which is not yet resolved must be looked up, dynamic linker doesn't know that it has to look into the libpython.

On the other hand with RTLD_NOW, all symbols are resolved and are visible when a C-extension is loaded (it is the same situation as in the "usual" case when libpython is linked in during the linkage step with -Xlinker -export-dynamic) and thus there is no issue finding e.g. PyFloat_Type-symbol.


As long as the embedded python is loaded with dlopen, the main executable doesn't need to be built/linked with -Xlinker -export-dynamic.

However, if the main executable is linked against the embedded-python-shared-object, -Xlinker -export-dynamic is necessary, otherwise the python-symbols want be visible when dlopen is used during the import of c-extension.


One might ask, why aren't C-extension linked against libpython in the first place?

Due to used RTLD_LOCAL, every C-extension would have its own (uninitialized) version of Python-interpreter (as the symbols from the libpython would not be interposed) and crash as soon as used.

To make it work, dlopen should be open with RTLD_GLOBAL-flag - but this is not a sane default option.