根据用户配置在运行时链接共享对象

时间:2017-11-27 09:57:38

标签: c++ linux c++11 shared-libraries

TL; DR

我有一个我想在我的程序中使用的库,它有两个不同的版本。两个版本都提供相同的接口,但用于编译它们的选项不同。

我现在想要使用该库的特定版本,因为两个版本都适用于不同的任务,用户应该定义要执行的任务,我需要决定在运行时使用哪个库。

我知道我可以根据用户的选择使用dlopendlsym在运行时加载正确的库,但是界面非常大,并且将我需要的所有内容加载到不同的函数指针中很乏味...

问题

我有一个库,它有两个不同的版本。两个版本都提供相同的界面,但它们适合的任务不同。 这就是文件树的样子:

lib
 \ - lib_task1
      \ - libsharedobj.so
 \ - lib_task2
      \ - libsharedobj.so

我希望用户可以选择在运行时执行哪个任务。因此,我需要决定在运行时选择哪个库。 我的想法是编写一个包装器,它提供与库相同的接口,我将dlopen所需的lib和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());
    }
};

问题

有没有更好的方法,或者我真的需要逐个加载每个符号?

操作系统:ubuntu 14.04。
兼容性:C ++ 11

4 个答案:

答案 0 :(得分:0)

您可以在启动程序之前设置环境变量LD_LIBRARY_PATH来解决此问题。如有必要,编写一个启动程序,它就是这样做的:

LD_LIBRARY_PATH=lib/lib_task1 ./myprog # or lib_task2

然后ld.so将首先在指定目录中查找libsharedobj.so,无论哪个链接(即ldd myprog显示哪一个)。

答案 1 :(得分:0)

您可以使用objcopy重命名库中的公共符号吗?

[按要求提供更多详情]

如果将两个库中的所有竞争函数重命名为非竞争命名空间,则可以同时加载它们,并在运行时选择所需的符号。

如果您可以使唯一名称看起来像真正的c ++名称空间,那么您应该能够通过#including在名称空间定义中重用现有的头文件。

这里有一些全局和名称空间损坏的名称:

             U _Z9MyTestFn1Pv
             U _Z9MyTestFn2Pv
             U _ZN2N19MyTestFn1EPv
             U _ZN2N19MyTestFn2EPv
             U _ZN2N29MyTestFn1EPv
             U _ZN2N29MyTestFn2EPv

您可以在此处对其进行解码:https://demangler.com/

使用objcopy --redefine-sym old=newobjcopy --redefine-syms=filename进行重命名。可以使用nm和sed生成重命名。

通过一些额外的聪明的宏工作,你甚至可以编写一个c风格的函数表原型。尽管如此,你仍然需要填充该表,但是通过拥有真正的原型,你不太可能遭受dlsym太容易出现的胖手指错误。

使用您的标题读取以下内容以允许指针声明:

int (PTR_MAYBE FirstExternalFn) ( int firstArg,  ...  ) ;
std::stringint (PTR_MAYBE SecondExternalFn) ( bool firstArg,  ...  ) ;

或者这可能太多了,但也可以自动填充跳转表:

RETURNS(int) (PTR_MAYBE FirstExternalFn) ARGUMENTS ( int firstArg,  ...  ) ENDLINE
RETURNS(std::stringint) (PTR_MAYBE SecondExternalFn) ARGUMENTS ( bool firstArg,  ...  ) ENDLINE

第三种方法,旨在帮助填充一个shim虚拟c ++类,要求你装饰OneWordType (name)形式的每个参数,这对模板来说相对容易,const char* name变为DecorateArg<char>::const_ptr (name)。然后,您可以调用声明!当然,使用虚拟类的代价是代码带有一个你永远不会使用的指针,你正在进行额外的垫片调用。

请注意,此文件包含多次,并且没有包含防护,但它并不是真正用于直接使用。如果未定义父包含,则可以#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
    };
}

这是一个令人讨厌的宏hackery,但如果你愿意自己构建表,你不需要ARGUMENTS或ENDLINE,这使它更加理智。

我们现在可以访问2个间接表,FirstLib :: indirection,SecondLib :: indirection,您可以通过指定收藏夹的指针,或多或少地直接从主代码调用库方法:

std::cout << currentIndirection->FirstExternalFn(nullptr);

答案 2 :(得分:0)

这与Is there an elegant way to avoid dlsym when using dlopen in C?的精神非常相似。基本上,您正在寻找一个等效的Windows导入库,它将提供存根符号以满足静态链接器,然后在运行时提供真实实现的dlopen动态库。

Linux上不支持导入库,因此人们通常手动或通过为特定项目定制的脚本(例如GLEW)来实现存根。我最近开发了Implib.so来自动生成与POSIX兼容的导入库。 Modulo错误,您应该可以将其用作

$ implib-gen.py --dlopen-callback=mycallback

mycallback选择适当的库版本,dlopen选择它。

答案 3 :(得分:0)

模糊LD_PRELOAD设置的一种方法可能是,当你的程序启动时,如果未正确设置LD_PRELOAD,确定你想要的设置,添加它,并立即用你自己的参数execv重启! 我认为LD_PRELOAD在这里更有意义,如果它能满足您的需求,因为它更容易检测它是否按您所需的方式设置。