前面我们通过容器接口抽象了双向链表和动态数组,这样队列的实现就不依赖于具体的容器了。但是作为队列的使用者,它仍然要在编译时决定使用哪个容器。队列的测试程序就是队列的使用者之一,它的实现代码如下:
Queue* queue = queue_create(linear_container_dlist_create(NULL, NULL));
for(i = 0; i < n; i++)
{
assert(queue_push(queue, (void*)i) == RET_OK);
assert(queue_head(queue, (void**)&ret_data) == RET_OK);
assert(queue_length(queue) == (i+1));
}
...
这里必须明确指定是 linear_container_dlist_create还是 linear_container_darray_create,假设使用者想要换一种容器,那还是要修改代码并重新编译才行。现在我们思考另外一个问 题,如何让使用者(如这里的测试程序)想换一种容器时,不需要修改代码也不需要重新编译。
在继续学习之前,我们先介绍几个概念:
静态库:在Linux下,静态库的扩展名为.a,a代表archive的意思。正常情况下一个C源文件编译之后生成一个目标文件(.o),目标文件 里存放的是程序的机器指令和数据,它不包含运行时的信息,所以不能直接运行。用命令ar把多个目标文件打包成一个文件就生成了所谓的静态库。可执行文件链 接静态库时,会把用到的函数和数据拷贝过去。多个可执行文件链接同一个静态库时,所用到的函数和数据就会拷贝多次,那就存在不少空间浪费。
共享库:顾名思义,共享库可以在多个可执行文件之间共享,链接时不用拷贝函数或数据,只是建立一个函数链接表(PLT),在运行时通过这个表来确定 具体调用的函数。共享库可以有效的避免空间浪费,但它也不是免费的午餐,它在加载时存在额外的开销,在链接多个共享库时会比较明显。不过目前出现的 prelink和gnu hash等技术,有效的缓解了这个问题。共享库另外一个好处就是可以单独升级,理论上,修改某个共享库不需要重新编译依赖它的应用程序(不过实际操作时与 它的接口变化和信息隐藏程度有关)。
可执行文件:Linux下加x属性的文件都是可执行文件,这里可执行文件特指ELF(Executable and Linking Format)格式的可执行文件。 可执行文件在编译时连接静态库,会把所用到的函数会被拷贝过来,运行时不再依赖于静态库。在编译时链接共享库,它不拷贝函数和数据,但在运行时会依赖于其 链接的共享库。
回到前面的问题:我们发现在编译时,无论是链接静态库还是共享库,它都绑定了调用者与使用者之间的关系。真正要在运行时决定而不是编译时决定使用哪种容器,那就不能包含具体容器的头文件,链接具体容器所在的库了。今天我们要学习一种新的技术:在运行时动态加载共享库。
除了一些嵌入式环境下使用的实时操作系统(RTOS)外,现代操作系统都支持在运行时动态加载共享库的机制。Linux下有dlopen系列函数,Windows下有LoadLibrary系列函数,其它平台也有类似的函数。下面是使用dlopen的简单示例:
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int main(int argc, char **argv)
{
void *handle = NULL;
double (*cosine)(double) = NULL;
/*加载共享库*/
handle = dlopen("libm.so", RTLD_LAZY);
/*通过函数名找到函数指针*/
*(void **) (&cosine) = dlsym(handle, "cos");
/*调用函数*/
printf("%f/n", (*cosine)(2.0));
/*卸载共享库*/
dlclose(handle);
return 0;
}
由于这些函数在每个平台的名称和参数都有所不同,直接使用这些函数会带来可移植性的问题,为此有必要对它们进行包装。不同平台有不同的函数,也就是 说存在多种不同的实现,那这是否意味着要用接口呢?答案是不用。原因是同一个平台只一种实现,而且不会出现潜在的变化。我们要做的只是加一个适配层,用它 来隔离不同平台就好了。
我们把这个对加载函数的适配层称为Module,声明(module.h)如下:
struct _Module;
typedef struct _Module Module;
typedef enum _ModuleFlags
{
MODULE_FLAGS_NONE,
MODULE_FLAGS_DELAY = 1
}ModuleFlags;
Module* module_create(const char* file_name, ModuleFlags flags);
void* module_sym(Module* thiz, const char* func_name);
void module_destroy(Module* thiz);
这里它们的实现只是对dl系列函数做个简单包装。由于不同平台有不同的实现,为了维护方便,我们把不同的实现放在不同的文件中,比如Linux的实现放在module_linux.c里。
Module* module_create(const char* file_name, ModuleFlags flags)
{
Module* thiz = NULL;
return_val_if_fail(file_name != NULL, NULL);
if((thiz = malloc(sizeof(Module))) != NULL)
{
thiz->handle = dlopen(file_name, flags & MODULE_FLAGS_DELAY ? RTLD_LAZY : RTLD_NOW);
if(thiz->handle == NULL)
{
free(thiz);
thiz = NULL;
printf("%s/n", dlerror());
}
}
return thiz;
}
我们再看看队列的测试程序怎么写:
#include "linear_container.h"
typedef LinearContainer* (*LinearContainerDarrayCreateFunc)(DataDestroyFunc data_destroy, void* ctx);
int main(int argc, char* argv[])
{
int i = 0;
int n = 1000;
int ret_data = 0;
Queue* queue = NULL;
Module* module = NULL;
LinearContainerDarrayCreateFunc linear_container_create = NULL;
if(argc != 3)
{
printf("%s sharelib linear_container_create/n", argv[0]);
return 0;
}
module = module_create(argv[1], 0);
return_val_if_fail(module != NULL, 0);
linear_container_create = (LinearContainerDarrayCreateFunc)module_sym(module, argv[2]);
return_val_if_fail(linear_container_create != NULL, 0);
queue = queue_create(linear_container_create(NULL, NULL));
...
}
说明:
o 头文件只包含linear_container.h,而不包含linear_container_dlist.h。
o 通过module_sym获取容器的创建函数,而不是直接调用linear_container_dlist_create。
编译时不再链接容器所在的共享库:
gcc -Wall -g module_linux.c queue.c queue_test.c -ldl -o queue_dynamic_test
运行决定要使用的容器:
./queue_dynamic_test ./libcontainer.so linear_container_dlist_create
这样一来,容器的调用者和使用者完全分隔开了。接口+动态加载的方式也称为插件式设计,它是软件可扩展性的基础,像操作系统、办公软件、浏览器和WEB服务器等大型软件都无一例外的使用了这类技术。
本节示例代码请到这里下载。