从一个logger引发的lib和dll探讨

一. 问题来由

项目写了个logger,本来是代码的,大家单独包含都可以使用,但是后来项目整合,每个人的部分打成lib,而前端将logger打包一起编译成lib,后台按道理应该是不用包含 .cpp 文件也可以用到logger,因为如果后台也包含并编译,应该会和前端的编译单元中的 logger 重定义。

但是奇怪的是,非但不重定义,反而这边获取不到 io 流 (流是静态变量),匪夷所思,最后是采用命名空间解决了。

虽然那个问题没有完美解决,但是我决定好好搞一下 lib 和 dll ,因为仅就 logger 来说:

好处1 这个项目中,日志模块是完全独立的模块,从设计的角度来看,本来就应该独立开来打包,然后大家就可以一起用这个包,实现了松耦合;

好处2 logger 打成 lib 或者 dll,大家就可以像使用一些api一样松松使用,不仅用于这个项目,以后实验室的项目都可以使用。

另外,也可以彻底搞一下动态库和静态库。

二. 从最简单的定义说起

最简单的定义,也是最具有可信性的:

lib 和 dll

一个 lib 文件是obj文件的集合。

当然,其中还夹杂着其他一些辅助信息,目的是为了让编译器能够准确找到对应的 obj 文件。我们可以通过tlib.exe(在tc2.0下的根目录)来对lib文件进行操作

dll 是运行时装载的函数实现代码,个人认为是函数实现代码段汇编对应的二进制码。

静态编译与动态编译

注: 本人对于静态编译和动态编译的概念几经查证,尚无定论,先留白在此,而后查证后更新。

如下解释只是个人理解。

动态编译: 可执行文件需要附带一个的动态链接库,动态链接库在运行时加载

静态编译: 将附加的静态链接库链接进最后的可执行文件,是编译期所做的事情

三. 静态链接库版本 logger

本人编译环境为: win7 vs2013,静态链接库为lib

1 生成 lib

我希望将 logger 打成一个 lib,供外部链接调用。理清包含关系如下:

(1) logger.h 包含 mutex.h 和一些其他头文件

(2) mutex.h 包含 pthread.h,并在代码中加入#pragma comment(lib, "pthreadVC2.lib"),用到第三方的函数

即: logger.h —> mutex.h —> pthread.h

而 pthread 提供 pthread.h + pthread 相关 lib + pthread 相关 dll

这里写图片描述

正片开始:

我发现,如果选择生成 lib,编译选项里面并不能添加 “附加依赖项”,所以我大胆猜测 代码中编译制导语句链接 pthread lib 那句话在生成 lib 时没有用。

这里写图片描述

注释编译制导语句,生成 lib 成功~~

这里写图片描述

这也就是说,生成 lib 的时候,并不会进行链接,自然就不会进行重定义和外部定义的检查,只会进行将当前项目的编译单元生成中间文件 (这里是 .obj 文件) 打包,即使在代码中指明需要链接外部 lib。

即 lib 不能链接外部 lib~~

不仅是静态 lib 不允许链接外部 lib,看起来动态 lib 好像可以链接,其实也不是,动态 lib 中只是 dll 暴露接口的函数入口地址,并不包含任何函数实现。

个人认为编译器这么做的目的,是为了防止很多 lib 重复定义同一个外部 lib。而且生成 lib 的确不应该做链接,因为生成一个 lib 的最终职能是为了让包含它的可执行程序跑起来,而仅仅生成 lib 时,都不完整,立马就链接是没有意义的。

2 外部使用 lib

需包含上一步生成的 lib,使用#pragma comment(lib, "logger.lib")完成

#include "logger.h"

#pragma comment(lib, "logger.lib")

