理解 C 语言 Linux 软件库的指南理解 C 语言 Linux 软件库的指南理解 C 语言 Linux 软件库的指南理解 C 语言 Linux 软件库的指南
  • 文章
  • 正则表达式
    • 工具
  • 登录
找到的结果: {phrase} (显示: {results_count} 共: {results_count_total})
显示: {results_count} 共: {results_count_total}

加载更多搜索结果...

搜索范围
模糊匹配
搜索标题
搜索内容
发表 admin at 2025年2月28日
类别
  • 未分类
标签

理解 C 语言 Linux 软件库的指南

软件库是重用代码的一种简单而明智的方法。

软件库是一种长期存在的、简单且明智的重用代码的方式。本文介绍了如何从头开始构建库并将其提供给客户。尽管这两个示例库面向 Linux,但创建、发布和使用这些库的步骤适用于其他类 Unix 系统。

示例库是用 C 语言编写的,非常适合该任务。 Linux 内核主要用 C 语言编写,其余部分用汇编语言编写。 (对于 Windows 和 Linux 的同类产品,例如 macOS,也是如此。)用于输入/输出、网络、字符串处理、数学、安全、数据编码等的标准系统库同样主要用 C 编写。因此C是用Linux的母语来编写的。此外,C 在高级语言中树立了性能标杆。

还有两个示例客户端(一个用 C 语言,另一个用 Python 语言)来访问这些库。 C 客户端可以访问用 C 编写的库并不奇怪,但 Python 客户端说明用 C 编写的库可以为其他语言的客户端提供服务。

静态库与动态库

Linux 系统有两种类型的库:

  • 静态库(又名库存档)在编译过程的链接阶段被烘焙到静态编译的客户端(例如,C 或 Rust 中的客户端)中。实际上,每个客户端都会获得自己的库副本。如果需要修改库(例如,修复错误),静态库的一个重大缺点就会凸显出来,因为每个库客户端都必须重新链接到静态库。接下来描述的动态库可以避免这个缺点。
  • 动态(又名共享)库在静态编译的客户端程序的链接阶段被标记,但客户端程序和库代码在运行时之前保持未连接状态 - 库代码不会烘焙到客户端中。在运行时,系统的动态加载器将共享库与执行客户端连接,无论客户端是来自静态编译语言(例如 C)还是动态编译语言(例如 Python)。因此,可以更新动态库而不会给客户带来不便。最后,多个客户端可以共享动态库的单个副本。

一般来说,动态库比静态库更受青睐,尽管在复杂性和性能方面存在成本。以下是每种库类型的创建和发布方式:

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

构建和发布每种类型库的详细步骤即将推出。不过,首先我将介绍这两个库中的 C 函数。

示例库函数

这两个示例库是由相同的五个 C 函数构建的,其中四个可供客户端程序访问。第五个函数是其他四个函数之一的实用程序,它显示了 C 如何支持隐藏信息。每个函数的源代码足够短,可以将这些函数容纳在单个源文件中,但也可以选择使用多个源文件(例如,四个已发布函数中的每个函数一个)。

库函数是基本函数,并以各种方式处理素数。所有函数都期望无符号(即非负)整数值作为参数:

  • is_prime 函数测试其单个参数是否为素数。
  • are_coprimes 函数检查其两个参数的最大公约数 (gcd) 是否为 1,这定义了互质数。
  • prime_factors 函数列出其参数的素因数。
  • Goldbach 函数需要 4 或更大的偶整数值,列出该参数的任意两个素数之和;可能有多个求和对。该函数以 18 世纪数学家克里斯蒂安·哥德巴赫 (Christian Goldbach) 的名字命名,他关于每个大于 2 的偶数都是两个素数之和的猜想仍然是数论中最古老的未解决问题之一。

实用函数gcd驻留在已部署的库文件中,但在其包含文件之外无法访问该函数;因此,库客户端无法直接调用 gcd 函数。仔细看看 C 函数就可以澄清这一点。

