软件库是一种是一直以来长期存在的、简单合理的复用代码的方式。这篇文章解释了如何从头开始构建库并使得其可用。尽管这两个示例库都以 Linux 为例,但创建、发布和使用这些库的步骤也可以应用于其它类 Unix 系统。
这些示例库使用 C 语言编写,非常适合该任务。Linux 内核大部分由 C 语言和少量汇编语言编写(Windows 和 Linux 的表亲如 macOS 也是如此)。用于输入/输出、网络、字符串处理、数学、安全、数据编码等的标准系统库等主要由 C 语言编写。所以使用 C 语言编写库就是使用 Linux 的原生语言来编写。除此之外,C 语言的性能也在一众高级语言中鹤立鸡群。
还有两个来访问这些库的示例客户程序client(一个使用 C,另一个使用 Python)。毫无疑问可以使用 C 语言客户程序来访问 C 语言编写的库,但是 Python 客户程序示例说明了一个由 C 语言编写的库也可以服务于其他编程语言。
静态库和动态库对比
Linux 系统存在两种类型库:
- 静态库(也被称为归档库):在编译过程中的链接阶段,静态库会被编译进程序(例如 C 或 Rust)中。每个客户程序都有属于自己的一份库的拷贝。静态库有一个显而易见的缺点 —— 当库需要进行一定改动时(例如修复一个 bug),静态库必须重新链接一次。接下来要介绍的动态库避免了这一缺点。
- 动态库(也被称为共享库):动态库首先会在程序编译中的链接阶段被标记,但是客户程序和库代码在运行之前仍然没有联系,且库代码不会进入到客户程序中。系统的动态加载器会把一个共享库和正在运行的客户程序进行连接,无论该客户程序是由静态编译语言(如 C)编写,还是由动态解释语言(如 Python)编写。因此,动态库不需要麻烦客户程序便可以进行更新。最后,多个客户程序可以共享同一个动态库的单一副本。
- 库的源代码会被编译成一个或多个目标模块,目标模块是二进制文件,可以被包含在库中并且链接到可执行的二进制中。
- 目标模块会会被打包成一个文件。对于静态库,标准的文件拓展名是 .a意为“归档archive”;对于动态库,标准的文件拓展名是.so意为“共享目标shared object”。对于这两个相同功能的示例库,分别发布为libprimes.a(静态库)和libshprimes.so(动态库)。两种库的文件名都使用前缀lib进行标识。
- 库文件被复制到标准目录下,使得客户程序可以轻松地访问到库。无论是静态库还是动态库,典型的位置是 /usr/lib或者/usr/local/lib,当然其他位置也是可以的。
示例库函数
这两个示例库都是由五个相同的 C 函数构建而成的,其中四个函数可供客户程序使用。第五个函数是其他四个函数的一个工具函数,它显示了 C 语言怎么隐藏信息。每个函数的源代码都很短,可以将这些函数放在单个源文件中,尽管也可以放在多个源文件中(如四个公布的函数都有一个文件)。
这些库函数是基本的处理函数,以多种方式来处理质数。所有的函数接收无符号(即非负)整数值作为参数:
- is_prime函数测试其单个参数是否为质数。
- are_coprimes函数检查了其两个参数的最大公约数greatest common divisor(gcd)是否为 1,即是否为互质数。
- prime_factors:函数列出其参数的质因数。
- glodbach:函数接收一个大于等于 4 的偶数,列出其可以分解为两个质数的和。它也许存在多个符合条件的数对。该函数是以 18 世纪数学家克里斯蒂安·哥德巴赫Christian Goldbach 命名的,他的猜想是任意一个大于 2 的偶数可以分解为两个质数之和,这依旧是数论里最古老的未被解决的问题。
更多关于 C 函数的内容
每个在 C 语言中的函数都有一个存储类,它决定了函数的范围。对于函数,有两种选择。
- 函数默认的存储类是 extern,它给了函数一个全局域。一个客户程序可以调用在示例库中用extern修饰的任意函数。下面是一个带有显式extern声明的are_coprimes函数定义:extern unsigned are_coprimes(unsigned n1, unsigned n2) { ... }复制代码
- 存储类 static将一个函数的的范围限制到函数被定义的文件中。在示例库中,工具函数gcd是静态的(static):static unsigned gcd(unsigned n1, unsigned n2) { ... }复制代码
在 primes.c文件中除了gcd函数外,其他函数并没有指明存储类,默认将会设置为外部的(extern)。然而,在库中显式注明extern更加常见。
C 语言区分了函数的定义definition和声明declaration,这对库来说很重要。接下来让我们开始了解定义。C 语言仅允许命名函数不允许匿名函数,并且每个函数需要定义以下内容:
- 一个唯一的名字。一个程序不允许存在两个同名的函数。
- 一个可以为空的参数列表。参数需要指明类型。
- 一个返回值类型(例如:int代表 32 位有符号整数),当没有返回值时设置为空类型(void)。
- 用一对花括号包围起来的函数主体部分。在一个特制的示例中,函数主体部分可以为空。
下面是库函数 are_coprimes的完整定义:
extern unsigned are_coprimes(unsigned n1, unsigned n2) { /* 定义 */ return 1 == gcd(n1, n2); /* 最大公约数是否为 1? */ }<code></code>
复制代码函数声明不同于定义,其不需要主体部分:
extern unsigned are_coprimes(unsigned n1, unsigned n2); /* 声明 */
复制代码为什么需要声明?在 C 语言中,一个被调用的函数必须对其调用者可见。有多种方式可以提供这样的可见性,具体依赖于编译器如何实现。一个必然可行的方式就是当它们二者位于同一个文件中时,将被调用的函数定义在在它的调用者之前。
void f {...} /* f 定义在其被调用前 */void g { f; } /* ok */
复制代码void f; /* 声明使得函数 f 对调用者可见 */void g { f; } /* ok */ void f {...} /* 相较于前一种方式,此方式显得更简洁 */
复制代码这个问题会影响库,无论是静态库还是动态库。例如在这两个质数库中函数被定义在源文件 primes.c中,每个库中都有该函数的二进制副本,但是这些定义的函数必须要对使用库的 C 程序可见,该 C 程序有其自身的源文件。
函数声明可以帮助提供跨文件的可见性。对于上述的“质数”例子,它有一个名为 primes.h的头文件,其声明了四个函数使得它们对使用库的 C 程序可见。
/** 头文件 primes.h:函数声明 **/extern unsigned is_prime(unsigned); extern void prime_factors(unsigned); extern unsigned are_coprimes(unsigned, unsigned); extern void goldbach(unsigned);<code></code>
复制代码为了客户程序的便利性,头文件 primes.h应该存储在 C 编译器查找路径下的目录中。典型的位置有/usr/include和/usr/local/include。一个 C 语言客户程序应使用#include包含这个头文件,并尽可能将这条语句其程序源代码的首部(头文件将会被导入另一个源文件的“头”部)。C 语言头文件可以被导入其他语言(如 Rust 语言)中的bindgen,使其它语言的客户程序可以访问 C 语言的库。
总之,一个库函数只可以被定义一次,但可以在任何需要它的地方进行声明,任一使用 C 语言库的程序都需要该声明。头文件可以包含函数声明,但不能包含函数定义。如果头文件包含了函数定义,那么该文件可能会在一个 C 语言程序中被多次包含,从而破坏了一个函数在 C 语言程序中必须被精确定义一次的规则。
库的源代码
下面是两个库的源代码。这部分代码、头文件、以及两个示例客户程序都可以在 我的网页上找到。
#include
复制代码这些函数可以被库利用。两个库可以从相同的源代码中获得,同时头文件 primes.h是两个库的 C 语言接口。
构建库
静态库和动态库在构建和发布的步骤上有一些细节的不同。静态库需要三个步骤,而动态库需要增加两个步骤即一共五个步骤。额外的步骤表明了动态库的动态方法具有更多的灵活性。让我们先从静态库开始。
库的源文件 primes.c被编译成一个目标模块。下面是命令,百分号%代表系统提示符,两个井字符#是我的注释。
% gcc -c primes.c ## 步骤1(静态)
复制代码下一步是使用 Linux 的 ar命令将目标对象归档。
% ar -cvq libprimes.a primes.o ## 步骤2(静态)
复制代码归档已经准备好要被发布:
% sudo cp libprimes.a /usr/local/lib ## 步骤3(静态)
复制代码动态库还需要一个或多个对象模块进行打包:
% gcc primes.c -c -fpic ## 步骤1(动态)
复制代码下面是从对象模块创建单个库文件的命令:
% gcc -shared -Wl,-soname,libshprimes.so -o libshprimes.so.1 primes.o ## 步骤2(动态)
复制代码接下来的一步是通过复制共享库到合适的目录下使得客户程序容易访问,例如 /usr/local/lib目录:
% sudo cp libshprimes.so.1 /usr/local/lib ## 步骤3(动态)
复制代码/usr/local/lib/libshprimes.so.1)之间设置一个符号链接。最简单的方式是将/usr/local/lib作为工作目录,在该目录下输入命令:
% sudo ln --symbolic libshprimes.so.1 libshprimes.so ## 步骤4(动态)
复制代码最后一步(一个预防措施)是调用 ldconfig工具来配置系统的动态加载器。这个配置保证了加载器能够找到新发布的库。
% sudo ldconfig ## 步骤5(动态)
复制代码一个使用库的 C 程序
这个示例 C 程序是一个测试程序,它的源代码以两条 #include指令开始:
#include
复制代码相比之下,库的源文件(primes.c)使用#include指令打开以下头文件:
#include
复制代码作为参考,这是测试库程序的源代码:
#include
复制代码在编译 tester.c文件到可执行文件时,难处理的部分时链接选项的顺序。回想前文中提到两个示例库都是用lib作为前缀开始,并且每一个都有一个常规的拓展后缀:.a代表静态库libprimes.a,.so代表动态库libshprimes.so。在链接规范中,前缀lib和拓展名被忽略了。链接标志以-l(小写 L)开始,并且一条编译命令可能包含多个链接标志。下面是一个完整的测试程序的编译指令,使用动态库作为示例:
% gcc -o tester tester.c -lshprimes -lm
复制代码链接器是懒惰的,这意味着链接标志的顺序是需要考虑的。例如,调整上述实例中的链接顺序将会产生一个编译时错误:
% gcc -o tester tester.c -lm -lshprimes ## 危险!
复制代码primes.c: undefined reference to 'sqrt'
复制代码% gcc -o tester tester.c -lshprimes -lm ## 首先链接 -lshprimes
复制代码下面是运行测试程序的部分输出结果:
is_primeSample prime ending in 1: 101 Sample prime ending in 1: 401 ... 168 primes in range of 1 to a thousand. prime_factors prime factors of 12: 2 2 3 prime factors of 13: 13 prime factors of 876,512,779: 211 4154089 are_coprime Are 21 and 22 coprime? yes Are 21 and 24 coprime? no goldbach Number must be > 2 and even: 11 is not. 4 = 2 + 2 6 = 3 + 3 ... 32 = 3 + 29 32 = 13 + 19 ... 100 = 3 + 97 100 = 11 + 89 ...
复制代码封装使用库的 Python 程序
与 C 不同,Python 不是一个静态编译语言,这意味着 Python 客户示例程序必须访问动态版本而非静态版本的 primes库。为了能这样做,Python 中有众多的支持外部语言接口foreign function interface(FFI)的模块(标准的或第三方的),它们允许用一种语言编写的程序来调用另一种语言编写的函数。Python 中的ctypes是一个标准的、相对简单的允许 Python 代码调用 C 函数的 FFI。
任何 FFI 都面临挑战,因为对接的语言不大可能会具有完全相同的数据类型。例如:primes库使用 C 语言类型unsigned int,而 Python 并不具有这种类型;因此ctypesFFI 将 C 语言中的unsigned int类型映射为 Python 中的int类型。在primes库中发布的四个externC 函数中,有两个在具有显式ctypes配置的 Python 中会表现得更好。
C 函数 prime_factors和goldbach返回void而不是返回一个具体类型,但是ctypes默认会将 C 语言中的void替换为 Python 语言中的int。当从 Python 代码中调用时,这两个 C 函数会从栈中返回一个随机整数值(因此,该值无任何意义)。然而,可以对ctypes进行配置,让这些函数返回None(Python 中为null类型)。下面是对prime_factors函数的配置:
primes.prime_factors.restype = None
复制代码下面的交互示例(在 Python3 中)展示了在 Python 客户程序和 primes库之间的接口是简单明了的。
>>> from ctypes import cdll>>> primes = cdll.LoadLibrary("libshprimes.so") ## 逻辑名 >>> primes.is_prime(13) 1 >>> primes.is_prime(12) 0 >>> primes.are_coprimes(8, 24) 0 >>> primes.are_coprimes(8, 25) 1 >>> primes.prime_factors.restype = None >>> primes.goldbach.restype = None >>> primes.prime_factors(72) 2 2 2 3 3 >>> primes.goldbach(32) 32 = 3 + 29 32 = 13 + 19
复制代码简单的 primes库和高级的Numpy库强调了 C 语言仍然是编程语言中的通用语言。几乎每一个语言都可以与 C 语言交互,同时通过 C 语言也可以和任何其他语言交互。Python 很容易和 C 语言交互,作为另外一个例子,当Panama 项目成为 Java Native Interface(JNI)一个替代品后,Java 语言和 C 语言交互也会变的很容易。
via: https://opensource.com/article/21/2/linux-software-libraries
作者:Marty Kalin选题:lujun9972译者:萌新阿岩校对:wxy本文由 LCTT原创编译,Linux中国荣誉推出