zoukankan      html  css  js  c++  java
  • Linux下实现C++类的动态链接

    1. 背景

    在java中,jvm支持类的动态链接(Class.forName(String className)),用起来也很方便。动态链接是实现IOC(Inversion of Control,控制反转,更形象的称作依赖注入)的关键,用于将类间依赖从程序移到配置文件中。在框架不重新编译的情况下,替换被依赖的类。

    在linux下,C++只能通过C的dl API实现动态链接,需要先将动态链接库编译成.so,然后再调用API进行链接。下面看一下如何实现动态链接C++类。和java实现IOC一样,链接类需要依赖于接口,先定义一下框架需要依赖的接口部分:

    class UserInterface {
    public:
        virtual bool func() = 0;
    };

    动态链接库实现部分继承这个接口,进行实现。

    class ConcretInterface: public UserInterface {
    public:
        virtual bool func() {
            printf("A implementition of UserInterface");
        }
    };

    2. dl API

    先介绍dl API部分,这些函数需要连接dl动态库,-ldl:

    void *dlopen(const char *filename, int flag);

    用于打开.so,并进行链接,返回链接后的handle。flag表示链接标志:

    RTLD_LAZY:延迟加载,在.so中第一次使用某个符号是再进行链接。只对函数符号有效,变量符号会被立即加载。

    RTLD_NOW:在dlopen返回后,所有未定义的符号都需要进行链接,如果符号解析失败,dlopen会返回失败。

    RTLD_GLOBAL:使用这个标志,则这个库中的符号可以被随后的动态库使用。

    RTLD_LOCAL:与RTLD_GLOBAL相反,不可以以被随后动态库使用。

    char *dlerror(void);

    返回dl相关错误,如果从初始化或者上一次调用到目前为止没有错误则返回NULL。

    void *dlsym(void *handle, const char *symbol);

    返回符号名为symbol的符号的地址,需要用到dlopen返回的动态链接库的handle。需要使用dlerror来检验dlsym是否出错,不能进通过返回的指针是否为NULL判断是否出错,因为符号地址为NULL属于正常情况。

    int dlclose(void *handle);

    减少.so的引用计数,如果引用计数为0,则.so会被卸载。

    3. 类加载实现

    通过上面dl API可知,只能返回动态链接库中的符号的地址,这个符号函数符号或者是变量符号,不能像java一样直接返回一个类。而我们在动态链接类时,实际上只是需要这个类的实例对象,所以可以通过返回创建类对象的函数符号实现类的连接。由于在C++中,可以对new操作符重载,进行其他的一些操作,分配资源等,此时调用delete释放对象,会造成资源泄露。所以为了支持重载new操作符,还需要释放对象的函数。定义如下:

    typedef bool (*user_interface_creator_t)(UserInterface **ui);
    typedef void (*user_interface_destroyer_t)(UserInterface *ui);

    上面定义了两个函数指针,分别是链接类的创建器和销毁器。动态链接库需要实现这两个函数,从而是框架可以创建和销毁对象。如果默认不支持new操作重载,可以去掉destroyer,简化接口。

    还有一点比较重要,由于C++支持了命名空间、类以及重载等机制,所以符号的命名规则和C不同。而dl API属于C的部分,使用的C的命名规则,所以在实现上述两个函数时(也就是说需要通过动态链接的C++函数)需要使用C的符号命名规则。这需要实现extern 'C'实现,具体如下:

    extern 'C' {
    
    bool my_creator(UserInterface **ui)
    {
        if (NULL == ui) {
            printf("invalid param");
            return false;
        }
    
        *ui = new (std::nothrow) ConcrectInterface;
        return true;
    }
    
    void my_destroyer(UserInterface *ui)
    {
        if (ui != NULL) {
            delete ui;
        }
    }
    
    };

    my_creator和my_destroyer就是动态链接库对接口的实现,可以通过函数名调用dlsym找到对应的地址,完成对象的创建和销毁。下面,介绍一下框架,也就是动态链接的部分。

    #include <dlfcn.h>
    #include <stdio.h>
    
    #include "user_interface.hpp"
    
    const char *so_path = "./user_impl.so";
    const char *creat_func = "my_creator";
    const char *destroy_func = "my_destroyer";
    
    int main ()
    {
        // load .so
        void *handle = dlopen(so_path, RTLD_LAZY);
        if (NULL == handle) {
            printf("failed to open %s, error: %s", so_path,
                    dlerror());
            return 1;
        }   
    
        // reset errors
        dlerror();
    
        user_interface_creator_t creator = 
            (user_interface_creator_t)dlsym(handle, creat_func);
        const char* err = dlerror();
        if (err != NULL) {
            printf("failed to load creator, error: %s", err);
            return 1;
        }   
    
        // reset errors
        dlerror();
    
        user_interface_destroyer_t destroyer = 
            (user_interface_destroyer_t)dlsym(handle, destroy_func);
        err = dlerror();
        if (err != NULL) {
            printf("failed to load destroyer, error: %s", err);
            return 1;
        }   
    
        UserInterface *ui;
        if (!(*creator)(&ui)) {
            printf("failed to creat ui");
            return 1;
        }   
        ui->func();
    
        (*destroyer)(ui);
    
        dlclose(handle);
    }

    这里需要注意每次调用dl API函数后需要调用dlerror清除error状态。

    4. 坑

    如果动态链接库和可执行程序依赖同一个库,比如依赖同一个日志库,在编译动态链接库和可执行程序时需要注意一下。要确保对于库的符号引用的解析会指向同一个符号。具体做法:

    (1)编译.so时,不链接依赖的库,对于库的符号的引用都是undefined。
    (2)可执行程序需要链接依赖的库,并且要指定链接选项-rdynamic,将全局符号可以用于动态链接库的符号引用的解析。

    如果没有做到以上两点,会出现同一个符号引用会被解析到两个符号,若符号依赖的是一个变量的符号(比如符号本身就是一个变量,或者是函数符号,但依赖于某个变量),那么就会出现个别古怪、异常的现象。

    对于.so依赖一个库,但是可执行程序没有依赖,那么编译.so时必须把这个库链接,否则会出现undefined symbol。

    再介绍下调试动态链接库时用到的工具,因为经常会出现各种未定义的符号,所以需要查看.so或者可执行程序的符号信息,可以使用nm,比如,类型U或者类型T的符号。还可以使用c++filt将符号名解析成可读的字符串。



  • 相关阅读:
    Delphi 10.4.2使用传统代码提示方案(auto complete)(转)
    Sqlserver 清除维护相关日志
    postgresql 时间戳自动更新
    sqlserver 修改电脑名或是 修复数据引擎
    postgresql uuid(guid)生成函数及使用
    List.toArray使用方法
    HashMap 1.7与1.8区别
    设计模式之观察者模式实现(JAVA)
    ubuntun下安装rabbitMq
    Java中replace与replaceAll区别
  • 原文地址:https://www.cnblogs.com/james1207/p/3265427.html
Copyright © 2011-2022 走看看