使用库的方法指南

88 阅读6分钟

注意:这篇文章有点偏向于类似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 。然而,这些环境变量可能不被所有编译器所识别。我知道它至少对clanggcc 起作用。

与一个库链接

当所有的源文件都用-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'