c++ pybind11模块导入或链接.so依赖关系macOS

68bkxrlz  于 2022-12-20  发布在  Mac
关注(0)|答案(1)|浏览(426)

TLDR:在python中导入pybind 11模块时,如何链接.so/import依赖项?
我试图构建一个pybind 11模块,它部分依赖于另一个python库的C++部分,在Linux上,我可以使用target_link_libraries在CMake中链接该库--这不适用于macOS(can't link with bundle (MH_BUNDLE) only dylibs (MH_DYLIB) file)上的.so库。
当在macOS上导入pybind 11生成的模块而没有在Python中链接时,我得到了一个ImportError: dlopen(/path/to/my_module.cpython-38-darwin.so, 0x0002): symbol not found in flat namespace (__<mangled symbol that is part of the library my module depends on>)。这可以通过在导入我自己的模块之前在Python中导入依赖本身来防止。
有没有办法链接那个库,或者确保Python在运行import my_module时,在加载我的二进制文件之前导入依赖项?
我尝试将共享库文件放在一个文件夹中,该文件夹包含__init__.py,它首先导入依赖项,然后从.so导入*--但这导致一些导入不再工作(例如,import my_module.my_submodule失败)。
编辑:一个可行的,但麻烦的,直接的解决方案是添加一个虚拟模块到管道中,也就是说,将原来的my_module重命名为_my_module,并创建一个虚拟的my_module,除了导入依赖项之外什么也不做:

#include <Python.h>

PyMODINIT_FUNC
PyInit_my_module(void)
{
    PyImport_ImportModule("the_dependency");
    return PyImport_ImportModule("_my_module");
}
goucqfw6

goucqfw61#

这不是一个理想的解决方案,但似乎是解决import-before-binary问题的最佳方法,同时还保留了以正常情况下相同的方式使用导入模块的能力,这是通过在导入原始模块之前使用一个伪模块导入python依赖项(其中包含相关的C++依赖项.so)来实现的。
假设使用CMake来编译项目,下面是如何完成的。
1.如果模块是为macOS编译的,则有条件地将模块名称设置为_my_module而不是my_module:

if (APPLE)
    set(MAIN_LIB_NAME _my_module)
else()
    set(MAIN_LIB_NAME my_module)
endif()
    
pybind11_add_module(${MAIN_LIB_NAME}
                    src/source1.cpp
                    # your source files, as before
)

1.添加一个使用原始名称的伪模块,然后使用该模块导入依赖项并加载实际模块

if (APPLE)
    pybind11_add_module(my_module macos_dummy.h macos_dummy.cpp)
elseif (UNIX)
    # in my case, on linux I just linked against the .so
    target_link_libraries(my_module PUBLIC my_dependency)
endif()

1.在你的原始模块中定义一个PYBIND11_MODULE,它使用伪名称,这样Python以后就可以正确地导入它(也就是说,让Pybind声明PyInit_函数),同时保留你的原始PYBIND11_MODULE(使用原始名称):

#ifdef __APPLE__ // If apple, a dummy module is added, so that the dependency can be imported before loading the actual binary
PYBIND11_MODULE(_my_module, m) {
    m.doc() = "dummy module; doesn't do anything; if you see this instead of the actual module, something went wrong.";
}
#endif

PYBIND11_MODULE(my_module, m) {  // the original module, left unchanged
// ...

1.实现实际的伪模块,也就是使用Python的导入机制导入依赖项,找到原始模块并假装一直是原始模块:

#include <dlfcn.h>
#include <macos_dummy.h>

typedef PyObject* (*PyInitFunc)(void);

PyMODINIT_FUNC PyInit_my_module(void)
{
    PyImport_ImportModule("my_dependency");  // import the dependency, this is the entire reason this exists in the first place
    PyObject* obj = PyImport_ImportModule("_my_module");  // let python find the correct binary
    const char* actual_module_path = PyUnicode_AsUTF8(PyObject_GetAttrString(obj, "__file__")); // get the path of the binary found by python

    void* actual_module = dlopen(actual_module_path, RTLD_LAZY | RTLD_GLOBAL);  // access the binary
    if (!actual_module) {
        printf("Module %s not found\n", actual_module_path);
        return NULL;
    } else {
        PyInitFunc actual_pyinit = dlsym(actual_module, "PyInit_my_module");  // retrieve the actual module
        return actual_pyinit();
    }
}

以及相关的报头:

#ifndef MY_MODULE_MACOS_DUMMY_H
#define MY_MODULE_MACOS_DUMMY_H
#include <Python.h>

__attribute__((visibility("default"))) PyMODINIT_FUNC PyInit_my_module(void);

#endif //MY_MODULE_MACOS_DUMMY_H

就是这样,从现在开始,假设两个生成的.so文件都在路径中,那么在原始名称下导入模块也会导入依赖项。

相关问题