只有 primes.c 文件中的函数可以调用 gcd,并且只有函数 are_coprimes 可以这样做。构建并发布静态和动态库后,其他程序可以调用 extern 函数,例如 are_coprimes,但不能调用 static 函数 gcd 。因此,static 存储类通过将函数的范围限制为其他库函数,对库客户端隐藏了 gcd 函数。

primes.c 文件中除 gcd 之外的函数不需要指定存储类,默认为 extern。然而,在库中使 extern 显式化是很常见的。

C 区分函数定义和声明,这对于库来说很重要。让我们从定义开始。 C 仅具有命名函数,每个函数都定义为:

  • 一个独特的名字。程序中的两个函数不能具有相同的名称。
  • 参数列表,可能为空。参数已键入。
  • 返回值类型(例如,int 表示 32 位有符号整数)或 void(如果没有返回值)。
  • 用大括号括起来的主体。在一个人为的例子中,主体可能是空的。

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

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

extern unsigned are_coprimes(unsigned n1, unsigned n2) { /* definition */
  return 1 == gcd(n1, n2); /* greatest common divisor of 1? */
}

该函数返回一个布尔值(0 表示 false,1 表示 true),具体取决于两个整数参数的最大公约数是否为 1。实用函数 gcd 计算整数的最大公约数参数 n1 和 n2。

与定义不同,函数声明没有函数体:

extern unsigned are_coprimes(unsigned n1, unsigned n2); /* declaration */

声明以参数列表后面的分号结束;没有花括号包围主体。一个函数可以在程序中声明多次。

为什么需要声明?在 C 中,被调用函数必须对其调用者可见。有多种方法可以提供这种可见性,具体取决于编译器的复杂程度。一种可靠的方法是当两者驻留在同一文件中时,将被调用函数定义在其调用者之上:

void f() {...}     /* f is defined before being called */
void g() { f(); }  /* ok */

如果 f 声明在调用上方,则函数 f 的定义可以移至函数 g 的调用下方:

void f();         /* declaration makes f visible to caller */
void g() { f(); } /* ok */
void f() {...}    /* easier to put this above the call from g */

但是,如果被调用函数与其调用者驻留在不同的文件中怎么办?鉴于每个函数必须在程序中精确定义一次,如何使一个文件中定义的函数在另一个文件中可见?

此问题会影响库,无论是静态库还是动态库。例如,两个 primes 库中的函数都在源文件 primes.c 中定义,每个库中都有其二进制副本;但这些定义的函数必须对 C 语言的库客户端可见,C 语言是一个具有自己的源文件的单独程序。

函数声明可以提供跨文件的可见性。对于“primes”示例,有一个名为 primes.h 的头文件,它声明了四个对 C 语言库客户端可见的函数:

/** header file primes.h: function declarations **/
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。 C 客户端会#include 这个头文件靠近客户端源代码的顶部。 (因此,头文件被导入到另一个源文件的“头”中。)C 头文件还可以用作实用程序(例如 Rust 的 bindgen)的输入,使其他语言的客户端能够访问 C图书馆。

总之,库函数只定义一次,但在需要的地方声明; C 中的任何库客户端都需要声明。头文件应包含函数声明,但不包含函数定义。如果头文件确实包含定义,则该文件可能会在 C 程序中多次包含,从而违反了函数必须在 C 程序中仅定义一次的规则。

库源代码

下面是两个库的源代码。我的网站上提供了此代码、头文件和两个示例客户端。

库函数

#include <stdio.h>
#include <math.h>

extern unsigned is_prime(unsigned n) { 
  if (n <= 3) return n > 1;                   /* 2 and 3 are prime */
  if (0 == (n % 2) || 0 == (n % 3)) return 0; /* multiples of 2 or 3 aren't */

  /* check that n is not a multiple of other values < n */
  unsigned i;
  for (i = 5; (i * i) <= n; i += 6)
    if (0 == (n % i) || 0 == (n % (i + 2))) return 0; /* not prime */

  return 1; /* a prime other than 2 or 3 */
}

