[BUG]: Import c-module from interpreter embedded in dlopen shared object raises undefined symbol (unix-only)
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 18k
- Forks
- 2.3k
- Avg merge
- 5d 17h
- Merged PRs (30d)
- 10
Description
Required prerequisites
- Make sure you've read the documentation. Your issue may be addressed there.
- Search the issue tracker and Discussions to verify that this hasn't already been reported. +1 or comment there if it has.
- Consider asking first in the Gitter chat room or in a Discussion.
Problem description
TL;DR; The problem can be seen in a very specific setup: the interpreter is embedded in a shared library. The shared library is dlopened by an application. When the interpreter tries to import a python c-module, some python symbols are undefined and everything crashes. The solution is to dlopen the python library with RTLD_NOW | RTLD_GLOBAL.
I'm noticing the problem while writing a shared library with an exposed C interface that uses the embedded interpreter. If the functions defined in the shared object try to import libraries that imports numpy, or try to directly import numpy, the process fails with the following message:
terminate called after throwing an instance of 'pybind11::error_already_set'
what(): ImportError:
https://numpy.org/devdocs/user/troubleshooting-importerror.html
* The Python version is: Python3.8 from "/usr/bin/python3"
* The NumPy version is: "1.20.3"
and make sure that they are the versions you expect.
Please carefully study the documentation linked above for further help.
Original error was: /usr/local/lib/python3.8/dist-packages/numpy/core/_multiarray_umath.cpython-38-x86_64-linux-gnu.so: undefined symbol: PyExc_RecursionError
At:
/usr/local/lib/python3.8/dist-packages/numpy/core/__init__.py(51): <module>
<frozen importlib._bootstrap>(219): _call_with_frames_removed
<frozen importlib._bootstrap_external>(848): exec_module
<frozen importlib._bootstrap>(686): _load_unlocked
<frozen importlib._bootstrap>(975): _find_and_load_unlocked
<frozen importlib._bootstrap>(991): _find_and_load
<frozen importlib._bootstrap>(219): _call_with_frames_removed
<frozen importlib._bootstrap>(1050): _handle_fromlist
<frozen importlib._bootstrap>(219): _call_with_frames_removed
<frozen importlib._bootstrap>(961): _find_and_load_unlocked
irb(main):003:1* module TestMain
=> #<FFI::Function address=0x00007f9d0ba43bb6>
irb(main):008:0>
irb(main):009:0> TestMain.main
terminate called after throwing an instance of 'pybind11::error_already_set'
what(): ImportError:
IMPORTANT: PLEASE READ THIS FOR ADVICE ON HOW TO SOLVE THIS ISSUE!
Importing the numpy C-extensions failed. This error can happen for
many reasons, often due to issues with your setup or how NumPy was
installed.
We have compiled some common reasons and troubleshooting tips at:
https://numpy.org/devdocs/user/troubleshooting-importerror.html
Please note and check the following:
* The Python version is: Python3.8 from "/usr/bin/python3"
* The NumPy version is: "1.20.3"
and make sure that they are the versions you expect.
Please carefully study the documentation linked above for further help.
Original error was: /usr/local/lib/python3.8/dist-packages/numpy/core/_multiarray_umath.cpython-38-x86_64-linux-gnu.so: undefined symbol: PyExc_RecursionError
At:
/usr/local/lib/python3.8/dist-packages/numpy/core/__init__.py(51): <module>
<frozen importlib._bootstrap>(219): _call_with_frames_removed
<frozen importlib._bootstrap_external>(848): exec_module
<frozen importlib._bootstrap>(686): _load_unlocked
<frozen importlib._bootstrap>(975): _find_and_load_unlocked
<frozen importlib._bootstrap>(991): _find_and_load
<frozen importlib._bootstrap>(219): _call_with_frames_removed
<frozen importlib._bootstrap>(1050): _handle_fromlist
/usr/local/lib/python3.8/dist-packages/numpy/__init__.py(145): <module>
<frozen importlib._bootstrap>(219): _call_with_frames_removed
<frozen importlib._bootstrap_external>(848): exec_module
<frozen importlib._bootstrap>(686): _load_unlocked
<frozen importlib._bootstrap>(975): _find_and_load_unlocked
<frozen importlib._bootstrap>(991): _find_and_load
<frozen importlib._bootstrap>(219): _call_with_frames_removed
<frozen importlib._bootstrap>(961): _find_and_load_unlocked
<frozen importlib._bootstrap>(991): _find_and_load
- pybind: 2.8.1
- numpy: tested several versions, from 1.19 to latest, with the latter also compiled locally.
- python: tested on both 3.6 and 3.8 on Ubuntu 18.04 and ubuntu 20.04. It works on Windows 10 x64
With respect to other issues:
- #3543 : maybe related?
- #3112 : I'm quite sure the sample I'm attaching is loading the interpreter once
Reproducible example code
This should be a minimal example to reproduce the issue:
// main.cc
#include "pybind11/embed.h"
namespace py = pybind11;
extern "C" {
int main() {
py::scoped_interpreter guard{};
auto py_module = py::module::import("numpy");
auto version = py_module.attr("__version__");
py::print(version);
return 0;
}
}
Using the following cmake should be possible to compile both an executable (issue_main) and a shared object (libissue.so). The library can be tested with the target loader.
cmake_minimum_required(VERSION 3.14)
include(FetchContent)
FetchContent_Declare(
pybind11
GIT_REPOSITORY https://github.com/pybind/pybind11
GIT_TAG v2.8.1)
FetchContent_MakeAvailable(pybind11)
project(
pybind_issue
LANGUAGES C CXX
VERSION 1.0.0)
add_library(issue SHARED main.cc)
set_target_properties(issue PROPERTIES
POSITION_INDEPENDENT_CODE ON
CXX_STANDARD 11)
target_link_libraries(issue PRIVATE pybind11::embed)
add_executable(issue_main main.cc)
set_target_properties(issue_main PROPERTIES
POSITION_INDEPENDENT_CODE ON
CXX_STANDARD 11)
target_link_libraries(issue_main PRIVATE pybind11::embed)
add_executable(loader load.cc)
target_link_libraries(loader PRIVATE ${CMAKE_DL_LIBS})
where the loader has the following code:
#include <dlfcn.h>
int main() {
void * lib = dlopen("./libissue.so", RTLD_NOW);
int(*fnc)(void) = (int(*)(void))dlsym(lib, "main");
fnc();
dlclose(lib);
return 0;
}
While running ./issue_main it is possible to get the current version of numpy, loading the shared object via ./loader, the previous stack trace is obtained. It is possible to remove the loader from the equation using another method to load the library (e.g. Ruby FFI):
require 'ffi'
module IssueLib
extend FFI::Library
ffi_lib './libissue.so'
attach_function :main, [], :int
end
IssueLib.main()
reports the same stack trace.
EDIT:
Test on windows
With the following modifications:
// main.cc
#include "pybind11/embed.h"
namespace py = pybind11;
extern "C" {
__declspec(dllexport) int main() {
py::scoped_interpreter guard{};
auto py_module = py::module::import("numpy");
auto version = py_module.attr("__version__");
py::print(version);
return 0;
}
}
#include <windows.h>
int main() {
HMODULE lib = LoadLibrary("./issue.dll");
int(*fnc)(void) = (int(*)(void))GetProcAddress(lib, "main");
fnc();
FreeLibrary(lib);
return 0;
}
it works correctly on Windows 10 x64.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the C++ reproducer in main.cc, the dlopen loader in load.cc, and the supplied CMake configuration. Compare running issue_main with loading libissue.so through loader on Unix, then verify the behavior against the documented Windows case; done means importing NumPy succeeds from the dlopen-loaded shared object without breaking the executable case.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cmake, cpp, numpy, python
- Domain
- backend, build-system
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100