Linux下的软件开发,因为有着众多的开源项目助力,开发难度应该降低很多才是。但是如果用的开源库太多了,偶尔也会遇到2个开源库相互冲突的问题。好比正在干活,有两个好朋友一起过来帮忙,但是活没有干完,这两个好朋友却先起冲突干起来了。人在江湖,朋友多了,似乎朋友之间的矛盾也成了自己的矛盾。-------------------------摘自某某码农开发笔记。
特别是在纯C的软件开发中,如果依赖的2个第三方库,有同名的函数,变量或宏定义,那么麻烦就大了去了。有人说,难道编译不过吗?不是,还真不影响编译。但就是编译时候,编译器无法判定这样的问题是错误,无法强制中断编译,就把问题掩盖下来了。直到运行期间,搞几个随机的bug,让你痛苦地debug。
问题背景
2个依赖的so库文件,如果有着同名的函数,变量或宏,会带来什么灾难呢?
比如A,B这2个库有一个同名的枚举定义, 但是对应的整型数值不一致,在调用过程里,到底真实调用的是哪一个值?随机的,这就是一个炸弹了。
这样的问题,还真让我遇上了, 某项目C依赖开源项目libev库开发的,但是某一天,该项目需要集成另外一个自定义的库D。但是库D又依赖于开源项目libevent。
问题分析
我也是掉入坑里后,通过debug发现,libev和libevent这2个库,居然有很多同名的宏和枚举变量。好家伙,软件一启动,就crash了。用gdb跟踪下,发现居然死在不可能出错的地方,再仔细看,引用libev的某某枚举的赋值,居然不对了,差点颠覆世界观。当然,捉住了鬼,自然就不怕了,软件居然在需要调用libev的这个变量时,错误的引用了libevent的同名宏定义。
自此确认,我掉入坑里了。
出坑填坑
第一个想法是:如何让“ldd 可执行文件” 的列表里,不同时出现2个相互冲突的so文件?
第二个想法是:如何不通过编译依赖关系去调用一个共享库文件里的函数?
有了以上2个想法后,根据经验,自然想到了dlopen的办法。
通过dlopen打开so文件,然后,将so文件里的函数名funcA作为参数,包装出一个函数指针变量d_funcA;最后,在原来需要调用funcA的地方,全部替换换成d_funcA的调用。
示例代码如下:
typedef int (*CONNECT_TO_SVR)(char*, int, int, char*, int);
static CONNECT_TO_SVR d_connect_to_svr = NULL;
void *handle = NULL;
char *error = NULL;
handle = dlopen("libsvrsdk.so", RTLD_LAZY | RTLD_DEEPBIND);
if (!handle) {
LOG_ERROR("failed to dlopen libsvrsdk.so\n");
return 1;
}
dlerror(); /* Clear any existing error */
if ((error = dlerror()) != NULL) {
LOG_ERROR("failed to dlopen libsvrsdk.so, error info:%s\n", dlerror());
return 2;
}
d_connect_to_svr = (CONNECT_TO_SVR)dlsym(handle, "connect_to_svr");
if ((error = dlerror()) != NULL) {
LOG_ERROR("failed to dlsym connect_to_svr, error info:%s\n", dlerror());
return 3;
}
同时必须指定so文件加载的路径,
export LD_LIBRARY_PATH=/home/wll/business_serve/build/lib:$LD_LIBRARY_PATH
总结
1. 使用dlopen调用so里的函数,肯定不是正常情况下引用第三方库的好办法
因为这样使用太不方便,关键的是在编译阶段,编译器无法帮助你检查调用的函数是否存在,传入的参数类型是否正确,一切都必须等到runtime的时候才能见分晓。
2. 但是在这里,dlopen可能是唯一的解决办法
并且需要注意,dlopen传入的参数“RTLD_LAZY | RTLD_DEEPBIND”, 这2个参数也是有讲究的。
3.函数dlopen加载so的路径问题
和应用寻找依赖的so库路径类似,只要指定了LD_LIBRARY_PATH环境变量,并将so文件放入LD_LIBRARY_PATH的路径里,就可以确保dlopen能找到该so文件。