注意:这篇文章有点偏向于类似Linux的环境。
本地与非本地
-
本地库:这些是你通过本地编译得到的库。这适用于诸如C、C++、Fortran等语言。这些库是可以互操作的。
这一类也适用于现代的原生编译语言,如Haskell或Rust,但我对这些语言不熟悉,所以我不会在这里讨论它们。
-
非原生库:这些由源代码或字节码组成,不直接在CPU上执行。例子包括C#、Java、Perl、Python、Ruby等。非原生库通常具有高度的语言特异性,而且彼此之间不具有互操作性。
这篇文章只关注本地库,特别是老一代的库(C、C++、Fortran)。
库的组成部分
库一般分为两个部分:
-
库文件,它主要包含机器代码。这是最基本的库实现。
文件的扩展名通常是:
.a/.so(ELF操作系统,如Linux)。.a/.dylib(OS X)。.lib/.dll(Windows)。
每对文件中的前者是静态库的文件扩展名,而每对文件中的后者是共享库,又称动态链接库(DLLs)。下一节将解释这种区别。
-
库的接口,这是一个库的用户必须遵守的协议,以便正确使用该库。如果不这样做,往往会导致崩溃(segfaults)和故障。
对于C和C++,这通常是作为头文件分发的(
.h,.hpp, 等等)。有一些方法可以在没有头文件的情况下使用一个库,但通常不鼓励这样做,因为它容易出错。不过有一个例外:想要使用C/C++库的非C/C++程序(例如Rust程序)通常不需要头文件。对于Fortran来说,库的接口通常只是源代码本身,或者可以互换为
.mod文件。
静态库与共享库
-
在静态库中,库自身的代码被复制并与用户的代码合并,形成一个合并文件。这意味着在编译之后,静态库文件本身不再需要也不再使用。(当然,你可能想保留它,以备你想用那个相同的库来编译更多的程序。)
请注意,静态库是相当愚蠢的:你不能将同一个静态库的多个副本链接到同一个程序中。你可能会问:"为什么有人会这么做?"好吧,这往往是无意的:比如说你使用库A和库B,它们都使用库C。如果这整个事情是静态链接的,那么A和B都会带来它们自己的C的副本
-
在共享库中,库自己的代码仍然是一个单独的文件。这意味着以后当程序运行时,它必须作为一个单独的步骤来加载库。如果找不到共享库,程序将无法启动。如果共享库被升级或改变,那么任何依赖该库的程序的行为都可能发生变化(这可能既有用又令人讨厌)。
尽管共享库从未被纳入最终程序,但在编译过程中仍然需要它们,因为编译器仍然需要了解库的接口。在非Windows系统上,编译器在编译时只需读取共享库本身(
.so或.dylib)。在Windows上,编译需要一个所谓的导入库,它的扩展名是.lib(不要与Windows上的静态库混淆,后者也有同样的扩展名)。
安装一个库
使用一个库的第一步显然是安装它。这个过程有很大的不同。最好是阅读特定库的文档。要弄清楚的最关键的事情之一是该库将被安装在哪里。
(另一件关键的事情是找出该库是否有可选的功能,因为它们可能在默认情况下是关闭的,或者在配置步骤中未能找到其依赖关系时被关闭!)。
前缀
在类Unix操作系统上,库通常默认安装到/usr/local 前缀。这个前缀意味着所有库的相关文件将被安装到/usr/local/lib (库文件)、/usr/local/include (头文件)、/usr/local/bin (可执行程序),等等。要改变前缀,其过程根据库所使用的构建系统而不同。
-
对于类似Autoconf的构建系统,通常可以通过
./configure脚本来改变。命令会是这样的./configure --prefix=/my_custom_prefix -
对于CMake构建系统,可以通过以下方式进行修改
cmake -DCMAKE_INSTALL_PREFIX=/my_custom_prefix .
将库安装到/usr 前缀被认为是不好的做法,因为这属于系统包管理器的范畴。
包括一个库
为了在你的C或C++代码中使用这个库,你可能已经在你的源代码中的不同地方添加了
#include <some_library_header.h>
到你的源代码中的各个地方,以告知编译器关于库的接口(通过向前声明各种类型、函数和变量)。这些头文件通常在库的安装地点的include 目录下找到。
如果库的头文件存在于一个标准化的位置,如/usr/include ,那么编译器就能找到它们。但是如果你把它们安装在一个不寻常的位置,如/my_custom_prefix/include ,那么你就必须给你的编译器一个提示,让它知道在哪里寻找。这可以通过-I 标志来完成。
cc -c -I/my_custom_prefix/include my_program.c
(用你使用的任何编译器替换cc 。)另一种方法是使用C_INCLUDE_PATH (C) 和/或CPLUS_INCLUDE_PATH (C++) 环境变量。
export C_INCLUDE_PATH=/my_custom_prefix/include
cc -c my_program.c
你可以在变量中使用冒号(:)设置多个路径,类似于PATH 。然而,这些环境变量可能不被所有编译器所识别。我知道它至少对clang 和gcc 起作用。
与一个库链接
当所有的源文件都用-c 编译后,你现在需要把所有的东西连接起来,包括你使用的任何库。这可以用-l 标志来完成。
cc my_program.o my_blah.o my_foo.o -lalpha -lbeta
-l 后面的字是库的名称。如果库文件是libalpha.so ,那么它的名字就是alpha 。
如果alpha 和/或beta 在一个非常规的位置/my_custom_prefix/lib ,那么你必须传入一个标志-L 来告诉编译器在哪里可以找到它们。
cc -L/my_custom_prefix/lib my_program.o my_blah.o my_foo.o -lalpha -lbeta
你也可以用另一个用冒号分隔的环境变量LIBRARY_PATH 来做这个:
export LIBRARY_PATH=/my_custom_prefix/lib
cc my_program.o my_blah.o my_foo.o -lalpha -lbeta
同样,我认为只有一些编译器能识别这个变量。
加载一个共享库
最后一步是确保程序能够真正加载共享库。通常这是自动的,但是如果库在一个非常规的地方,比如/my_custom_prefix/lib ,那么你就得用另一个用冒号分隔的环境变量LD_LIBRARY_PATH (如果你用的是OS X,则是DYLIB_LIBRARY_PATH )给它一个提示:
export LD_LIBRARY_PATH=/my_custom_prefix/lib
./a.out
另外,你也可以把库的路径直接放在程序中,这样你就不需要环境变量来运行程序。这就是 -rpath标志:
cc -Wl,-rpath=/my_custom_prefix/lib my_program.o my_blah.o my_foo.o -lalpha -lbeta
你可以使用${ORIGIN} 这个占位符来指定一个相对于程序本身位置的路径。注意,这不是一个shell变量,所以一定要在shell中单引号:
cc -Wl,-rpath='${ORIGIN}/lib' …