根据用户配置在运行时链接共享对象
Linking shared objects at runtime depending on user configuration
TL;DR
我有一个我想在我的程序中使用的库,它有两个不同的版本。两个版本都提供相同的接口,但用于编译它们的选项不同。
我现在想使用特定版本的库,但是,由于两个版本都适用于不同的任务,并且用户应该定义要执行的任务,所以我需要在运行时决定使用哪个库。
我知道我可以使用 dlopen
和 dlsym
来根据用户的选择在运行时加载正确的库,但是接口非常大,并且将我需要的所有内容加载到不同的函数指针中会很乏味...
问题
我有一个有两个不同版本的库。两个版本都提供相同的界面,但它们适用的任务不同。
这是文件树的样子:
lib
\ - lib_task1
\ - libsharedobj.so
\ - lib_task2
\ - libsharedobj.so
我想为用户提供选择在运行时执行哪个任务的可能性。因此,我还需要在运行时决定选择哪个库。
我的想法是编写一个包装器,它提供与库相同的接口,并且我将 dlopen
所需的库和 dlsym
相应的符号放入函数指针中。然而,库接口非常大,按照描述包装它会非常乏味,再加上它是一个 C 接口,所以它还包含很多我不希望在包装器之外看到的原始指针。
这是一个小例子
// library interface
typedef struct {
// ...
} a_type;
void do_something(a_type* param);
// wrapper
class LibWrapper {
private:
void (*do_something)(a_type*);
void* lib;
public:
LibWrapper(const bool task_one) { // specified by the user
if (task_one) {
lib = dlopen("/usr/lib/lib_task1/libsharedobj.so", RTLD_NOW);
} else {
lib = dlopen("/usr/lib/lib_task2/libsharedobj.so", RTLD_NOW);
}
do_something = dlsym(lib, "do_something");
}
~LibWrapper() {
if (lib) {
dlclose(lib);
}
}
void do_something(std::unique_ptr<a_type> param) {
do_something(param.get());
}
};
问题
有没有更好的方法,或者我真的需要一个一个地加载每个符号吗?
OS:ubuntu 14.04。
兼容性:C++11
您可以通过在启动程序之前设置环境变量 LD_LIBRARY_PATH
来解决这个问题。如有必要,编写一个启动程序来执行此操作:
LD_LIBRARY_PATH=lib/lib_task1 ./myprog # or lib_task2
那么ld.so
会先在指定的目录下寻找libsharedobj.so
,不管链接的是哪个(即ldd myprog
显示的是哪个)
您可以考虑使用 objcopy 重命名库中的 public 符号吗?
[根据要求提供更多详细信息]
如果您将两个库中的所有竞争函数重命名为 non-competing 命名空间,那么您可以同时加载它们,并在运行时 select 加载您想要的符号。
如果您可以使唯一名称看起来像真正的 c++ 名称空间,那么您应该能够通过将现有的 header 文件 #include 到名称空间定义中来重用它。
这里有一些全局和名称空间的错位名称:
U _Z9MyTestFn1Pv
U _Z9MyTestFn2Pv
U _ZN2N19MyTestFn1EPv
U _ZN2N19MyTestFn2EPv
U _ZN2N29MyTestFn1EPv
U _ZN2N29MyTestFn2EPv
您可以在这里拆解它们:https://demangler.com/
使用objcopy --redefine-sym old=new
或objcopy --redefine-syms=filename
进行重命名。可以使用 nm 和 sed 生成重命名。
通过一些额外的巧妙的宏工作,您甚至可以让它编写一个 c-style 函数 table 原型。不过,您仍然需要填充 table,但是通过拥有真正的原型,您不太可能会遇到使用 dlsym 太容易出现的粗指错误。
通过您的 header 阅读类似以下内容以允许指针声明:
int (PTR_MAYBE FirstExternalFn) ( int firstArg, ... ) ;
std::stringint (PTR_MAYBE SecondExternalFn) ( bool firstArg, ... ) ;
或者这可能太多了,但也允许跳转 table 为 auto-populated:
RETURNS(int) (PTR_MAYBE FirstExternalFn) ARGUMENTS ( int firstArg, ... ) ENDLINE
RETURNS(std::stringint) (PTR_MAYBE SecondExternalFn) ARGUMENTS ( bool firstArg, ... ) ENDLINE
第三种方法,旨在帮助填充虚拟 c++ class,要求您将每个参数装饰为 OneWordType (name)
形式,这对于模板来说相对容易,即 const char* name
变为DecorateArg<char>::const_ptr (name)
。然后,这允许您调用声明!当然,使用虚拟 class 的代价是代码携带了一个您永远不会使用的 this 指针,并且您正在进行额外的 shim 调用。
请注意,此文件被多次包含并且没有包含保护,但它不再真正用于直接使用。如果未定义 parent 包含,您可能会 #error。
您可以通过多种方式使用它:
//Declare the external methods
#define PTR_MAYBE
#define ARGUMENTS
#define ENDLINE ;
namespace FirstLib {
#include Header.hpp
}
// And the second library
namespace SecondLib {
#include Header.hpp
}
//define the indirection table declaration
#define PTR_MAYBE *
struct Indirection
{
#include Header.hpp
void* dummy;
};
namespace FirstLib
{
extern Indirection indirection;
}
namespace SecondLib
{
extern Indirection indirection;
}
// populate the table
#define ARGUMENTS(...)
#define ENDLINE ,
namespace FirstLib
{
Indirection indirection =
{
#include Header.hpp
nullptr
};
}
/* looks like:
FirstExternalFn,
SecondExternalFn,
nullptr
*/
namespace SecondLib
{
Indirection indirection =
{
#include Header.hpp
nullptr
};
}
这有点讨厌宏黑客,但如果您愿意自己构建 table,则不需要 ARGUMENTS 或 ENDLINE,这会让它更加理智。
我们现在可以访问 2 个间接 tables,FirstLib::indirection,SecondLib::indirection,您可以在其中或多或少地直接从主代码调用库方法,方法是分配一个指向您最喜欢的指针:
std::cout << currentIndirection->FirstExternalFn(nullptr);
这在本质上与 Is there an elegant way to avoid dlsym when using dlopen in C? 非常相似。基本上,您正在寻找 Windows 导入库的等价物,它将提供存根符号以满足静态链接器的要求,然后 dlopen
动态库在运行时具有真实的实现。
Linux 不支持开箱即用的导入库,因此人们通常手动或通过为特定项目量身定制的脚本来实现存根(例如 GLEW). I've recently developed Implib.so 以生成 POSIX 兼容自动导入库。模数错误,你应该可以使用它作为
$ implib-gen.py --dlopen-callback=mycallback
(mycallback
选择合适的库版本并 dlopen
)。
混淆 LD_PRELOAD 设置的一种方法可能是,当您的程序启动时,如果 LD_PRELOAD 设置不合适,请确定您想要的设置,添加它,然后立即使用您的 execv自己论据重启!
我认为 LD_PRELOAD 在这里更有意义,如果它满足您的需要,因为它更容易检测是否设置为您需要的方式。
TL;DR
我有一个我想在我的程序中使用的库,它有两个不同的版本。两个版本都提供相同的接口,但用于编译它们的选项不同。
我现在想使用特定版本的库,但是,由于两个版本都适用于不同的任务,并且用户应该定义要执行的任务,所以我需要在运行时决定使用哪个库。
我知道我可以使用 dlopen
和 dlsym
来根据用户的选择在运行时加载正确的库,但是接口非常大,并且将我需要的所有内容加载到不同的函数指针中会很乏味...
问题
我有一个有两个不同版本的库。两个版本都提供相同的界面,但它们适用的任务不同。 这是文件树的样子:
lib
\ - lib_task1
\ - libsharedobj.so
\ - lib_task2
\ - libsharedobj.so
我想为用户提供选择在运行时执行哪个任务的可能性。因此,我还需要在运行时决定选择哪个库。
我的想法是编写一个包装器,它提供与库相同的接口,并且我将 dlopen
所需的库和 dlsym
相应的符号放入函数指针中。然而,库接口非常大,按照描述包装它会非常乏味,再加上它是一个 C 接口,所以它还包含很多我不希望在包装器之外看到的原始指针。
这是一个小例子
// library interface
typedef struct {
// ...
} a_type;
void do_something(a_type* param);
// wrapper
class LibWrapper {
private:
void (*do_something)(a_type*);
void* lib;
public:
LibWrapper(const bool task_one) { // specified by the user
if (task_one) {
lib = dlopen("/usr/lib/lib_task1/libsharedobj.so", RTLD_NOW);
} else {
lib = dlopen("/usr/lib/lib_task2/libsharedobj.so", RTLD_NOW);
}
do_something = dlsym(lib, "do_something");
}
~LibWrapper() {
if (lib) {
dlclose(lib);
}
}
void do_something(std::unique_ptr<a_type> param) {
do_something(param.get());
}
};
问题
有没有更好的方法,或者我真的需要一个一个地加载每个符号吗?
OS:ubuntu 14.04。
兼容性:C++11
您可以通过在启动程序之前设置环境变量 LD_LIBRARY_PATH
来解决这个问题。如有必要,编写一个启动程序来执行此操作:
LD_LIBRARY_PATH=lib/lib_task1 ./myprog # or lib_task2
那么ld.so
会先在指定的目录下寻找libsharedobj.so
,不管链接的是哪个(即ldd myprog
显示的是哪个)
您可以考虑使用 objcopy 重命名库中的 public 符号吗?
[根据要求提供更多详细信息]
如果您将两个库中的所有竞争函数重命名为 non-competing 命名空间,那么您可以同时加载它们,并在运行时 select 加载您想要的符号。
如果您可以使唯一名称看起来像真正的 c++ 名称空间,那么您应该能够通过将现有的 header 文件 #include 到名称空间定义中来重用它。
这里有一些全局和名称空间的错位名称:
U _Z9MyTestFn1Pv
U _Z9MyTestFn2Pv
U _ZN2N19MyTestFn1EPv
U _ZN2N19MyTestFn2EPv
U _ZN2N29MyTestFn1EPv
U _ZN2N29MyTestFn2EPv
您可以在这里拆解它们:https://demangler.com/
使用objcopy --redefine-sym old=new
或objcopy --redefine-syms=filename
进行重命名。可以使用 nm 和 sed 生成重命名。
通过一些额外的巧妙的宏工作,您甚至可以让它编写一个 c-style 函数 table 原型。不过,您仍然需要填充 table,但是通过拥有真正的原型,您不太可能会遇到使用 dlsym 太容易出现的粗指错误。
通过您的 header 阅读类似以下内容以允许指针声明:
int (PTR_MAYBE FirstExternalFn) ( int firstArg, ... ) ;
std::stringint (PTR_MAYBE SecondExternalFn) ( bool firstArg, ... ) ;
或者这可能太多了,但也允许跳转 table 为 auto-populated:
RETURNS(int) (PTR_MAYBE FirstExternalFn) ARGUMENTS ( int firstArg, ... ) ENDLINE
RETURNS(std::stringint) (PTR_MAYBE SecondExternalFn) ARGUMENTS ( bool firstArg, ... ) ENDLINE
第三种方法,旨在帮助填充虚拟 c++ class,要求您将每个参数装饰为 OneWordType (name)
形式,这对于模板来说相对容易,即 const char* name
变为DecorateArg<char>::const_ptr (name)
。然后,这允许您调用声明!当然,使用虚拟 class 的代价是代码携带了一个您永远不会使用的 this 指针,并且您正在进行额外的 shim 调用。
请注意,此文件被多次包含并且没有包含保护,但它不再真正用于直接使用。如果未定义 parent 包含,您可能会 #error。
您可以通过多种方式使用它:
//Declare the external methods
#define PTR_MAYBE
#define ARGUMENTS
#define ENDLINE ;
namespace FirstLib {
#include Header.hpp
}
// And the second library
namespace SecondLib {
#include Header.hpp
}
//define the indirection table declaration
#define PTR_MAYBE *
struct Indirection
{
#include Header.hpp
void* dummy;
};
namespace FirstLib
{
extern Indirection indirection;
}
namespace SecondLib
{
extern Indirection indirection;
}
// populate the table
#define ARGUMENTS(...)
#define ENDLINE ,
namespace FirstLib
{
Indirection indirection =
{
#include Header.hpp
nullptr
};
}
/* looks like:
FirstExternalFn,
SecondExternalFn,
nullptr
*/
namespace SecondLib
{
Indirection indirection =
{
#include Header.hpp
nullptr
};
}
这有点讨厌宏黑客,但如果您愿意自己构建 table,则不需要 ARGUMENTS 或 ENDLINE,这会让它更加理智。
我们现在可以访问 2 个间接 tables,FirstLib::indirection,SecondLib::indirection,您可以在其中或多或少地直接从主代码调用库方法,方法是分配一个指向您最喜欢的指针:
std::cout << currentIndirection->FirstExternalFn(nullptr);
这在本质上与 Is there an elegant way to avoid dlsym when using dlopen in C? 非常相似。基本上,您正在寻找 Windows 导入库的等价物,它将提供存根符号以满足静态链接器的要求,然后 dlopen
动态库在运行时具有真实的实现。
Linux 不支持开箱即用的导入库,因此人们通常手动或通过为特定项目量身定制的脚本来实现存根(例如 GLEW). I've recently developed Implib.so 以生成 POSIX 兼容自动导入库。模数错误,你应该可以使用它作为
$ implib-gen.py --dlopen-callback=mycallback
(mycallback
选择合适的库版本并 dlopen
)。
混淆 LD_PRELOAD 设置的一种方法可能是,当您的程序启动时,如果 LD_PRELOAD 设置不合适,请确定您想要的设置,添加它,然后立即使用您的 execv自己论据重启! 我认为 LD_PRELOAD 在这里更有意义,如果它满足您的需要,因为它更容易检测是否设置为您需要的方式。