能否在使用RTLD_LOCAL方式加载的多个库中合并弱符号(如vtable/typeinfo)?

5

为了说明情况:我有一个Java项目,其中部分使用了两个JNI库。以libbar.solibfoo.so为例。如果这些是系统库,

System.loadLibrary("bar");

如果只是使用已有的库,那么问题就迎刃而解了。但由于这些库是我自己打包到JAR文件中的,所以我需要做类似于下面这样的操作:

System.load("/path/to/libfoo.so");
System.load("/path/to/libbar.so");

需要先安装libfoo,否则libbar无法找到它,因为它不在系统库搜索路径中。

这样做已经运作良好一段时间了,但我现在遇到了一个问题,即使类型正确,std::any_cast仍然会抛出std::bad_any_cast。 我追踪了一下发现,这两个库对该类型的typeinfo有不同的定义,并且它们在运行时没有被合并。 这似乎是因为System.load()最终使用RTLD_LOCAL而不是RTLD_GLOBAL调用dlopen()

我写了这个示例来演示这种行为,而不需要JNI:

foo.hpp

class foo { };

extern "C" const void* libfoo_foo_typeinfo();

foo.cpp

#include "foo.hpp"
#include <typeinfo>

extern "C" const void* libfoo_foo_typeinfo()
{
    return &typeid(foo);
}

bar.cpp

#include "foo.hpp"
#include <typeinfo>

extern "C" const void* libbar_foo_typeinfo()
{
    return &typeid(foo);
}

main.cpp

#include <iostream>
#include <typeinfo>
#include <dlfcn.h>

int main() {
    void* libfoo = dlopen("./libfoo.so", RTLD_NOW | RTLD_LOCAL);
    void* libbar = dlopen("./libbar.so", RTLD_NOW | RTLD_LOCAL);

    auto libfoo_fn = reinterpret_cast<const void* (*)()>(
        dlsym(libfoo, "libfoo_foo_typeinfo"));
    auto libbar_fn = reinterpret_cast<const void* (*)()>(
        dlsym(libbar, "libbar_foo_typeinfo"));

    auto libfoo_ti = static_cast<const std::type_info*>(libfoo_fn());
    auto libbar_ti = static_cast<const std::type_info*>(libbar_fn());

    std::cout << std::boolalpha
              << (libfoo_ti == libbar_ti) << "\n"
              << (*libfoo_ti == *libbar_ti) << "\n";
    return 0;
}

Makefile

all: libfoo.so libbar.so main

libfoo.so: foo.cpp
        $(CXX) -fpic -shared -Wl,-soname=$@ $^ -o $@

libbar.so: bar.cpp
        $(CXX) -fpic -shared -Wl,-soname=$@ $^ -L. -lfoo -o $@

main: main.cpp
        $(CXX) $^ -ldl -o $@
在我的系统上,我得到了:
$ make
...
$ ./main
false
true

这是因为尽管类型信息地址不同,但GCC的libstdc++使用了编码后的名称来确定相等性。例如,在LLVM的libc++中,相等性基于类型信息地址本身,这样就会得到:

$ make CXX="clang++ -stdlib=libc++"
$ ./main
false
false

如果我传递 RTLD_GLOBAL,那么我就会看到...
true
true

如果我先编辑 main.cpp 以先加载 libbar.so ,只需告诉它可以找到 libfoo.so ,它也可以正常工作:

$ LD_LIBRARY_PATH=. ./main
true
true

但出于本篇文章开头所述的原因,这两种方法都不是可行的解决方案。

这与https://github.com/android-ndk/ndk/issues/533非常相似,但是它涉及到非动态类型,因此没有办法添加一个“关键函数”来强制 typeinfo 成为一个强符号。我碰巧在 Android 上首先复现了这个问题,但它并不特定于 Android。


问题问得很好。不幸的是,我没有答案。但是,因为问题质量高,我会给你点个赞。 - Jesper Juhl
你不应该使用C++来编写共享库:C++因与动态加载不兼容而闻名。 - Lorinczy Zsigmond
@LorinczyZsigmond 我认为那个建议有点悲观。确实,用C++编写动态库存在更多的困难,但也有许多成功的项目,比如Qt。 - Tavian Barnes
1个回答

2
不,这是不可能的。RTLD_LOCAL旨在防止这种情况发生,不幸的是必须用于System.loadLibrary,否则如果你加载了两个定义了不同foo类的库,将会发生糟糕的事情。

尽管使用了 RTLD_LOCALlibbar.so 仍能够引用 libfoo.so 中的符号。例如,如果你将 libbar_foo_typeinfo() 更改为 return libfoo_foo_typeinfo();,一切都可以正常工作。但是,令人奇怪的是,这些含糊的链接 typeinfo 符号无法合并。 - Tavian Barnes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接