int main(int argc, char *argv[])
{
    initLogger("whInfolog.txt", "whWarnlog.txt", "whErrolog.txt", false);//初始化日志文件

    LOG2(_INFO,"first log");

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

编译器配置如下:

这里写图片描述

特别需要注意的是:

因为上一步生成 lib 时,没有包含 pthread 的 lib 文件,在最后生成可执行文件的时候,必须:

(1) 包含 lib 所在文件夹为库文件夹

(2) 在包含的用到 pthread lib 文件的头文件处 (这里是 mutex.h),使用#pragma comment(lib, "pthreadVC2.lib") 或项目属性中设置附加依赖项

(3) pthread dll 放到可执行程序目录,或者放到环境变量中

然后就可以正常使用了。

四. 动态库链接库版本 logger

因为是 win 平台,所以生成动态库是 dll

1 生成 dll

dll 是实现的二进制码,运行时装载,自然在项目属性中有了 “附加依赖项” 的选项,这意味着生成 dll 时,是需要链接的,dll 中包含外部 lib 的实现。

这里写图片描述

于是这里需要包含,pthread lib 目录,并使用#pragma comment(lib, "pthreadVC2.lib") 或项目属性中设置附加依赖项

另外由于 dll 的特殊性,dll 需要暴露在黑盒外的函数接口有特殊写法,详见代码:

/*
*
* function: 日志 dll API loggerAPI.h
*
* Date:2016-04-28
*
*    Author: Bill Wang
*/

#ifndef _LOGGER_API_H_
#define _LOGGER_API_H_

#include "logger.h"

#define WH_DLL_EXPORTS
#ifdef WH_DLL_EXPORTS
#define WH_DLL_API __declspec(dllexport) //导出
#else
#define WH_DLL_API __declspec(dllimport) //导入
#endif

extern "C"
{

    WH_DLL_API void api_InitLogger(const std::string& info_log_filename,
        const std::string& warn_log_filename,
        const std::string& error_log_filename,
        bool isAppend = true);

    WH_DLL_API std::ostream& api_WriteLogger(log_rank_t rank, const std::string reason);

    WH_DLL_API void echoTest(); //echo测试
}

#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
/*
*
* function: 日志 dll API loggerAPI.cpp
*
* Date:2016-04-28
*
*    Author: Bill Wang
*/

#include "loggerAPI.h"
#include "logger.h"
#include <iostream>

using namespace std;

void api_InitLogger(const std::string& info_log_filename,
    const std::string& warn_log_filename,
    const std::string& error_log_filename,
    bool isAppend) {

    initLogger(info_log_filename, warn_log_filename, error_log_filename, isAppend);//初始化日志文件
}

std::ostream& api_WriteLogger(log_rank_t rank, const std::string reason) {

    return LOG2(rank, reason);
}

//echo测试
void echoTest() {
    cout << "Echo Success!!" << endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

用 depends.exe 查看结果如下,可以清晰的看到,dll 是给出暴露接口的函数入口地址

这里写图片描述

生成以后,会在输出目录下生成一个 dll 和一个 lib,这个lib 应该就是所谓的动态 lib,是对 dll 文件的索引。

这里写图片描述

动态库生成的那个lib一般管它叫“导入库”,这样的lib在大多数情况下要比静态库的lib小,里面不包含涉及到的函数的具体代码,里面只包含“这个函数在什么dll里面叫什么名字”这样的信息。

2 外部使用 dll

//#include "loggerAPI.h"
#include<Windows.h>
#include<iostream>
#include<string>

using namespace std;

typedef enum log_rank {
    _INFO,
    _WARNING,
    _ERROR,
    _FATAL
}log_rank_t;

typedef void (*API_INITLOGGER)(const std::string& info_log_filename,
    const std::string& warn_log_filename,
    const std::string& error_log_filename,
    bool isAppend);

typedef std::ostream& (*API_WRITELOGGER)(log_rank_t rank, const std::string reason);
typedef void (*API_ECHOTEST)();

int main(int argc, char *argv[])
{
    HMODULE hm = ::LoadLibrary(TEXT("logger.dll"));
    //如果Dll加载失败,释放它占用的资源
    if (hm == NULL) {
        cout << "hm is null" << endl;
        ::FreeLibrary(hm);
        return 0;
    }

    API_ECHOTEST main_EchoTest = (API_ECHOTEST)::GetProcAddress(hm, "echoTest");
    if (main_EchoTest == NULL) {
        cout << "main_EchoTest is null" << endl;
        ::FreeLibrary(hm);
    }
    main_EchoTest();

    API_INITLOGGER main_API_InitLogger = (API_INITLOGGER)::GetProcAddress(hm, "api_InitLogger");
    main_API_InitLogger("whInfolog.txt", "whWarnlog.txt", "whErrolog.txt", false);//初始化日志文件

    API_WRITELOGGER main_API_WriteLogger = (API_WRITELOGGER)::GetProcAddress(hm, "api_WriteLogger");
    main_API_WriteLogger(_INFO, "first log");

    ::FreeLibrary(hm);

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

如果细心可以发现,我上面代码中,将 #include "loggerAPI.h" 注释掉了,而改成了包含很多其他的头文件和枚举量申明,原因就是,使用 dll 其实没有必要包含实现 dll 的全部头文件,只要函数指针类型正确,所有变量类型无误,即可获得函数地址,读者可自行试验。

dll 的两边只要函数签名完全一致,即可

这里写图片描述

所以这边既不用包含 pthread.h,也不用包含 pthread lib库,不过 pthread dll 还是需要的,因为 logger.dll 中链接的 pthread lib 库,依赖 pthread dll

这里写图片描述

如果是动态 lib + dll 的组合,不用动态解析 dll,直接使用 头文件中 extern C 的函数即可

五. 驳斥

今天在看一篇博文的时候,发现其中很大问题,关键是这篇文章还高居搜索排行榜首

http://blog.csdn.net/wuan584974722/article/details/7953213

问题1: 动态lib相当于一个h文件,是对实现部分(.dll文件)的导出部分的声明

相当于一个h文件,已经有头文件了,何须再有头文件。另外是对 dll 的声明,声明已在头文件中,这种说法就不对了,应该是保存了函数入口地址,相当与解析了dll~~

这里就再延伸一下,动态 lib 和直接使用 dll 有什么优缺点呢?

动态 lib: 省去使用者需要解析 dll 的麻烦,直接使用 lib,出现问题可以在编译器查出来。不过如果 dll 中函数原形改变,当然函数入口地址就改变了,这样使用 lib 就重新编译。

直接解析 dll: 如果函数原型改变,也需要重新写函数指针,重编译。

问题2: 静态编译就是编译器在编译可执行文件的时候,将可执行文件需要调用的对应动态链接库(.so)中的部分提取出来

链接到可执行文件中去,使可执行文件在运行的时候不依赖于动态链接库。

网上看到过有的博客可以脱离第三方 dll,不过都是 MFC, 
http://www.cnblogs.com/jifeng/archive/2011/06/24/2088872.html,这篇博客里面提到:

1.项目 -> 配置属性->常规->MFC的使用 :在静态库中使用MFC 
2.项目 -> 配置属性->C/C++->代码生成->运行库 :选择/MT

减少对 dll 依赖,应该是使用了 MFC 库,可能 MFC 库有特殊的方法(比如编译器偷偷链接到了相应静态库),完成了这个工作,因为 dll 是运行时动态加载到内存的,而想编译时抽取这是不可能的事

想想也是,如果 dll 可以做到抽取,exe 也必然可以了,那怎么可能,不然 exe 打包还如何保护代码权益

证据有:https://stackoverflow.com/questions/725472/static-link-of-shared-library-function-in-gcc

这里写图片描述

第三方解决方案可以做到,但是也不是所谓的编译期抽取

http://bbs.csdn.net/topics/300205804

另附vczh群聊天记录:

这里写图片描述

这里写图片描述

所以不得不又一次说,那篇博客的博主是一本正经的胡说了~~

小结

网上找了一些资料,越来越觉得这些底层的知识,需要看专业书籍,正因为网上的资料良莠不齐,我们甄选的时候更需要带批判的眼光看问题

附录

源代码地址:https://github.com/billhhh/logger


参考资料

[1] http://blog.csdn.net/lushuner/article/details/25048465

[2] http://bbs.csdn.net/topics/390177392

[3] https://www.cppfans.org/1394.html


转载自:http://blog.csdn.net/Scythe666/article/details/51278638

猜你喜欢

转载自blog.csdn.net/brunomarss/article/details/53502631