起因源自"dlfcn.h",很多没用过linux的朋友可能会文这个头文件是来干什么的??实际windows下也有类似的东西,而且我纠结的东西在windows也是一样的.当你去google一下"动态链接库的使用和加载",满眼望去都是"windows.h, HMODULE, LoadLibrary, GetProcAddress, FreeLibrary"无外乎这一个头文件,一个类,加三个函数.千篇一律,基本都是引用来引用去的.在linux下也有类似的东西就是这个"dlfcn.h"和它其下的各种函数, 那么今天就拿这个东东来说事.
就因为网上那些千篇一律的抄袭, 我这边纠结了好几天, 有以下几个问题一直困扰我:
1. 总会看见extern "c"这个是不是必要的? 以前写程序就没怎么用过这个东西, 为啥我的动态链接库一样能用.
2. windows下我即没用到"LoadLibrary, GetProcAddress, FreeLibrary"这几个函数怎么一样用了N多的dll文件?
3. linux应该也有类似的东西吧? <<UNIX环境高级编程>>那么经典的一本书也没有讲到这个问题.
4. 动态链接库怎么加载, 什么时候用, 要那么多动态的干吗都用静态的多干净呀?
大家尽情BS我吧, 问了这么小白的问题, 还有那么蛋疼的问题知道了有意义吗!
还是听人家把话说完吧, 也许你真的不知道, 至少我这个学了几年编程的研究生还疑惑着......
动态链接库的显式与隐式调用,也叫动态与静态调用:
我也是查了N多网站才发现有这个说法, 可能你早就会用但不一定完全懂, 还有就是明明是动态库怎么还有个静态调用, 是不是我搞错了.
1.显示调用(动态调用):
windows下不用说了, 就是刚才提到的那些东东. LoadLibray之流......
linux下简单说一下, 实际严谨一点应该是UNIX下:
1 #include <iostream>
2 #include <dlfcn.h>
3
4 int main() {
5 using std::cout;
6 using std::cerr;
7
8 cout <<"C++ dlopen demo\n\n";
9
10
11 cout <<"Opening hello.so...\n";
12 void* handle = dlopen("./hello.so", RTLD_LAZY);
13
14 if (!handle) {
15 cerr << "Cannot open library: " << dlerror() <<'\n';
16 return 1;
17 }
18
19
20 cout << "Loading symbol hello...\n";
21 typedef void (*hello_t)();
22
23
24 dlerror();
25 hello_t hello = (hello_t) dlsym(handle, "hello");
26 const char *dlsym_error = dlerror();
27 if (dlsym_error) {
28 cerr <<"Cannot load symbol 'hello': " << dlsym_error <<'\n';
29 dlclose(handle);
30 return 1;
31 }
32
33
34 cout <<"Calling hello...\n";
35 hello();
36
37
38 cout <<"Closing library...\n";
39 dlclose(handle);
40 }
1 #include <iostream>
2
3 extern "C" void hello() {
4 std::cout <<"hello" <<'\n';
5 }
这是一段动态链接库的调用代码,和windows下的大同小异,不外乎换了几个名字.仔细观察不难发现无论是windows下还是linux都是根据名字来调用的.没有参数,只是指定了返回类型, 但是C++是允许重载的, 这样的话就非常难办了.
撤远一点, 简单说明一下C++的重载机制, 为了实现重载, C++在编译的时候给函数加了一点东西来区分不同参数但是同名的函数. 例如foo(int), 编译器在编译之后可能会变成foo_int(int)来处理(编译器具体的实现可不是这样命名,只是打个比方), 这样编译器就实现了重载机制. 但是无论是dlopen API, 还是windows下的LoadLibrary API都是通过函数名的符号链接来从动态链接库中找到指定函数的.所以extern "C"关键字由此而生, 它只是告诉C++编译器按照C的方法去编译,这样就不会出现重命名问题, 当然被extern "C" 限制的函数也无法重载了.
当然类也是C++的特性之一,下面把类的调用也加进去:
1 #include "polygon.hpp"
2 #include <iostream>
3 #include <dlfcn.h>
4
5 int main() {
6 using std::cout;
7 using std::cerr;
8
9
10 void* triangle = dlopen("./triangle.so", RTLD_LAZY);
11 if (!triangle) {
12 cerr <<"Cannot load library: " <<dlerror() <<'\n';
13 return 1;
14 }
15
16
17 dlerror();
18
19
20 create_t* create_triangle = (create_t*) dlsym(triangle, "create");
21 const char* dlsym_error = dlerror();
22 if (dlsym_error) {
23 cerr <<"Cannot load symbol create: " <<dlsym_error <<'\n';
24 return 1;
25 }
26
27 destroy_t* destroy_triangle = (destroy_t*) dlsym(triangle, "destroy");
28 dlsym_error = dlerror();
29 if (dlsym_error) {
30 cerr << "Cannot load symbol destroy: " <<dlsym_error << '\n';
31 return 1;
32 }
33
34 polygon* poly = create_triangle();
35
36 poly->set_side_length(7);
37 cout <<"The area is: " <<poly->area() <<'\n';
38
39
40 destroy_triangle(poly);
41
42
43 dlclose(triangle);
44 }
1 #ifndef POLYGON_HPP
2 #define POLYGON_HPP
3
4 class polygon {
5 protected:
6 double side_length_;
7
8 public:
9 polygon()
10 : side_length_(0) {}
11
12 virtual ~polygon() {}
13
14 void set_side_length(double side_length) {
15 side_length_ = side_length;
16 }
17
18 virtual double area() const = 0;
19 };
20
21
22 typedef polygon* create_t();
23 typedef void destroy_t(polygon*);
24
25 #endif
1 #include "polygon.hpp"
2 #include <cmath>
3
4 class triangle : public polygon {
5 public:
6 virtual double area() const {
7 return side_length_ * side_length_ * sqrt(3) / 2;
8 }
9 };
10
11
12
13 extern "C" polygon* create() {
14 return new triangle;
15 }
16
17 extern "C" void destroy(polygon* p) {
18 delete p;
19 }
2.隐式调用(静态调用):
关于隐式调用的介绍少, 但是却反尔更加常用, 也可能是因为这种调用方法比较简单没有介绍的必要吧. 隐式调用实际更像是静态库的调用, 在gnu编译器中, 必须要有声明的头文件, 然后就是-l参数加上你的动态链接库, 只不过运行的时候动态库也是要放在当前目录或者是环境变量指定目录下的.
3.隐式调用 VS 静态链接库:
既然隐式调用和静态库如此相似为什么不干脆用静态库, 还弄个隐式调用, 然后发行的时候还要打包各种动态库如此的麻烦?
拿一个最典型的动态链接库msvcp60.dll来说事, 有人问这个是什么东东? 这个就是大名鼎鼎的vc6.0运行库, 没有它你用vc6编译器出来的程序是跑不起来的, 只不过现在的操作系统在安装的时候已经帮你打包了, 一半都放在system32目录下. 基本上用到的80%以上的windows应用程序都会用到这个动态库, 要是都把它改成静态编译的, 已经见底的硬盘和内存又不知道要多出多少份拷贝来(动态库在运行时,内存中只有一份拷贝, 如果把动态库放到环境变量之下比如/windows/system32,那么其硬盘拷贝也只是一份).
静态库又好在哪呢?静态库第一它不需要带上那么多繁杂的动态链接库(各种dll),而且静态库也可以选择性的把内容编译进可执行程序之中.
比如Qt程序在windows的打包:一半装好之后, 如果Qt程序想要放到其它没有安装Qt环境的windows上去运行的话, 那么需要根据模块, 打包Qt运行库, 最基本的动态链接库要几百M, 为了一个3M的程序再打包几百M的运行环境实在是......但是如果是静态编译呢, 一个小程序大概10M左右, 这样还不用去安装Qt运行环境.
4.隐式调用 VS 显式调用:
隐式调用使用方便简单, 但其和静态库相似, 在编译期就和程序绑定了,灵活性差, 并且其生存期和进程一样, 进程开始, 调用开始, 进程结束, 动态库才卸载. 另外还需要将整个动态库全部加进内存.
显式调用使用起来比较复杂, 但是却可以在运行期间选择所需要调用的动态链接库, 并且可以控制动态库生存期, 需要加载时候再加载, 用完了就可以卸掉, 而且不用将整个动态库都放进内存, 只要加载要用到的函数即可.
结合:
看gdal库的源代码发现了一个比较有趣的技巧, 就是关于隐式与显式调用的技巧. gdal做了一个封装, 它实际用的方法是显式调用, 但是显式调用用起来比较复杂, 所以这套库编译结束之后, 会生成一套库即有动态库又有静态库, 但实际在编程序时两者都会用到, 而不是两种打包形式. 它在静态库中对dlopen API封装, 然后用了设计模式的工厂模式, 来选择调用动态库的函数, 及加载时间. 但是gdal的API使用者却不需要考虑这么多, 只要在编译的时候加上-lgdal参数, 但同时运行是也要将gdal的动态库带上即可, 这就是面向对象所谓封装的结果.
from:http://xuwenzhang.org/blog/2010/10/13/01-18/#comments