extern void prime_factors(unsigned n) {
  /* list 2s in n's prime factorization */
  while (0 == (n % 2)) {  
    printf("%i ", 2);
    n /= 2;
  }

  /* 2s are done, the divisor is now odd */
  unsigned i;
  for (i = 3; i <= sqrt(n); i += 2) {
    while (0 == (n % i)) {
      printf("%i ", i);
      n /= i;
    }
  }

  /* one more prime factor? */
  if (n > 2) printf("%i", n);
}

/* utility function: greatest common divisor */
static unsigned gcd(unsigned n1, unsigned n2) {
  while (n1 != 0) {
    unsigned n3 = n1;
    n1 = n2 % n1;
    n2 = n3;
  }
  return n2;
}

extern unsigned are_coprimes(unsigned n1, unsigned n2) {
  return 1 == gcd(n1, n2);
}

extern void goldbach(unsigned n) {
  /* input errors */
  if ((n <= 2) || ((n & 0x01) > 0)) {
    printf("Number must be > 2 and even: %i is not.\n", n);
    return;
  }

  /* two simple cases: 4 and 6 */
  if ((4 == n) || (6 == n)) {
    printf("%i = %i + %i\n", n, n / 2, n / 2);
    return;
  }
  
  /* for n >= 8: multiple possibilities for many */
  unsigned i;
  for (i = 3; i < (n / 2); i++) {
    if (is_prime(i) && is_prime(n - i)) {
      printf("%i = %i + %i\n", n, i, n - i);
      /* if one pair is enough, replace this with break */
    }
  }
}

这些功能为图书馆磨坊提供了谷物。这两个库派生自完全相同的源代码,头文件 primes.h 是这两个库的 C 接口。

建设图书馆

构建和发布静态库和动态库的步骤在一些细节上有所不同。静态库只需要三个步骤,动态库只需要两步。构建动态库的额外步骤反映了动态方法增加的灵活性。让我们从静态库开始。

库源文件primes.c被编译成目标模块。这是命令,以百分号作为系统提示符(双尖号介绍我的评论):

% gcc -c primes.c ## step 1 static

这会生成二进制文件primes.o,即对象模块。标志 -c 表示仅编译。

下一步是使用 Linux ar 实用程序归档目标模块:

% ar -cvq libprimes.a primes.o ## step 2 static

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

存档已准备好发布:

% sudo cp libprimes.a /usr/local/lib ## step 3 static

客户端现在可以访问静态库,示例即将推出。 (包含 sudo 是为了确保将文件复制到 /usr/local/lib 的正确访问权限。)

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

% gcc primes.c -c -fpic ## step 1 dynamic

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

以下是从对象模块创建单个库文件的命令:

% gcc -shared -Wl,-soname,libshprimes.so -o libshprimes.so.1 primes.o ## step 2 dynamic

标志 -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 ## step 3 dynamic

现在,在共享库的逻辑名称 (libshprimes.so) 与其完整物理文件名称 (/usr/local/lib/libshprimes.so.1) 之间建立了符号链接>)。最简单的方法是使用 /usr/local/lib 作为工作目录来执行命令:

% sudo ln --symbolic libshprimes.so.1 libshprimes.so ## step 4 dynamic

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

最后一步(预防性步骤)是调用 ldconfig 实用程序,该实用程序配置系统的动态加载程序。此配置可确保加载器能够找到新发布的库:

% sudo ldconfig ## step 5 dynamic

动态库现已准备好供客户使用,包括下面的两个示例。

C 库客户端

示例 C 客户端是程序测试器,其源代码以两个 #include 指令开头:

#include <stdio.h>  /* standard input/output functions */
#include <primes.h> /* my library functions */

文件名两边的尖括号表示这些头文件可以在编译器的搜索路径中找到(对于primes.h来说,目录/usr/local/include)。如果没有此#include,编译器会抱怨缺少诸如 is_prime 和 prime_factors 等函数的声明,这些函数均在两个库中发布。顺便说一句,测试器程序的源代码根本不需要更改即可测试这两个库中的每一个。

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

