C语言编写库就是使用Python代码的一种代码

软件库是重用代码的一种简单且合乎逻辑的方式。

软件库是一种长期存在的、简单合理的代码复用方式。本文介绍了如何从头开始构建库并使其可用。尽管这两个示例库都以 Linux 为例,但创建、分发和使用这些库的步骤也可以应用于其他类 Unix 系统。

这些示例库是用 C 语言编写的,非常适合此任务。Linux 内核主要是用 C 和一点汇编语言编写的(就像 Windows 和 Linux 表亲,如 macOS)。用于输入/输出、网络、字符串处理、数学、安全、数据编码等的标准系统库,主要是用 C 语言编写的。因此,用 C 编写库就是用 Linux 的本地语言编写它。此外,C 语言的性能在高级语言中也很突出。

还有两个示例客户端(一个在 C 中,一个在 Python 中)可以访问这些库。毫无疑问,C 客户端可用于访问用 C 编写的库,但 Python 客户端示例表明,用 C 编写的库也可以服务于其他编程语言。

静态库和动态库对比

Linux 系统中有两种类型的库:

一般来说,动态库比静态库更受欢迎,尽管它们具有更高的复杂性和更低的性能。以下是这两种类型的库的创建和发布方式:

库的源代码被编译成一个或多个目标模块,这些目标模块是可以包含在库中并链接到可执行二进制文件中的二进制文件。对象模块被打包到一个文件中。对于静态库,标准文件扩展名为 .a 表示“归档存档”;对于动态库,“共享对象”的标准文件扩展名是 .so。对于这两个具有相同功能的示例库,它们分别作为 libprimes.a(静态库)和 libshprimes.so(动态库)发布。两个库的文件名都用前缀 lib 标识。库文件被复制到标准目录,以便客户端程序可以轻松访问该库。不管是静态库还是动态库,典型的位置是/usr/lib或者/usr/local/lib,当然,

下面详细介绍了构建和发布每个库的具体步骤。首先介绍一下这两个库中涉及到的C函数。

示例库函数

两个示例库均由五个相同的 C 函数构建而成,其中四个可用于客户端程序。第五个函数是其他四个函数的实用函数,它显示了 C 语言如何隐藏信息。每个函数的源代码都很短,这些函数可以放在一个源文件中,但也可以将它们放在多个源文件中(例如,四个已发布函数中的每一个都放在一个文件中)。

这些库函数是以多种方式处理素数的基本处理函数。所有函数都接受无符号(即非负)整数值作为参数:

实用函数 gcd 保留在部署的库文件中,但不能在不包含此函数的文件中访问。因此c语言是系统软件么,使用该库的客户端程序不能调用 gcd 函数。仔细观察 C 函数可以清楚地看出这一点。

更多关于 C 函数

C 中的每个函数都有一个存储类,它决定了函数的范围。对于函数,有两种选择。

只有 primes.c 文件中的函数可以调用 gcd,并且只有 are_coprimes 函数会调用它。静态库和动态库在构建和发布时,其他程序可以调用外部(extern)函数,如are_coprimes,但不能调用静态(static)函数gcd。静态(静态)存储类通过将函数范围限制为其他库函数,对库的客户端程序隐藏 gcd 函数。

在primes.c文件中,除gcd函数外,其他函数不指定存储类,默认设置为external(extern)。但是,在库中显式注释 extern 更为常见。

C语言区分函数的定义和声明,这对于库来说非常重要。接下来让我们开始定义。C语言只允许命名函数,不允许匿名函数,每个函数需要定义如下:

程序中的每个函数都必须定义一次。

以下是库函数are_coprimes的完整定义:

extern unsigned are_coprimes(unsigned n1, unsigned n2) { /* 定义 */
  return 1 == gcd(n1, n2); /* 最大公约数是否为 1? */
}

该函数根据两个整数参数值的最大公约数是否为 1 返回一个布尔值(0 为假,1 为真)。效用函数 gcd 计算两个整数参数 n1 和 n2 的最大公约数。

与定义不同,函数声明不需要正文部分:

extern unsigned are_coprimes(unsigned n1, unsigned n2); /* 声明 */

声明以参数列表后的分号结尾,并且它没有被花括号包围的主体部分。程序中的函数可以声明多次。

为什么需要声明?在 C 中,被调用函数必须对其调用者可见。有几种方法可以提供这种可见性,具体取决于编译器如何实现它。一个可靠的方法是在调用者之前定义被调用函数,当两者都在同一个文件中时。

void f {...}     /* f 定义在其被调用前 */
void g { f; }  /* ok */

当函数 f 在调用之前声明时,函数 f 的定义可以移到函数 g 之下。

