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


咨詢(xún)
建站咨詢(xún)