#include <stdio.h>
#include <math.h>

需要头文件 math.h,因为库函数 prime_factors 调用标准库 libm.so< 中的数学函数 sqrt /代码>。

作为参考,这里是测试程序的源代码:

测试程序

#include <stdio.h>
#include <primes.h>

int main() {
  /* is_prime */
  printf("\nis_prime\n");
  unsigned i, count = 0, n = 1000; 
  for (i = 1; i <= n; i++) {
    if (is_prime(i)) {
      count++;
      if (1 == (i % 100)) printf("Sample prime ending in 1: %i\n", i);
    }
  }
  printf("%i primes in range of 1 to a thousand.\n", count);

  /* prime_factors */
  printf("\nprime_factors\n");
  printf("prime factors of 12: ");
  prime_factors(12);
  printf("\n");
  
  printf("prime factors of 13: ");
  prime_factors(13);
  printf("\n");
  
  printf("prime factors of 876,512,779: ");
  prime_factors(876512779);
  printf("\n");

  /* are_coprimes */
  printf("\nare_coprime\n");
  printf("Are %i and %i coprime? %s\n",
	 21, 22, are_coprimes(21, 22) ? "yes" : "no");
  printf("Are %i and %i coprime? %s\n",
	 21, 24, are_coprimes(21, 24) ? "yes" : "no");

  /* goldbach */
  printf("\ngoldbach\n");
  goldbach(11);    /* error */
  goldbach(4);     /* small one */
  goldbach(6);     /* another */
  for (i = 100; i <= 150; i += 2) goldbach(i); 

  return 0;
}

将 tester.c 编译为可执行文件时,棘手的部分是链接标志的顺序。回想一下,这两个示例库都以前缀 lib 开头,并且每个库都有常用的扩展名:.a(静态库 libprimes.a)和.so 表示动态库 libshprimes.so。在链接规范中,前缀 lib 和扩展名消失了。链接标志以-l(小写L)开头,并且编译命令可能包含许多链接标志。以下是测试程序的完整编译命令,以动态库为例:

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

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

链接器是惰性的,这意味着链接标志的顺序很重要。例如,颠倒链接规范的顺序会生成编译时错误:

% gcc -o tester tester.c -lm -lshprimes ## danger!

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

primes.c: undefined reference to 'sqrt'

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

% gcc -o tester tester.c -lshprimes -lm ## -lshprimes 1st

链接器获取对 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
...

对于goldbach函数,即使是相对较小的偶数(例如18)也可能有多对素数相加(在本例中为5+13和7+11)。如此多的素数对是使哥德巴赫猜想的证明变得复杂的因素之一。

结束 Python 客户端

与 C 不同,Python 不是静态编译语言,这意味着示例 Python 客户端必须访问 primes 库的动态版本而不是静态版本。为此,Python 具有支持外部函数接口 (FFI) 的各种模块(标准模块和第三方模块),该接口允许用一种语言编写的程序调用用另一种语言编写的函数。 Python ctypes 是一个标准且相对简单的 FFI,使 Python 代码能够调用 C 函数。

任何 FFI 都面临挑战,因为接口语言不太可能具有完全相同的数据类型。例如,primes库使用C类型unsigned int,这是Python所没有的; ctypes FFI 将 C unsigned int 映射到 Python int。在 primes 库中发布的四个 extern C 函数中,有两个在具有显式 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

类似的语句处理goldbach 函数。

下面的交互式会话(在 Python 3 中)显示 Python 客户端和 primes 库之间的接口非常简单:

>>> from ctypes import cdll

>>> primes = cdll.LoadLibrary("libshprimes.so") ## logical name

>>> 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 与任何其他与 C 对话的语言对话。Python 可以轻松地与 C 对话,再举一个例子,当巴拿马项目成为 Java 本机接口 (JNI) 的替代方案时,Java 也可以做同样的事情。 )。

©2015-2025 艾丽卡 support@alaica.com