void f;         /* 声明使得函数 f 对调用者可见 */
void g { f; } /* ok */
void f {...}    /* 相较于前一种方式,此方式显得更简洁 */

但是如果被调用函数和调用它的函数不在同一个文件中怎么办?因为上面提到了一个函数需要在程序中定义一次,那么如何让一个文件中定义的函数在另一个文件中可见呢?

此问题会影响库,无论是静态的还是动态的。例如,在两个素数库中,函数在源文件 primes.c 中定义,并且每个库都有函数的二进制副本,但这些定义的函数必须对使用该库的 C 程序可见,该库有自己的自己的源文件。

函数声明可以帮助提供跨文件的可见性。对于上面的“prime”示例,它有一个名为 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);

这些声明通过为每个函数指定它们的调用语法来充当接口。

为了客户端程序的方便,头文件 primes.h 应该存放在 C 编译器搜索路径下的目录中。典型的位置是 /usr/include 和 /usr/local/include。AC 语言客户端程序应使用#include 包含此头文件,并尽可能在其程序源代码的第一部分包含此语句(该头文件将导入另一个源文件的“头”部分)。C头文件可以导入到其他语言(如Rust)的bindgen中,允许其他语言的客户端访问C库。

总之,库函数只能定义一次,但可以在任何需要的地方声明,这是任何使用 C 库的程序都需要的。头文件可以包含函数声明,但不能包含函数定义。如果头文件包含函数定义,则该文件可能会在 C 程序中包含多次,从而打破了函数必须在 C 程序中仅定义一次的规则。

库源代码

下面是这两个库的源代码。这部分代码、头文件和两个示例客户端可以在我的网页上找到。

#include 

库函数

库可以使用这些函数。这两个库都可以从相同的源代码中获得,头文件 primes.h 是两个库的 C 语言接口。

建库

静态库和动态库在构建和发布的步骤中存在一些细节上的不同。静态库需要三步,而动态库需要加两步,一共五步。额外的步骤表明动态库的动态方法具有更大的灵活性。让我们先从静态库开始。

库的源文件 primes.c 被编译成一个目标模块。下面是命令,百分号%代表系统提示符,两个井号#是我的注释。

% gcc -c primes.c ## 步骤1(静态)

这一步生成的目标模块是二进制文件primes.o。-c 标志表示仅编译。

下一步是使用 Linux 的 ar 命令归档目标对象。

% ar -cvq libprimes.a primes.o ## 步骤2(静态)

三个标志 -cvq 是“create”、“verbose”和“quick add”的缩写(以防新文件未添加到存档中)。回想一下,我提到过前缀 lib 是必需的,并且库名称是任意的。当然,库文件名必须是唯一的以避免冲突。

存档已准备好发布:

图片[1]-C语言编写库就是使用Python代码的一种代码-老王博客

% sudo cp libprimes.a /usr/local/lib ## 步骤3(静态)

静态库现在对下一个客户端程序可见,示例如下。(包括 sudo 确保可以将文件复制到 /usr/local/lib 中)

动态库还需要一个或多个对象模块进行打包:

% gcc primes.c -c -fpic ## 步骤1(动态)

添加的选项 -fpic 指示编译器生成与位置无关的代码,这意味着二进制模块不需要加载到固定的内存位置。这种灵活性在具有多个动态库的系统中至关重要。生成的目标模块将比静态库生成的稍大。

这是从目标模块创建单个库文件的命令:

% gcc -shared -Wl,-soname,libshprimes.so -o libshprimes.so.1 primes.o ## 步骤2(动态)

选项 -shared 表示库是共享的(动态的)而不是静态的。-Wl选项引入了一系列编译器选项,其中第一个是设置动态库的-soname,必须设置。soname 首先指定库的逻辑名称(libshprimes.so),然后 -o 选项指定库的物理文件名(libshprimes.so.1)。这样做的目的是保持逻辑名称不变。也允许物理名称随着新版本而改变。在这个例子中,物理文件名 libshprimes.so.1 中的最后一个 1 代表第一个库的版本。虽然逻辑文件名和物理文件名可以相同, 但最佳实践是用不同的方式命名它们. 客户端程序将通过其逻辑名称 (libshprimes.so 在这种情况下) 访问库, 我’

下一步是通过将共享库复制到适当的目录(例如 /usr/local/lib)使其可供客户端程序访问:

% sudo cp libshprimes.so.1 /usr/local/lib ## 步骤3(动态)

现在在共享库的逻辑名 (libshprimes.so) 和它的物理文件名 (/usr/local/lib/libshprimes.so.1) 之间建立一个符号链接。最简单的方法是把 /usr/ local/lib 作为工作目录,在该目录下输入命令:

