用 C 語言理解 Linux 軟件庫
軟件庫是重復使用代碼的一種簡單而合理的方式。
軟件庫是一種是一直以來長期存在的、簡單合理的復用代碼的方式。這篇文章解釋了如何從頭開始構(gòu)建庫并使得其可用。盡管這兩個示例庫都以 Linux 為例,但創(chuàng)建、發(fā)布和使用這些庫的步驟也可以應用于其它類 Unix 系統(tǒng)。
這些示例庫使用 C 語言編寫,非常適合該任務。Linux 內(nèi)核大部分由 C 語言和少量匯編語言編寫(Windows 和 Linux 的表親如 macOS 也是如此)。用于輸入/輸出、網(wǎng)絡、字符串處理、數(shù)學、安全、數(shù)據(jù)編碼等的標準系統(tǒng)庫等主要由 C 語言編寫。所以使用 C 語言編寫庫就是使用 Linux 的原生語言來編寫。除此之外,C 語言的性能也在一眾高級語言中鶴立雞群。
還有兩個來訪問這些庫的示例客戶程序(一個使用 C,另一個使用 Python)。毫無疑問可以使用 C 語言客戶程序來訪問 C 語言編寫的庫,但是 Python 客戶程序示例說明了一個由 C 語言編寫的庫也可以服務于其他編程語言。
靜態(tài)庫和動態(tài)庫對比
Linux 系統(tǒng)存在兩種類型庫:
- 靜態(tài)庫(也被稱為歸檔庫):在編譯過程中的鏈接階段,靜態(tài)庫會被編譯進程序(例如 C 或 Rust)中。每個客戶程序都有屬于自己的一份庫的拷貝。靜態(tài)庫有一個顯而易見的缺點 —— 當庫需要進行一定改動時(例如修復一個 bug),靜態(tài)庫必須重新鏈接一次。接下來要介紹的動態(tài)庫避免了這一缺點。
- 動態(tài)庫(也被稱為共享庫):動態(tài)庫首先會在程序編譯中的鏈接階段被標記,但是客戶程序和庫代碼在運行之前仍然沒有聯(lián)系,且?guī)齑a不會進入到客戶程序中。系統(tǒng)的動態(tài)加載器會把一個共享庫和正在運行的客戶程序進行連接,無論該客戶程序是由靜態(tài)編譯語言(如 C)編寫,還是由動態(tài)解釋語言(如 Python)編寫。因此,動態(tài)庫不需要麻煩客戶程序便可以進行更新。最后,多個客戶程序可以共享同一個動態(tài)庫的單一副本。
通常來說,動態(tài)庫優(yōu)于靜態(tài)庫,盡管其復雜性較高而性能較低。下面是兩種類型的庫如何創(chuàng)建和發(fā)布:
- 庫的源代碼會被編譯成一個或多個目標模塊,目標模塊是二進制文件,可以被包含在庫中并且鏈接到可執(zhí)行的二進制中。
- 目標模塊會會被打包成一個文件。對于靜態(tài)庫,標準的文件拓展名是
.a意為“歸檔”;對于動態(tài)庫,標準的文件拓展名是.so意為“共享目標”。對于這兩個相同功能的示例庫,分別發(fā)布為libprimes.a(靜態(tài)庫)和libshprimes.so(動態(tài)庫)。兩種庫的文件名都使用前綴lib進行標識。 - 庫文件被復制到標準目錄下,使得客戶程序可以輕松地訪問到庫。無論是靜態(tài)庫還是動態(tài)庫,典型的位置是
/usr/lib或者/usr/local/lib,當然其他位置也是可以的。
構(gòu)建和發(fā)布每種庫的具體步驟會在下面詳細介紹。首先我將介紹兩種庫里涉及到的 C 函數(shù)。
示例庫函數(shù)
這兩個示例庫都是由五個相同的 C 函數(shù)構(gòu)建而成的,其中四個函數(shù)可供客戶程序使用。第五個函數(shù)是其他四個函數(shù)的一個工具函數(shù),它顯示了 C 語言怎么隱藏信息。每個函數(shù)的源代碼都很短,可以將這些函數(shù)放在單個源文件中,盡管也可以放在多個源文件中(如四個公布的函數(shù)都有一個文件)。
這些庫函數(shù)是基本的處理函數(shù),以多種方式來處理質(zhì)數(shù)。所有的函數(shù)接收無符號(即非負)整數(shù)值作為參數(shù):
is_prime函數(shù)測試其單個參數(shù)是否為質(zhì)數(shù)。are_coprimes函數(shù)檢查了其兩個參數(shù)的最大公約數(shù)(gcd)是否為 1,即是否為互質(zhì)數(shù)。prime_factors:函數(shù)列出其參數(shù)的質(zhì)因數(shù)。glodbach:函數(shù)接收一個大于等于 4 的偶數(shù),列出其可以分解為兩個質(zhì)數(shù)的和。它也許存在多個符合條件的數(shù)對。該函數(shù)是以 18 世紀數(shù)學家 克里斯蒂安·哥德巴赫 命名的,他的猜想是任意一個大于 2 的偶數(shù)可以分解為兩個質(zhì)數(shù)之和,這依舊是數(shù)論里最古老的未被解決的問題。
工具函數(shù) gcd 留在已部署的庫文件中,但是在沒有包含這個函數(shù)的文件無法訪問此函數(shù)。因此,一個使用庫的客戶程序無法調(diào)用 gcd 函數(shù)。仔細觀察 C 函數(shù)可以明白這一點。
更多關(guān)于 C 函數(shù)的內(nèi)容
每個在 C 語言中的函數(shù)都有一個存儲類,它決定了函數(shù)的范圍。對于函數(shù),有兩種選擇。
-
函數(shù)默認的存儲類是
extern,它給了函數(shù)一個全局域。一個客戶程序可以調(diào)用在示例庫中用extern修飾的任意函數(shù)。下面是一個帶有顯式extern聲明的are_coprimes函數(shù)定義:extern unsigned are_coprimes(unsigned n1, unsigned n2) {...}
-
存儲類
static將一個函數(shù)的的范圍限制到函數(shù)被定義的文件中。在示例庫中,工具函數(shù)gcd是靜態(tài)的(static):static unsigned gcd(unsigned n1, unsigned n2) {...}
只有在 primes.c 文件中的函數(shù)可以調(diào)用 gcd,而只有 are_coprimes 函數(shù)會調(diào)用它。當靜態(tài)庫和動態(tài)庫被構(gòu)建和發(fā)布后,其他的程序可以調(diào)用外部的(extern)函數(shù),如 are_coprimes ,但是不可以調(diào)用靜態(tài)(static)函數(shù) gcd。靜態(tài)(static)存儲類通過將函數(shù)范圍限制在其他庫函數(shù)內(nèi),進而實現(xiàn)了對庫的客戶程序隱藏 gcd 函數(shù)。
在 primes.c 文件中除了 gcd 函數(shù)外,其他函數(shù)并沒有指明存儲類,默認將會設(shè)置為外部的(extern)。然而,在庫中顯式注明 extern 更加常見。
C 語言區(qū)分了函數(shù)的定義和聲明,這對庫來說很重要。接下來讓我們開始了解定義。C 語言僅允許命名函數(shù)不允許匿名函數(shù),并且每個函數(shù)需要定義以下內(nèi)容:
- 一個唯一的名字。一個程序不允許存在兩個同名的函數(shù)。
- 一個可以為空的參數(shù)列表。參數(shù)需要指明類型。
- 一個返回值類型(例如:
int代表 32 位有符號整數(shù)),當沒有返回值時設(shè)置為空類型(void)。 - 用一對花括號包圍起來的函數(shù)主體部分。在一個特制的示例中,函數(shù)主體部分可以為空。
程序中的每個函數(shù)必須要被定義一次。
下面是庫函數(shù) are_coprimes 的完整定義:
extern unsigned are_coprimes(unsigned n1, unsigned n2) { /* 定義 */return 1 == gcd(n1, n2); /* 最大公約數(shù)是否為 1? */}
函數(shù)返回一個布爾值(0 代表假,1 代表真),取決于兩個整數(shù)參數(shù)值的最大公約數(shù)是否為 1。工具函數(shù) gcd 計算兩個整數(shù)參數(shù) n1 和 n2 的最大公約數(shù)。
函數(shù)聲明不同于定義,其不需要主體部分:
extern unsigned are_coprimes(unsigned n1, unsigned n2); /* 聲明 */
聲明在參數(shù)列表后用一個分號代表結(jié)束,它沒有被花括號包圍起來的主體部分。程序中的函數(shù)可以被多次聲明。
為什么需要聲明?在 C 語言中,一個被調(diào)用的函數(shù)必須對其調(diào)用者可見。有多種方式可以提供這樣的可見性,具體依賴于編譯器如何實現(xiàn)。一個必然可行的方式就是當它們二者位于同一個文件中時,將被調(diào)用的函數(shù)定義在在它的調(diào)用者之前。
void f() {...} /* f 定義在其被調(diào)用前 */void g() { f(); } /* ok */
當函數(shù) f 被在調(diào)用前聲明,此時函數(shù) f 的定義可以移動到函數(shù) g 的下方。
void f(); /* 聲明使得函數(shù) f 對調(diào)用者可見 */void g() { f(); } /* ok */void f() {...} /* 相較于前一種方式,此方式顯得更簡潔 */
但是當如果一個被調(diào)用的函數(shù)和調(diào)用它的函數(shù)不在同一個文件中時呢?因為前文提到一個函數(shù)在一個程序中需要被定義一次,那么如何使得讓一個文件中被定義的函數(shù)在另一個文件中可見?
這個問題會影響庫,無論是靜態(tài)庫還是動態(tài)庫。例如在這兩個質(zhì)數(shù)庫中函數(shù)被定義在源文件 primes.c 中,每個庫中都有該函數(shù)的二進制副本,但是這些定義的函數(shù)必須要對使用庫的 C 程序可見,該 C 程序有其自身的源文件。
函數(shù)聲明可以幫助提供跨文件的可見性。對于上述的“質(zhì)數(shù)”例子,它有一個名為 primes.h 的頭文件,其聲明了四個函數(shù)使得它們對使用庫的 C 程序可見。
/** 頭文件 primes.h:函數(shù)聲明 **/extern unsigned is_prime(unsigned);extern void prime_factors(unsigned);extern unsigned are_coprimes(unsigned, unsigned);extern void goldbach(unsigned);
這些聲明通過為每個函數(shù)指定其調(diào)用語法來作為接口。
為了客戶程序的便利性,頭文件 primes.h 應該存儲在 C 編譯器查找路徑下的目錄中。典型的位置有 /usr/include 和 /usr/local/include。一個 C 語言客戶程序應使用 #include 包含這個頭文件,并盡可能將這條語句其程序源代碼的首部(頭文件將會被導入另一個源文件的“頭”部)。C 語言頭文件可以被導入其他語言(如 Rust 語言)中的 bindgen,使其它語言的客戶程序可以訪問 C 語言的庫。
總之,一個庫函數(shù)只可以被定義一次,但可以在任何需要它的地方進行聲明,任一使用 C 語言庫的程序都需要該聲明。頭文件可以包含函數(shù)聲明,但不能包含函數(shù)定義。如果頭文件包含了函數(shù)定義,那么該文件可能會在一個 C 語言程序中被多次包含,從而破壞了一個函數(shù)在 C 語言程序中必須被精確定義一次的規(guī)則。
庫的源代碼
下面是兩個庫的源代碼。這部分代碼、頭文件、以及兩個示例客戶程序都可以在 我的網(wǎng)頁 上找到。
#include <stdio.h>#include <math.h>extern unsigned is_prime(unsigned n) {if (n <= 3) return n > 1; /* 2 和 3 是質(zhì)數(shù) */if (0 == (n % 2) || 0 == (n % 3)) return 0; /* 2 和 3 的倍數(shù)不會是質(zhì)數(shù) *//* 檢查 n 是否是其他 < n 的值的倍數(shù) */unsigned i;for (i = 5; (i * i) <= n; i += 6)if (0 == (n % i) || 0 == (n % (i + 2))) return 0; /* 不是質(zhì)數(shù) */return 1; /* 一個不是 2 和 3 的質(zhì)數(shù) */}extern void prime_factors(unsigned n) {/* 在數(shù)字 n 的質(zhì)因數(shù)分解中列出所有 2 */while (0 == (n % 2)) {printf("%i ", 2);n /= 2;}/* 數(shù)字 2 已經(jīng)處理完成,下面處理奇數(shù) */unsigned i;for (i = 3; i <= sqrt(n); i += 2) {while (0 == (n % i)) {printf("%i ", i);n /= i;}}/* 還有其他質(zhì)因數(shù)?*/if (n > 2) printf("%i", n);}/* 工具函數(shù):計算最大公約數(shù) */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) {/* 輸入錯誤 */if ((n <= 2) || ((n & 0x01) > 0)) {printf("Number must be > 2 and even: %i is not.\n", n);return;}/* 兩個簡單的例子:4 和 6 */if ((4 == n) || (6 == n)) {printf("%i = %i + %i\n", n, n / 2, n / 2);return;}/* 當 n > 8 時,存在多種可能性 */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);/* 如果只需要一對,那么用 break 語句替換這句 */}}}
庫函數(shù)
這些函數(shù)可以被庫利用。兩個庫可以從相同的源代碼中獲得,同時頭文件 primes.h 是兩個庫的 C 語言接口。
構(gòu)建庫
靜態(tài)庫和動態(tài)庫在構(gòu)建和發(fā)布的步驟上有一些細節(jié)的不同。靜態(tài)庫需要三個步驟,而動態(tài)庫需要增加兩個步驟即一共五個步驟。額外的步驟表明了動態(tài)庫的動態(tài)方法具有更多的靈活性。讓我們先從靜態(tài)庫開始。
庫的源文件 primes.c 被編譯成一個目標模塊。下面是命令,百分號 % 代表系統(tǒng)提示符,兩個井字符 # 是我的注釋。
% gcc -c primes.c ## 步驟1(靜態(tài))
這一步生成目標模塊是二進制文件 primes.o。-c 標志意味著只編譯。
下一步是使用 Linux 的 ar 命令將目標對象歸檔。
% ar -cvq libprimes.a primes.o ## 步驟2(靜態(tài))
-cvq 三個標識分別是“創(chuàng)建”、“詳細的”、“快速添加”(以防新文件沒有添加到歸檔中)的簡稱。回憶一下,前文提到過前綴 lib 是必須的,而庫名是任意的。當然,庫的文件名必須是唯一的,以避免沖突。
歸檔已經(jīng)準備好要被發(fā)布:
% sudo cp libprimes.a /usr/local/lib ## 步驟3(靜態(tài))
現(xiàn)在靜態(tài)庫對接下來的客戶程序是可見的,示例在后面。(包含 sudo 可以確保有訪問權(quán)限將文件復制進 /usr/local/lib 目錄中)
動態(tài)庫還需要一個或多個對象模塊進行打包:
% gcc primes.c -c -fpic ## 步驟1(動態(tài))
增加的選項 -fpic 指示編譯器生成與位置無關(guān)的代碼,這意味著不需要將該二進制模塊加載到一個固定的內(nèi)存位置。在一個擁有多個動態(tài)庫的系統(tǒng)中這種靈活性是至關(guān)重要的。生成的對象模塊會略大于靜態(tài)庫生成的對象模塊。
下面是從對象模塊創(chuàng)建單個庫文件的命令:
% gcc -shared -Wl,-soname,libshprimes.so -o libshprimes.so.1 primes.o ## 步驟2(動態(tài))
選項 -shared 表明了該庫是一個共享的(動態(tài)的)而不是靜態(tài)的。-Wl 選項引入了一系列編譯器選項,第一個便是設(shè)置動態(tài)庫的 -soname,這是必須設(shè)置的。soname 首先指定了庫的邏輯名字(libshprimes.so),接下來的 -o 選項指明了庫的物理文件名字(libshprimes.so.1)。這樣做的目的是為了保持邏輯名不變的同時允許物理名隨著新版本而發(fā)生變化。在本例中,在物理文件名 libshprimes.so.1 中最后的 1 代表是第一個庫的版本。盡管邏輯文件名和物理文件名可以是相同的,但是最佳做法是將它們命名為不同的名字。一個客戶程序?qū)ㄟ^邏輯名(本例中為 libshprimes.so)來訪問庫,稍后我會進一步解釋。
接下來的一步是通過復制共享庫到合適的目錄下使得客戶程序容易訪問,例如 /usr/local/lib 目錄:
% sudo cp libshprimes.so.1 /usr/local/lib ## 步驟3(動態(tài))
現(xiàn)在在共享庫的邏輯名(libshprimes.so)和它的物理文件名(/usr/local/lib/libshprimes.so.1)之間設(shè)置一個符號鏈接。最簡單的方式是將 /usr/local/lib 作為工作目錄,在該目錄下輸入命令:
% sudo ln --symbolic libshprimes.so.1 libshprimes.so ## 步驟4(動態(tài))
邏輯名 libshprimes.so 不應該改變,但是符號鏈接的目標(libshrimes.so.1)可以根據(jù)需要進行更新,新的庫實現(xiàn)可以是修復了 bug,提高性能等。
最后一步(一個預防措施)是調(diào)用 ldconfig 工具來配置系統(tǒng)的動態(tài)加載器。這個配置保證了加載器能夠找到新發(fā)布的庫。
% sudo ldconfig ## 步驟5(動態(tài))
到現(xiàn)在,動態(tài)庫已為包括下面的兩個在內(nèi)的示例客戶程序準備就緒了。
一個使用庫的 C 程序
這個示例 C 程序是一個測試程序,它的源代碼以兩條 #include 指令開始:
#include <stdio.h> /* 標準輸入/輸出函數(shù) */#include <primes.h> /* 我的庫函數(shù) */
文件名兩邊的尖括號表示可以在編譯器的搜索路徑中找到這些頭文件(對于 primes.h 文件來說在 /usr/local/inlcude 目錄下)。如果不包含 #include,編譯器會抱怨缺少 is_prime 和 prime_factors 等函數(shù)的聲明,它們在兩個庫中都有發(fā)布。順便提一句,測試程序的源代碼不需要更改即可測試兩個庫中的每一個庫。
相比之下,庫的源文件(primes.c)使用 #include 指令打開以下頭文件:
#include <stdio.h>#include <math.h>
math.h 頭文件是必須的,因為庫函數(shù) prime_factors 會調(diào)用數(shù)學函數(shù) sqrt,其在標準庫 libm.so 中。
作為參考,這是測試庫程序的源代碼:
#include <stdio.h>#include <primes.h>int main() {/* 是質(zhì)數(shù) */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");/* 是合數(shù) */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");/* 哥德巴赫 */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 文件到可執(zhí)行文件時,難處理的部分時鏈接選項的順序。回想前文中提到兩個示例庫都是用 lib 作為前綴開始,并且每一個都有一個常規(guī)的拓展后綴:.a 代表靜態(tài)庫 libprimes.a,.so 代表動態(tài)庫 libshprimes.so。在鏈接規(guī)范中,前綴 lib 和拓展名被忽略了。鏈接標志以 -l (小寫 L)開始,并且一條編譯命令可能包含多個鏈接標志。下面是一個完整的測試程序的編譯指令,使用動態(tài)庫作為示例:
% gcc -o tester tester.c -lshprimes -lm
第一個鏈接標志指定了庫 libshprimes.so,第二個鏈接標志指定了標準數(shù)學庫 libm.so。
鏈接器是懶惰的,這意味著鏈接標志的順序是需要考慮的。例如,調(diào)整上述實例中的鏈接順序?qū)a(chǎn)生一個編譯時錯誤:
% gcc -o tester tester.c -lm -lshprimes ## 危險!
鏈接 libm.so 庫的標志先出現(xiàn),但是這個庫中沒有函數(shù)被測試程序顯式調(diào)用;因此,鏈接器不會鏈接到 math.so 庫。調(diào)用 sqrt 庫函數(shù)僅發(fā)生在 libshprimes.so 庫中包含的 prime_factors 函數(shù)。編譯測試程序返回的錯誤是:
primes.c: undefined reference to 'sqrt'
因此,鏈接標志的順序應該是通知鏈接器需要 sqrt 函數(shù):
% gcc -o tester tester.c -lshprimes -lm ## 首先鏈接 -lshprimes
鏈接器在 libshprimes.so 庫中發(fā)現(xiàn)了對庫函數(shù) sqrt 的調(diào)用,所以接下來對數(shù)學庫 libm.so做了合適的鏈接。鏈接還有一個更復雜的選項,它支持鏈接的標志順序。然而在本例中,最簡單的方式就是恰當?shù)嘏帕墟溄訕酥尽?/p>
下面是運行測試程序的部分輸出結(jié)果:
is_primeSample prime ending in 1: 101Sample prime ending in 1: 401...168 primes in range of 1 to a thousand.prime_factorsprime factors of 12: 2 2 3prime factors of 13: 13prime factors of 876,512,779: 211 4154089are_coprimeAre 21 and 22 coprime? yesAre 21 and 24 coprime? nogoldbachNumber must be > 2 and even: 11 is not.4 = 2 + 26 = 3 + 3...32 = 3 + 2932 = 13 + 19...100 = 3 + 97100 = 11 + 89...
對于 goldbach 函數(shù),即使一個相當小的偶數(shù)值(例如 18)也許存在多個一對質(zhì)數(shù)之和的組合(在這種情況下,5+13 和 7+11)。因此這種多個質(zhì)數(shù)對是使得嘗試證明哥德巴赫猜想變得復雜的因素之一。
封裝使用庫的 Python 程序
與 C 不同,Python 不是一個靜態(tài)編譯語言,這意味著 Python 客戶示例程序必須訪問動態(tài)版本而非靜態(tài)版本的 primes 庫。為了能這樣做,Python 中有眾多的支持外部語言接口(FFI)的模塊(標準的或第三方的),它們允許用一種語言編寫的程序來調(diào)用另一種語言編寫的函數(shù)。Python 中的 ctypes 是一個標準的、相對簡單的允許 Python 代碼調(diào)用 C 函數(shù)的 FFI。
任何 FFI 都面臨挑戰(zhàn),因為對接的語言不大可能會具有完全相同的數(shù)據(jù)類型。例如:primes 庫使用 C 語言類型 unsigned int,而 Python 并不具有這種類型;因此 ctypes FFI 將 C 語言中的 unsigned int 類型映射為 Python 中的 int 類型。在 primes 庫中發(fā)布的四個 extern C 函數(shù)中,有兩個在具有顯式 ctypes 配置的 Python 中會表現(xiàn)得更好。
C 函數(shù) prime_factors 和 goldbach 返回 void 而不是返回一個具體類型,但是 ctypes 默認會將 C 語言中的 void 替換為 Python 語言中的 int。當從 Python 代碼中調(diào)用時,這兩個 C 函數(shù)會從棧中返回一個隨機整數(shù)值(因此,該值無任何意義)。然而,可以對 ctypes 進行配置,讓這些函數(shù)返回 None (Python 中為 null 類型)。下面是對 prime_factors 函數(shù)的配置:
primes.prime_factors.restype = None
可以用類似的語句處理 goldbach 函數(shù)。
下面的交互示例(在 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 + 2932 = 13 + 19
在 primes 庫中的函數(shù)只使用一個簡單數(shù)據(jù)類型:unsigned int。如果這個 C 語言庫使用復雜的類型如結(jié)構(gòu)體,如果庫函數(shù)傳遞和返回指向結(jié)構(gòu)體的指針,那么比 ctypes 更強大的 FFI 更適合作為一個在 Python 語言和 C 語言之間的平滑接口。盡管如此,ctypes 示例展示了一個 Python 客戶程序可以使用 C 語言編寫的庫。值得注意的是,用作科學計算的流行的 Numpy 庫是用 C 語言編寫的,然后在高級 Python API 中公開。
簡單的 primes 庫和高級的 Numpy 庫強調(diào)了 C 語言仍然是編程語言中的通用語言。幾乎每一個語言都可以與 C 語言交互,同時通過 C 語言也可以和任何其他語言交互。Python 很容易和 C 語言交互,作為另外一個例子,當 Panama 項目 成為 Java Native Interface(JNI)一個替代品后,Java 語言和 C 語言交互也會變的很容易。






















