理解GNU libtools
一、 田园时代
我要写一个叫做foo的库,它提供一个什么也不做的函数。这个库的头文件为foo.h:
#ifndef FOO_H__
#define FOO_H__
void foo(void);
#endif // FOO_H__
foo.c是这个库的实现:
#include <stdio.h>
#include <stdlib.h>
void foo(void)
{
printf("In foo.\n");
}
用gcc编译生成共享库文件libfoo.so:
$ gcc -shared -fPIC foo.c -o libfoo.so
如果用clang,可以这样:
$ clang -shared -fPIC foo.c -o libfoo.so
如果是在Windows环境中(例如mingw或cygwin之类的环境),可以这样:
$ gcc -shared -fPIC foo.c -o libfoo.dll
于是,问题出现了。。。。如果我想让foo库能够跨平台运行,那么我就不得不为每一个特定的平台提供相应的编译命令或脚本。这意味着,你必须知道各个平台在共享库支持方面的差异及处理方式。这通常是很繁琐很无趣的事,何况我还没有说构建静态库的事情。
这时候,一个10000多行的Bash Shell脚本libtool站了出来,这些破事,我来做!
二、消除环境差异的方法
要有效的消除各个环境差异性,往往有三种办法。
- 第一种办法是革命。。。不要害怕,不是革程序猿的命,而是革环境的命。譬如Linux(我更愿意是Windows)扫清寰宇,一统天下,那么环境的差异也就不行存在了。但是,人类历史已经证明了这条路是走不通的。因为,一旦某个环境对决的统治了一切,那么它下一个要面对的问题就是自身的分裂。。。整部中国历史记录的都是这种事!
- 第二种办法是改良。。。有一批仁人志士成立了某个团体,颁布了一些标准,并号召大家都遵守这个标准,别再自行其是。C语言标准、C++标准、scheme标准等。。。都挺成功的。现在似乎还没有共享库和动态连接库的标准。
- 第三种办法是和谐。。。不要害怕,这里没有GFW。和谐就是承认现实就是这么个狗血的现实,然后追求和而不同。
libtool选择了第三种办法。
三、libtool的[和]
gcc编译生成共享库的命令可以粗略的拆分为两部:编译和连接:
$ gcc -fPIC foo.c -c -o libfoo.o #编译
$ gcc -shared libfoo.o -o libfoo.so # 连接
与之相对应,libtool说:如果你要用gcc编译生成一个共享库,不管在Linux里,还是在Solaris,还是在Mac OS X里,或者是在Windows(Cygwin或MinGW)里,可以使用同样的命令:
$ libtool --tag=cc --mode=compile gcc -c foo.c -o libfoo.lo #编译
$ libtool --tag=cc --mode=link gcc libfoo.lo -rpath /usr/local/lib -o libfoo.la #连接
似乎libtool把问题弄得更加复杂了!不过,仔细观察一下,可以发现一些规律,比如这两个命令的前一半都是:
$ libtool --tag=cc --mode=
--tag选项用于告诉libtool要编译的库是用什么语言写的,cc表示C语言。libtool目前支持的语言如下:
语言 | Tag名称 |
---|---|
C | CC |
C++ | CXX |
Java | GCJ |
Fortran 77 | F77 |
Fortran | FC |
GO | GO |
Window Resource | RC |
--mode选项用于设定libtool的共工作模式。上面的例子中,--mode=compile就是告诉libtool要做的工作是编译,而--mode=link就是连接。
libtool支持7中模式:编译(compile)、连接(link)、执行(execute)、安装(install)、完成、卸载(uninstall)、清理(clean)。每个模式都对应于库的某个开发阶段。这7种模式抽象了大部分平台上的库的开发过程。这是libtool和而不同的第一步:库开发过程的抽象。
下面来看编译过程,当libtool的--mode选项设为compile时,那么随后便是具体的编译命令,本例中是gcc -c foo.c -o libfoo.lo,这条gcc编译命令,会被libtool --tag=cc --mode=compile变换为:
$ gcc -c foo.c -fPIC -DPIC -o .libs/libfoo.o
$ gcc -c foo.c -o libfoo.o > /dev/null 2>&1
注意、注意、注意!事实上,libtool命令中的gcc -c foo.c -o libfoo.lo,并非真正的gcc的编译命令(gcc输出的目标文件默认的扩展名是.o而非.lo),它只是libtool对编译器工作方式的一种抽象。在libtool看来,他所支持的编译器,都应该这样工作:
$ 编译器 -c 源文件 -o 目标文件
如果libtool所支持的编译器并不支持-c与-o选项,那么libtool也会想办法让它们像这样工作!这是libtool和而不同的第二部:库编译过程的抽象。
下面观察一下执行libtool命令前后文件目录的变化。假设foo.h与foo.c位于foo目录,并且foo目录里面只有这两个文件:
$ cd foo
$ tree -a
.
├── foo.c
└── foo.h
0 directories, 2 files
现在执行libtool编译命令:
$ libtool --tag=CC --mode=compile gcc -c foo.c -o libfoo.lo
然后再看一下foo目录:
$ tree -a
.
├── foo.c
├── foo.h
├── libfoo.lo
├── libfoo.o
└── .libs
└── libfoo.o
1 directory, 5 files
执行libtool命令后,多出来一个隐藏目录.libs,以及三份文件libfoo.o、.libs/libfoo.o、libfoo.o。有点诧异的就是有两份libfoo.o文件,虽然它们位于不同的目录,但是它们的内容相同么?libfoo.lo文件说,它们不相同。因为libfoo.lo是一份人类可读的文本文件,用文本编辑器打开它,可以看到如下内容:
# libfoo.lo - a libtool object file
# Generated by libtool (GNU libtool) 2.4.6
#
# Please DO NOT delete this file!
# It is necessary for linking the library.
# Name of the PIC object.
pic_object='.libs/libfoo.o'
# Name of the non-PIC object
non_pic_object='libfoo.o'
位于foo/.libs目录中的libfoo.o,是PIC目标文件,而位于foo目录中的libfoo.o则是非PIC目标文件。在gcc看来,PIC目标文件就是共享库的目标文件,而非PIC的目标文件就是静态库的目标文件。也就是说,libtool的目标不仅仅要生成共享库文件、也要生成静态库文件。这是libtool和而不同的第三步:目标文件的抽象。
接下来,再执行以下libtool的连接命令:
$ libtool --tag=cc --mode=link gcc libfoo.lo -rpath /usr/local/lib -o libfoo.la
从[形状]上来看,这条命令与:
$ gcc -shared libfoo.o -o libfoo.so
相似,libfoo.lo对应linfoo.o,而libfoo.la对应libfoo.so。事实上就是这样对应的。libfoo.lo是对libfoo.o的抽象,而libfoo.la是对libfoo.so的抽象。libfoo.lo抽象的是共享库与静态库的目标文件,而libfoo.la抽象的就是共享库与静态库。libfoo.la也是人类可读的,用文本编辑器打开libfoo.la文件,可以看到:
# libfoo.la - a libtool library file
# Generated by libtool (GNU libtool) 2.4.6
#
# Please DO NOT delete this file!
# It is necessary for linking the library.
# The name that we can dlopen(3).
dlname='libfoo.so.0'
# Names of this library.
library_names='libfoo.so.0.0.0 libfoo.so.0 libfoo.so'
# The name of the static archive.
old_library='libfoo.a'
# Linker flags that cannot go in dependency_libs.
inherited_linker_flags=''
# Libraries that this one depends upon.
dependency_libs=''
# Names of additional weak libraries provided by this library
weak_library_names=''
# Version information for libfoo.
current=0
age=0
revision=0
# Is this an already installed library?
installed=no
# Should we warn about portability when linking against -modules?
shouldnotlink=no
# Files to dlopen/dlpreopen
dlopen=''
dlpreopen=''
# Directory that this library needs to be installed in:
libdir='/usr/local/lib'
文件内容太多,要关注的内容是:
# The name that we can dlopen(3).
dlname='libfoo.so.0'
# Names of this library.
library_names='libfoo.so.0.0.0 libfoo.so.0 libfoo.so'
# The name of the static archive.
old_library='libfoo.a'
Directory that this library needs to be installed in:
libdir='/usr/local/lib'
显然,libfoo.la包含了libtool生成的(其实是gcc生成的)共享库和静态库信息,并且它还包含了一个libdir变量。这个变量的值,显然是libtool的连接命令中的-rpath /usr/local/lib设定的。libdir表示libfoo.la、libfoo.so*、libfoo.a等文件最终都应该放到/usr/local/lib目录。
下面看一下foo目录中的文件变化:
$ tree -a
.
├── foo.c
├── foo.h
├── libfoo.la
├── libfoo.lo
├── libfoo.o
└── .libs
├── libfoo.a
├── libfoo.la -> ../libfoo.la
├── libfoo.lai
├── libfoo.o
├── libfoo.so -> libfoo.so.0.0.0
├── libfoo.so.0 -> libfoo.so.0.0.0
└── libfoo.so.0.0.0
1 directory, 12 files
这就是libtool三步抽象的所有成果,libfoo.a与含有.so的那些文件,就是最终生成的静态库和共享库文件,而libfoo.la是它们的抽象。
注意,还有一个libfoo.lai文件,他是一个临时文件,当我们使用libtool将foo库安装到/usr/local/lib目录时,它就变成了libfoo.la。其实,libfoo.la与libfoo.lai的区别是,前者的内容中有一个installed变量,它的值是no,而在后者的内容中,这个变量的值是yes。可以将此刻的libfoo.la视为安装前的库的抽象,而将libfoo.lai视为安装后的库的抽象。
对未安装的库进行抽象,有什么用?便于在库的开发过程中对其进行单元测试。
四、库的测试
为了显得不那么业务,我需要对foo目录中的文件进行一些变动,变动后的目录结构如下:
$ tree -a
.
├── lib
│ ├── foo.c
│ └── foo.h
└── test
2 directories, 2 files
就是将foo.h与foo.c放到lib目录中,另外新建了一个test目录。我要在test目标中建立测试程序,即test.c,其内容如下:
#include <foo.h>
int main(void)
{
foo();
return 0;
}
然后使用libtool重新编译生成库文件:
$ cd lib
$ libtool --tag=CC --mode=compile gcc -c foo.c -o libfoo.lo
$ libtool --tag=CC --mode=link gcc libfoo.lo -rpath /usr/local/lib -o libfoo.la
现在,foo目录结构变成:
$ cd .. # 返回 foo 目录,因为刚才是在 foo/lib 目录里
$ tree -a
.
├── lib
│ ├── foo.c
│ ├── foo.h
│ ├── libfoo.la
│ ├── libfoo.lo
│ ├── libfoo.o
│ └── .libs
│ ├── libfoo.a
│ ├── libfoo.la -> ../libfoo.la
│ ├── libfoo.lai
│ ├── libfoo.o
│ ├── libfoo.so -> libfoo.so.0.0.0
│ ├── libfoo.so.0 -> libfoo.so.0.0.0
│ └── libfoo.so.0.0.0
└── test
└── test.c
下面,编译测试程序,即编译test.c:
$ cd test
$ libtool --tag=CC --mode=compile gcc -I../lib -c test.c
$ libtool --tag=CC --mode=link gcc ../lib/libfoo.la test.lo -o test
执行test程序:
$ ./test
In foo.
再看一下foo的目录结构的变化:
$ cd .. # 因为刚才在 foo/test 目录中
$ tree -a
.
├── lib
│ ├── foo.c
│ ├── foo.h
│ ├── libfoo.la
│ ├── libfoo.lo
│ ├── libfoo.o
│ └── .libs
│ ├── libfoo.a
│ ├── libfoo.la -> ../libfoo.la
│ ├── libfoo.lai
│ ├── libfoo.o
│ ├── libfoo.so -> libfoo.so.0.0.0
│ ├── libfoo.so.0 -> libfoo.so.0.0.0
│ └── libfoo.so.0.0.0
└── test
├── .libs
│ ├── test
│ └── test.o
├── test
├── test.c
├── test.lo
└── test.o
4 directories, 18 files
结果,在test目录中生成了可执行文件test,但是test目录也有一个.libs目录,而这个.libs目录里也包含了一份可执行文件test。。。。这是libtool的第四部抽象:可执行文件的抽象。不知你有没有注意到,生成test的过程与生成libfoo.la的过程几乎是一样的!其实也没有什么好奇怪的,因为共享库或静态库本身就是可执行文件。
foo/test/test文件,其实是一份Bash脚本,而foo/test/.libs/test才是真正的test程序。为了便于描述,我将前者称为test脚本,将后者称为test程序。test脚本就是对test程序的抽象。
test 脚本所做的工作就是为 test 程序的运行提供正确的环境。因为运行 test 程序,需要加载 foo 库。按照 Linux 的共享库加载逻辑,系统会自动去 /usr/lib 目录为 test 程序搜索共享库 libfoo.so,或者去环境变量 LD_LIBRARY_PATH 所定义的路径去搜索 libfoo.so。但是,但是,但是,此刻我们的 foo 库还没有被安装,我们也没有设置 LD_LIBRARY_PATH 变量,test 程序是运行不起来的,所以,需要一个 test 脚本来抽象它!
用文本编辑器打开 test 脚本,在 200 多行 Bash 代码中可以看到以下内容:
# Add our own library path to LD_LIBRARY_PATH
LD_LIBRARY_PATH="/tmp/foo/lib/.libs:$LD_LIBRARY_PATH"
# Some systems cannot cope with colon-terminated LD_LIBRARY_PATH
# The second colon is a workaround for a bug in BeOS R4 sed
LD_LIBRARY_PATH=`$ECHO "$LD_LIBRARY_PATH" | /bin/sed 's/::*$//'`
export LD_LIBRARY_PATH
我看着这份死活也看不懂的test脚本,深深感到libtool为了让这个什么也不做test程序能够正确的运行,呕心沥血,很拼的!!!
五、库的安装与卸载
foo 库,经过我的精心测试,没发现它有什么 bug,现在我要将它安装到系统中:
$ cd lib # 因为刚才跑到 foo 目录下查看了目录结构
$ sudo libtool --mode=install install -c libfoo.la /usr/local/lib
我觉得我没必要再说废话,简明扼要的说,这是 libtool 的第五步抽象:库文件安装抽象。
事实上,很少有人去用 libtool 来安装库。大部分情况下,libtool 是与 GNU Autotools 配合使用的。更正确的说法是,libtool 属于 GNU Autotools。我不知道在这里我将话题引到 GNU Autotools 是不是太唐突,因为有关 GNU Autotools 的故事,要差不多半个月才能讲完……
简单的说,GNU Autotools 就是产生两份文件,一份文件是 configure,用于检测项目构建(预处理、编译、连接、安装)环境是否完备;另一份文件是 Makefile,用于项目的构建。如果我们的项目是开发一个库,那么一旦有了 GNU Autotools 生成的 Makefile,编译与安装这个库的命令通常是:
$ ./configure # 检测构建环境
$ make # 编译、连接
$ sudo make install # 安装
也就是说,Makefile 中包含了 libtool 的编译、连接以及安装等命令。这篇文章的目的是帮助你理解 libtool,并非希望你使用 libtool 这个小木船来取代航母级别的 GNU Autotools。
既然以后很可能是用 GNU Autotools 来构建项目,因此可以用 libtool 命令卸载刚才所安装的库文件:
$ sudo libtool --mode=uninstall rm /usr/local/lib/libfoo.la
这是 libtool 的第六步抽象:库文件卸载抽象。它对应于 GNU Autotools 产生的 Makefile 中的 make uninstall。
六、归根复命
foo 库,经过我的精心测试,没发现它有什么 bug 了,我想将源代码分享给我的朋友……虽然我几乎没有这种朋友。
既然是分享,那么就得将一些对别人无用的东西都删掉。现在我的 foo 目录中有这些文件:
$ cd .. # 因为刚才在 foo/lib 目录中
$ tree -a
.
├── lib
│ ├── foo.c
│ ├── foo.h
│ ├── libfoo.la
│ ├── libfoo.lo
│ ├── libfoo.o
│ └── .libs
│ ├── libfoo.a
│ ├── libfoo.la -> ../libfoo.la
│ ├── libfoo.lai
│ ├── libfoo.o
│ ├── libfoo.so -> libfoo.so.0.0.0
│ ├── libfoo.so.0 -> libfoo.so.0.0.0
│ └── libfoo.so.0.0.0
└── test
├── .libs
│ ├── test
│ └── test.o
├── test
├── test.c
├── test.lo
└── test.o
4 directories, 18 files
我要删除 .o,.lo,*.la, *.lai, *.a, .so. 等文件,只保留源代码文件。手动删除有点繁琐,为此,libtool 提供了第七步抽象:归根复命的抽象。归根复命,这么高大上的概念,是老子创造的。他说,『夫物芸芸,各归其根。归根曰静,是谓复命』。哲学的事,先放一边,libtool 让 foo 库归根复命的命令是:
$ cd lib
$ libtool --mode=clean rm libfoo.lo libfoo.la
$ cd ../test
$ libtool --mode=clean rm test.lo test
然后再看一下 foo 的目录结构:
$ cd .. # 因为刚才在 test 目录中
$ tree -a
.
├── lib
│ ├── foo.c
│ └── foo.h
└── test
└── test.c
2 directories, 3 files
对于 GNU Autotools 产生的 Makefile 而言,libtool 的 clean 模式对应于 make clean。
七、总结
将上面所讲的 libtool 命令的用法都忘了吧,只需要大致上理解它对哪些事物进行了抽象即可。因为,GNU Autotools 提供的 autoconf 与 automake 已将所有的 libtool 命令隐藏在它们的黑暗魔法中了。
本文转自garfileo博客