% sudo ln --symbolic libshprimes.so.1 libshprimes.so ## 步骤4(动态)

逻辑名称 libshprimes.so 不应更改,但符号链接的目标 (libshrimes.so.1) 可以根据需要进行更新,新的库实现可以是错误修复、性能改进等。

最后一步(预防措施)是调用 ldconfig 工具来配置系统的动态加载程序。此配置确保加载器可以找到新发布的库。

% sudo ldconfig ## 步骤5(动态)

至此,动态库已经准备好用于示例客户端,包括以下两个。

使用库的 AC 程序

此示例 C 程序是一个测试程序,其源代码以两个 #include 指令开头:

#include 

文件名周围的尖括号表示这些头文件可以在编译器的搜索路径中找到(在 /usr/local/inlcude 中的 primes.h 的情况下)。如果不包含#include,编译器会抱怨缺少is_prime 和prime_factors 等函数的声明,这些函数在两个库中都发布。顺便说一句,测试程序的源代码不需要更改来测试两个库中的每一个。

相比之下,库的源文件 (primes.c) 使用 #include 指令打开以下头文件:

math.h 头文件是必需的,因为库函数 prime_factors 调用了标准库 libm.so 中的数学函数 sqrt。

供参考,这里是测试库程序的源代码:

#include 

测试程序

棘手的部分是将 tester.c 文件编译为可执行文件时链接选项的顺序。回想一下,上面提到的两个示例库都以 lib 为前缀,并且每个都有一个常规的扩展后缀:.a 代表静态库 libprimes.a,.so 代表动态库 libshprimes.so。在链接规范中,前缀 lib 和 extension 被忽略。链接标志以 -l(小写 L)开头,一个编译命令可能包含多个链接标志。下面是一个完整的测试程序的编译说明,以动态库为例:

% gcc -o tester tester.c -lshprimes -lm

第一个链接标志指定库 libshprimes.so,第二个链接标志指定标准数学库 libm.so。

链接器是惰性的,这意味着要考虑链接标志的顺序。例如,在上例中调整链接顺序会产生编译时错误:

% gcc -o tester tester.c -lm -lshprimes ## 危险!

首先是链接 libm.so 库的标志,但该库中没有函数被测试程序显式调用;因此,链接器不会链接到 math.so 库。调用 sqrt 库函数只发生在 libshprimes.so 库中包含的 prime_factors 函数中。编译测试程序返回的错误是:

primes.c: undefined reference to 'sqrt'

因此,链接标志的顺序应该是通知链接器需要 sqrt 函数:

% gcc -o tester tester.c -lshprimes -lm ## 首先链接 -lshprimes

链接器在 libshprimes.so 库中找到了对库函数 sqrt 的调用,因此数学库 libm.so 已正确链接。链接还有一个更复杂的选项,支持链接标志顺序。然而,在这种情况下,最简单的方法是适当地排列链接标志。

以下是运行测试程序的部分输出:

is_prime
Sample 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
...

对于哥德巴赫函数,即使是 18) 这样相当小的偶数值也可能具有一对素数之和的多种组合(在本例中为 5+13 和 7+11)。因此c语言是系统软件么,如此多对素数是使证明哥德巴赫猜想变得复杂的事情之一。

包装使用该库的 Python 程序

与 C 不同,Python 不是静态编译语言,这意味着 Python 客户端示例程序必须访问素数库的动态版本而不是静态版本。为了能够做到这一点,Python 有许多支持外部函数接口 (FFI) 的模块(标准或第三方),这些模块允许用一种语言编写的程序调用用另一种语言编写的函数。Python 中的 ctypes 是一个标准的、相对简单的 FFI,它允许 Python 代码调用 C 函数。

任何 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 库中的函数仅使用一种简单的数据类型:unsigned int。如果 C 库使用结构体等复杂类型,并且库函数传递和返回指向结构体的指针,那么比 ctypes 更强大的 FFI 更适合作为 Python 和 C 之间的平滑接口。 尽管如此,ctypes 示例显示Python 客户端可以使用的用 C 编写的库。值得注意的是,用于科学计算的流行 Numpy 库是用 C 语言编写的,然后在高级 Python API 中公开。

简单素数库和高级 Numpy 库强调 C 仍然是编程语言中的通用语言。几乎每一种语言都可以与 C 交互,并且通过 C 也可以与任何其他语言交互。Python 很容易与 C 语言交互,再比如,当 Panama 项目成为 Java Native Interface (JNI) 的替代品时,Java 语言和 C 语言的交互将变得非常容易。

通过:

作者:Marty Kalin 题目:lujun9972 译者:梦欣阿燕 校对:wxy

本文由LCTT原创编译,Linux中国光荣推出

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论