从 pybind11 包装的代码动态链接共享库
Dynamically linking a shared library from a pybind11-wrapped code
我正在尝试将 python 绑定添加到中型 C++ 科学代码(大约数万个 LOC)。我已经设法让它在没有太多问题的情况下工作,但我现在遇到了一个我无法自己解决的问题。代码组织如下:
- 所有类和数据结构都编译在一个库中
libcommon.a
- 可执行文件是通过 linking 这个库
创建的
- pybind11 用于创建
core.so
python 模块
"main" 部分的绑定工作正常。事实上,从独立代码或 python 启动的模拟给出了完全相同的结果。
但是,该代码还支持类似插件的系统,可以在运行时加载共享库。这些共享库包含继承自主代码中定义的接口的 类。事实证明,如果我尝试从 python link 这些共享库,我会得到臭名昭著的 "undefined symbol" 错误。我检查过这些符号在 core.so
模块中(使用 nm -D
)。事实上,使用独立代码执行动态 linking 的模拟效果很好(在同一文件夹中并使用相同的输入)。不知何故,共享库在通过 python 调用时找不到正确的符号,但在由独立代码加载时没有问题。我正在使用 CMake 构建系统。
接下来是MCE。将每个文件复制到一个文件夹中,将 pybind11
文件夹复制(或 link)到同一位置并使用以下命令:
mkdir build
cd build
cmake ..
make
这将生成一个 standalone
二进制文件和一个 python 模块。 standalone
可执行文件将产生正确的输出。相比之下,在 python3 中使用以下命令(至少在我看来应该是等效的)会产生错误:
import core
b = core.load_plugin()
main.cpp
#include "Base.h"
#include "plugin_loader.h"
#include <iostream>
int main() {
Base *d = load_plugin();
if(d == NULL) {
std::cerr << "No lib found" << std::endl;
return 1;
}
d->foo();
return 0;
}
Base.h
#ifndef BASE
#define BASE
struct Base {
Base();
virtual ~Base();
virtual void foo();
};
#endif
Base.cpp
#include "Base.h"
#include <iostream>
Base::Base() {}
Base::~Base() {}
void Base::foo() {
std::cout << "Hey, it's Base!" << std::endl;
}
plugin_loader.h
#ifndef LOADER
#define LOADER
#include "Base.h"
Base *load_plugin();
#endif
plugin_loader.cpp
#include "plugin_loader.h"
#include <dlfcn.h>
#include <iostream>
typedef Base* make_base();
Base *load_plugin() {
void *handle = dlopen("./Derived.so", RTLD_LAZY | RTLD_GLOBAL);
const char *dl_error = dlerror();
if(dl_error != nullptr) {
std::cerr << "Caught an error while opening shared library: " << dl_error << std::endl;
return NULL;
}
make_base *entry = (make_base *) dlsym(handle, "make");
return (Base *) entry();
}
Derived.h
#include "Base.h"
struct Derived : public Base {
Derived();
virtual ~Derived();
void foo() override;
};
extern "C" Base *make() {
return new Derived();
}
Derived.cpp
#include "Derived.h"
#include <iostream>
Derived::Derived() {}
Derived::~Derived() {}
void Derived::foo() {
std::cout << "Hey, it's Derived!" << std::endl;
}
bindings.cpp
#include <pybind11/pybind11.h>
#include "Base.h"
#include "plugin_loader.h"
PYBIND11_MODULE(core, m) {
pybind11::class_<Base, std::shared_ptr<Base>> base(m, "Base");
base.def(pybind11::init<>());
base.def("foo", &Base::foo);
m.def("load_plugin", &load_plugin);
}
CMakeLists.txt
PROJECT(foobar)
# compile the library
ADD_LIBRARY(common SHARED Base.cpp plugin_loader.cpp)
TARGET_LINK_LIBRARIES(common ${CMAKE_DL_LIBS})
SET_TARGET_PROPERTIES(common PROPERTIES POSITION_INDEPENDENT_CODE ON)
# compile the standalone code
ADD_EXECUTABLE(standalone main.cpp)
TARGET_LINK_LIBRARIES(standalone common)
# compile the "plugin"
SET(CMAKE_SHARED_LIBRARY_PREFIX "")
ADD_LIBRARY(Derived SHARED Derived.cpp)
# compile the bindings
ADD_SUBDIRECTORY(pybind11)
INCLUDE_DIRECTORIES( ${PROJECT_SOURCE_DIR}/pybind11/include )
FIND_PACKAGE( PythonLibs 3 REQUIRED )
INCLUDE_DIRECTORIES( ${PYTHON_INCLUDE_DIRS} )
ADD_LIBRARY(_oxpy_lib STATIC bindings.cpp)
TARGET_LINK_LIBRARIES(_oxpy_lib ${PYTHON_LIBRARIES} common)
SET_TARGET_PROPERTIES(_oxpy_lib PROPERTIES POSITION_INDEPENDENT_CODE ON)
pybind11_add_module(core SHARED bindings.cpp)
TARGET_LINK_LIBRARIES(core PRIVATE _oxpy_lib)
你是对的,导入库中的符号不可见,因为 core
加载时没有设置 RTLD_GLOBAL
标志。您可以在 python 侧添加几行来解决此问题:
import sys, os
sys.setdlopenflags(os.RTLD_GLOBAL | os.RTLD_LAZY)
import core
b = core.load_plugin()
来自 sys.setdlopenflags()
doc:
To share symbols across extension modules, call as sys.setdlopenflags(os.RTLD_GLOBAL)
. Symbolic names for the flag values can be found in the os
module (RTLD_xxx
constants, e.g. os.RTLD_LAZY
).
我正在尝试将 python 绑定添加到中型 C++ 科学代码(大约数万个 LOC)。我已经设法让它在没有太多问题的情况下工作,但我现在遇到了一个我无法自己解决的问题。代码组织如下:
- 所有类和数据结构都编译在一个库中
libcommon.a
- 可执行文件是通过 linking 这个库 创建的
- pybind11 用于创建
core.so
python 模块
"main" 部分的绑定工作正常。事实上,从独立代码或 python 启动的模拟给出了完全相同的结果。
但是,该代码还支持类似插件的系统,可以在运行时加载共享库。这些共享库包含继承自主代码中定义的接口的 类。事实证明,如果我尝试从 python link 这些共享库,我会得到臭名昭著的 "undefined symbol" 错误。我检查过这些符号在 core.so
模块中(使用 nm -D
)。事实上,使用独立代码执行动态 linking 的模拟效果很好(在同一文件夹中并使用相同的输入)。不知何故,共享库在通过 python 调用时找不到正确的符号,但在由独立代码加载时没有问题。我正在使用 CMake 构建系统。
接下来是MCE。将每个文件复制到一个文件夹中,将 pybind11
文件夹复制(或 link)到同一位置并使用以下命令:
mkdir build
cd build
cmake ..
make
这将生成一个 standalone
二进制文件和一个 python 模块。 standalone
可执行文件将产生正确的输出。相比之下,在 python3 中使用以下命令(至少在我看来应该是等效的)会产生错误:
import core
b = core.load_plugin()
main.cpp
#include "Base.h"
#include "plugin_loader.h"
#include <iostream>
int main() {
Base *d = load_plugin();
if(d == NULL) {
std::cerr << "No lib found" << std::endl;
return 1;
}
d->foo();
return 0;
}
Base.h
#ifndef BASE
#define BASE
struct Base {
Base();
virtual ~Base();
virtual void foo();
};
#endif
Base.cpp
#include "Base.h"
#include <iostream>
Base::Base() {}
Base::~Base() {}
void Base::foo() {
std::cout << "Hey, it's Base!" << std::endl;
}
plugin_loader.h
#ifndef LOADER
#define LOADER
#include "Base.h"
Base *load_plugin();
#endif
plugin_loader.cpp
#include "plugin_loader.h"
#include <dlfcn.h>
#include <iostream>
typedef Base* make_base();
Base *load_plugin() {
void *handle = dlopen("./Derived.so", RTLD_LAZY | RTLD_GLOBAL);
const char *dl_error = dlerror();
if(dl_error != nullptr) {
std::cerr << "Caught an error while opening shared library: " << dl_error << std::endl;
return NULL;
}
make_base *entry = (make_base *) dlsym(handle, "make");
return (Base *) entry();
}
Derived.h
#include "Base.h"
struct Derived : public Base {
Derived();
virtual ~Derived();
void foo() override;
};
extern "C" Base *make() {
return new Derived();
}
Derived.cpp
#include "Derived.h"
#include <iostream>
Derived::Derived() {}
Derived::~Derived() {}
void Derived::foo() {
std::cout << "Hey, it's Derived!" << std::endl;
}
bindings.cpp
#include <pybind11/pybind11.h>
#include "Base.h"
#include "plugin_loader.h"
PYBIND11_MODULE(core, m) {
pybind11::class_<Base, std::shared_ptr<Base>> base(m, "Base");
base.def(pybind11::init<>());
base.def("foo", &Base::foo);
m.def("load_plugin", &load_plugin);
}
CMakeLists.txt
PROJECT(foobar)
# compile the library
ADD_LIBRARY(common SHARED Base.cpp plugin_loader.cpp)
TARGET_LINK_LIBRARIES(common ${CMAKE_DL_LIBS})
SET_TARGET_PROPERTIES(common PROPERTIES POSITION_INDEPENDENT_CODE ON)
# compile the standalone code
ADD_EXECUTABLE(standalone main.cpp)
TARGET_LINK_LIBRARIES(standalone common)
# compile the "plugin"
SET(CMAKE_SHARED_LIBRARY_PREFIX "")
ADD_LIBRARY(Derived SHARED Derived.cpp)
# compile the bindings
ADD_SUBDIRECTORY(pybind11)
INCLUDE_DIRECTORIES( ${PROJECT_SOURCE_DIR}/pybind11/include )
FIND_PACKAGE( PythonLibs 3 REQUIRED )
INCLUDE_DIRECTORIES( ${PYTHON_INCLUDE_DIRS} )
ADD_LIBRARY(_oxpy_lib STATIC bindings.cpp)
TARGET_LINK_LIBRARIES(_oxpy_lib ${PYTHON_LIBRARIES} common)
SET_TARGET_PROPERTIES(_oxpy_lib PROPERTIES POSITION_INDEPENDENT_CODE ON)
pybind11_add_module(core SHARED bindings.cpp)
TARGET_LINK_LIBRARIES(core PRIVATE _oxpy_lib)
你是对的,导入库中的符号不可见,因为 core
加载时没有设置 RTLD_GLOBAL
标志。您可以在 python 侧添加几行来解决此问题:
import sys, os
sys.setdlopenflags(os.RTLD_GLOBAL | os.RTLD_LAZY)
import core
b = core.load_plugin()
来自 sys.setdlopenflags()
doc:
To share symbols across extension modules, call as
sys.setdlopenflags(os.RTLD_GLOBAL)
. Symbolic names for the flag values can be found in theos
module (RTLD_xxx
constants, e.g.os.RTLD_LAZY
).