從 Rust 調(diào)用 C 庫函數(shù)
為什么要從 Rust 調(diào)用 C 函數(shù)?簡(jiǎn)短的答案就是軟件庫。冗長(zhǎng)的答案則觸及到 C 在眾多編程語言中的地位,特別是相對(duì) Rust 而言。C、C++,還有 Rust 都是系統(tǒng)語言,這意味著程序員可以訪問機(jī)器層面的數(shù)據(jù)類型與操作。在這三個(gè)系統(tǒng)語言中,C 依然占據(jù)主導(dǎo)地位?,F(xiàn)代操作系統(tǒng)的內(nèi)核主要是用 C 來寫的,其余部分依靠匯編語言補(bǔ)充。在標(biāo)準(zhǔn)系統(tǒng)函數(shù)庫中,輸入與輸出、數(shù)字處理、加密計(jì)算、安全、網(wǎng)絡(luò)、國際化、字符串處理、內(nèi)存管理等等,大多都是用 C 來寫的。這些函數(shù)庫所代表的是一個(gè)龐大的基礎(chǔ)設(shè)施,支撐著用其他語言寫出來的應(yīng)用。Rust 發(fā)展至今也有著可觀的函數(shù)庫,但是 C 的函數(shù)庫 —— 自 1970 年代就已存在,迄今還在蓬勃發(fā)展 —— 是一種無法被忽視的資源。最后一點(diǎn)是,C 依然還是編程語言中的 ??通用語??:大部分語言都可以與 C 交流,透過 C,語言之間可以互相交流。
兩個(gè)概念證明的例子
Rust 支持 FFI(外部函數(shù)接口Foreign Function Interface)用以調(diào)用 C 函數(shù)。任何 FFI 所需要面臨的問題是調(diào)用方語言是否涵蓋了被調(diào)用語言的數(shù)據(jù)類型。例如,??ctypes?
?? 是 Python 調(diào)用 C 的 FFI,但是 Python 并沒有包括 C 所支持的無符號(hào)整數(shù)類型。結(jié)果就是,??ctypes?
? 必須尋求解決方案。
相比之下,Rust 包含了所有 C 中的原始(即,機(jī)器層面)類型。比如說,Rust 中的 ??i32?
?? 類對(duì)應(yīng) C 中的 ??int?
?? 類。C 特別聲明了 ??char?
?? 類必須是一個(gè)字節(jié)大小,而其他類型,比如 ??int?
??,必須至少是這個(gè)大?。↙CTT 譯注:原文處有評(píng)論指出 ??int?
?? 大小依照 C 標(biāo)準(zhǔn)應(yīng)至少為 2 字節(jié));然而如今所有合理的 C 編譯器都支持四字節(jié)的 ??int?
??,以及八字節(jié)的 ??double?
??(Rust 中則是 ??f64?
? 類),以此類推。
針對(duì) C 的 FFI 所面臨的另一個(gè)挑戰(zhàn)是:FFI 是否能夠處理 C 的裸指針,包括指向被看作是字符串的數(shù)組指針。C 沒有字符串類型,它通過結(jié)合字符組和一個(gè)非打印終止符(大名鼎鼎的 空終止符)來實(shí)現(xiàn)字符串。相比之下,Rust 有兩個(gè)字符串類型:??String?
?? 和 ??&str?
? (字符串切片)。問題是,Rust FFI 是否能將 C 字符串轉(zhuǎn)化成 Rust 字符串——答案是 肯定的。
出于對(duì)效率的追求,結(jié)構(gòu)體指針在 C 中也很常見。一個(gè) C 結(jié)構(gòu)體在作為一個(gè)函數(shù)的參數(shù)或者返回值的時(shí)候,其默認(rèn)行為是傳遞值(即,逐字節(jié)復(fù)制)。C 結(jié)構(gòu)體,如同它在 Rust 中的對(duì)應(yīng)部分一樣,可以包含數(shù)組和嵌套其他結(jié)構(gòu)體,所以其大小是不定的。結(jié)構(gòu)體在兩種語言中的最佳用法是傳遞或返回引用,也就是說,傳遞或返回結(jié)構(gòu)體的地址而不是結(jié)構(gòu)體本身的副本。Rust FFI 再一次成功處理了 C 的結(jié)構(gòu)體指針,其在 C 函數(shù)庫中十分普遍。
第一段代碼案例專注于調(diào)用相對(duì)簡(jiǎn)單的 C 庫函數(shù),比如 ??abs?
??(絕對(duì)值)和 ??sqrt?
??(平方根)。這些函數(shù)使用非指針標(biāo)量參數(shù)并返回一個(gè)非指針標(biāo)量值。第二段代碼案例則涉及了字符串和結(jié)構(gòu)體指針,在這里會(huì)介紹工具 ??bindgen???,其通過 C 接口(頭文件)生成 Rust 代碼,比如 ??math.h?
?? 以及 ??time.h?
??。C 頭文件聲明了 C 函數(shù)的調(diào)用語法,并定義了會(huì)被調(diào)用的結(jié)構(gòu)體。兩段代碼都能在 ??我的主頁上?? 找到。
調(diào)用相對(duì)簡(jiǎn)單的 C 函數(shù)
第一段代碼案例有四處 Rust 對(duì)標(biāo)準(zhǔn)數(shù)學(xué)庫內(nèi)的 C 函數(shù)的調(diào)用:兩處分別調(diào)用了 ??abs?
??(絕對(duì)值)和 ??pow?
??(冪),兩處重復(fù)調(diào)用了 ??sqrt?
??(平方根)。這個(gè)程序可以直接用 ??rustc?
?? 編譯器進(jìn)行構(gòu)建,或者使用更方便的命令 ??cargo build?
?:
頂部的兩個(gè) ??use?
?? 聲明是 Rust 的數(shù)據(jù)類型 ??c_int?
?? 和 ??c_double?
??,對(duì)應(yīng) C 類型里的 ??int?
?? 和 ??double?
??。Rust 標(biāo)準(zhǔn)模塊 ??std::os::raw?
?? 定義了 14 個(gè)類似的類型以確保跟 C 的兼容性。模塊 ??std::ffi?
? 中有 14 個(gè)同樣的類型定義,以及對(duì)字符串的支持。
位于 ??main?
?? 函數(shù)上的 ??extern "C"?
?? 區(qū)域聲明了 3 個(gè) C 庫函數(shù),這些函數(shù)會(huì)在 ??main?
?? 函數(shù)內(nèi)被調(diào)用。每次調(diào)用都使用了標(biāo)準(zhǔn)的 C 函數(shù)名,但每次調(diào)用都必須發(fā)生在一個(gè) ??unsafe?
?? 區(qū)域內(nèi)。正如每個(gè)新接觸 Rust 的程序員所發(fā)現(xiàn)的那樣,Rust 編譯器極度強(qiáng)制內(nèi)存安全。其他語言(特別是 C 和 C++)作不出相同的保證。??unsafe?
? 區(qū)域其實(shí)是說:Rust 對(duì)外部調(diào)用中可能存在的不安全行為不負(fù)責(zé)。
第一個(gè)程序輸出為:
輸出的最后一行的 ??NaN?
?? 表示不是數(shù)字Not a Number:C 庫函數(shù) ??sqrt?
?? 期待一個(gè)非負(fù)值作為參數(shù),這使得參數(shù) ??-3.14?
?? 生成了 ??NaN?
? 作為返回值。
調(diào)用涉及指針的 C 函數(shù)
C 庫函數(shù)為了提高效率,經(jīng)常在安全、網(wǎng)絡(luò)、字符串處理、內(nèi)存管理,以及其他領(lǐng)域中使用指針。例如,庫函數(shù) ??asctime?
??(ASCII 字符串形式的時(shí)間)期待一個(gè)結(jié)構(gòu)體指針作為其參數(shù)。Rust 調(diào)用類似 ??asctime?
?? 的 C 函數(shù)就會(huì)比調(diào)用 ??sqrt?
? 要更加棘手一些,后者既沒有牽扯到指針,也不涉及到結(jié)構(gòu)體。
函數(shù) ??asctime?
?? 調(diào)用的 C 結(jié)構(gòu)體類型為 ??struct tm?
??。一個(gè)指向此結(jié)構(gòu)體的指針會(huì)作為參數(shù)被傳遞給庫函數(shù) ??mktime?
??(時(shí)間作為值)。此結(jié)構(gòu)體會(huì)將時(shí)間拆分成諸如年、月、小時(shí)之類的單位。此結(jié)構(gòu)體的字段field類型為 ??time_t?
??,是 ??int?
??(32位)和 ??long?
??(64 位)的別名。兩個(gè)庫函數(shù)將這些破碎的時(shí)間片段組合成了一個(gè)單一值:??asctime?
?? 返回一個(gè)以字符串表示的時(shí)間,而 ??mktime?
?? 返回一個(gè) ??time_t?
?? 值表示自 “??紀(jì)元??Epoch
以下的 C 程序調(diào)用了 ??asctime?
?? 和 ??mktime?
??,并使用了其他庫函數(shù) ??strftime?
?? 來將 ??mktime?
? 的返回值轉(zhuǎn)化成一個(gè)格式化的字符串。這個(gè)程序可被視作 Rust 對(duì)應(yīng)版本的預(yù)熱:
程序輸出為:
(LCTT 譯注:如果你嘗試在自己電腦上運(yùn)行這段代碼,然后得到了一行關(guān)于 ??mktime?
?? 的錯(cuò)誤信息,然后又在網(wǎng)上隨便找了個(gè)在線 C 編譯器,復(fù)制代碼然后得到了跟這里的結(jié)果有區(qū)別但是沒有錯(cuò)誤的結(jié)果,不要慌,我的電腦上也是這樣的。導(dǎo)致本地機(jī)器上 ??mktime?
?? 失敗的原因是作者沒有設(shè)置 ??tm_isdst?
??,這個(gè)是用來標(biāo)記夏令時(shí)的標(biāo)志。??tm_isdst???。加入 ??sometime.tm_isdst = 0?
?? 或 ??= -1?
?? 后應(yīng)該就能得到跟在線編譯器大致一樣的結(jié)果。不同的地方在于結(jié)果第一行我得到的是 ??Mon Feb ...?
??,這個(gè)與作者代碼中 ??sometime.tm_wday = 1?
? 對(duì)應(yīng),這里應(yīng)該是作者寫錯(cuò)了;第二行我和作者和網(wǎng)上得到的數(shù)字都不一樣,這大概是合理的,因?yàn)檫@與機(jī)器的紀(jì)元有關(guān);第三行我跟作者的結(jié)果是一樣的,1901 年 2 月 1 日也確實(shí)是周五,這是因?yàn)?nbsp;??mktime???。至于夏令時(shí)具體是如何影響 ??mktime?
?? 這個(gè)問題,我能查到的只有 ??mktime?
? 的計(jì)算受時(shí)區(qū)影響,更底層的原因我也不知道了。)
總的來說,Rust 在調(diào)用庫函數(shù) ??asctime?
?? 和 ??mktime?
? 時(shí),必須處理以下兩個(gè)問題:
- 將裸指針作為唯一參數(shù)傳遞給每個(gè)庫函數(shù)。
- 把從?
?asctime?
? 返回的 C 字符串轉(zhuǎn)化為 Rust 字符串。
Rust 調(diào)用 asctime 和 mktime
工具 ??bindgen?
?? 會(huì)根據(jù)類似 ??math.h?
?? 和 ??time.h?
?? 之類的 C 頭文件生成 Rust 支持的代碼。下面這個(gè)簡(jiǎn)化版的 ??time.h?
? 就可以用來做例子,簡(jiǎn)化版與原版主要有兩個(gè)不同:
- 內(nèi)置類型?
?int?
?? 被用來取代別名類型??time_t?
??。工具 bindgen 可以處理??time_t?
?? 類,但是會(huì)生成一些煩人的警告,因?yàn)??time_t?
?? 不符合 Rust 的命名規(guī)范:??time_t?
?? 以下劃線區(qū)分??time?
?? 和??t?
??;Rust 更偏好駝峰式命名方法,比如??TimeT?
?。 - 出于同樣的原因,這里選擇?
?StructTM?
?? 作為??struct tm?
? 的別名。
以下是一份簡(jiǎn)化版的頭文件,??mktime?
?? 和 ??asctime?
? 在文件底部:
??bindgen?
?? 安裝好后,??mytime.h?
?? 作為以上提到的頭文件,以下命令(??%?
?? 是命令行提示符)可以生成所需的 Rust 代碼并將其保存到文件 ??mytime.rs?
?:
以下是 ??mytime.rs?
? 中的重要部分:
Rust 結(jié)構(gòu)體 ??struct tm?
??,跟原本在 C 中的一樣,包含了 9 個(gè) 4 字節(jié)的整型字段。這些字段名稱在 C 和 Rust 中是一樣的。??extern "C"?
?? 區(qū)域聲明了庫函數(shù) ??astime?
?? 和 ??mktime?
?? 分別需要只一個(gè)參數(shù),一個(gè)指向可變實(shí)例 ??StructTM?
? 的裸指針。(庫函數(shù)可能會(huì)通過指針改變作為參數(shù)傳遞的結(jié)構(gòu)體。)
??#[test]?
?? 屬性下的其余代碼是用來測(cè)試 Rust 版的時(shí)間結(jié)構(gòu)體的布局。通過命令 ??cargo test?
?? 可以進(jìn)行這些測(cè)試。問題在于,C 沒有規(guī)定編譯器應(yīng)該如何對(duì)結(jié)構(gòu)體中的字段進(jìn)行布局。比如說,C 的 ??struct tm?
?? 以字段 ??tm_sec?
? 開頭用以表示秒;但是 C 不需要編譯版本遵循這個(gè)排序。不管怎樣,Rust 測(cè)試應(yīng)該會(huì)成功,而 Rust 對(duì)庫函數(shù)的調(diào)用也應(yīng)如預(yù)期般工作。
設(shè)置好第二個(gè)案例并開始運(yùn)行
從 ??bindgen?
?? 生成的代碼不包含 ??main?
?? 函數(shù),所以是一個(gè)天然的模塊。以下是一個(gè) ??main?
?? 函數(shù)初始化了 ??StructTM?
?? 并調(diào)用了 ??asctime?
?? 和 ??mktime?
?:
這段 Rust 代碼可以被編譯(直接用 ??rustc?
?? 或使用 ??cargo?
?)并運(yùn)行。輸出為:
對(duì) C 函數(shù) ??asctime?
?? 和 ??mktime?
?? 的調(diào)用必須再一次被放在 ??unsafe?
?? 區(qū)域內(nèi),因?yàn)?Rust 編譯器無法對(duì)這些外部函數(shù)的潛在內(nèi)存安全風(fēng)險(xiǎn)負(fù)責(zé)。此處聲明一下,??asctime?
?? 和 ??mktime?
?? 并沒有安全風(fēng)險(xiǎn)。調(diào)用的兩個(gè)函數(shù)的參數(shù)是裸指針 ??ptr?
??,其指向結(jié)構(gòu)體 ??sometime?
? (在棧stack中)的地址。
??asctime?
?? 是兩個(gè)函數(shù)中調(diào)用起來更棘手的那個(gè),因?yàn)檫@個(gè)函數(shù)返回的是一個(gè)指向 C ??char?
?? 的指針,如果函數(shù)返回 ??Mon?
?? 那么指針就指向 ??M?
??。但是 Rust 編譯器并不知道 C 字符串 (??char?
?? 的空終止數(shù)組)的儲(chǔ)存位置。是內(nèi)存里的靜態(tài)空間?還是堆heap???asctime?
? 函數(shù)內(nèi)用來儲(chǔ)存時(shí)間的文字表達(dá)的數(shù)組實(shí)際上是在內(nèi)存的靜態(tài)空間里。無論如何,C 到 Rust 字符串轉(zhuǎn)化需要兩個(gè)步驟來避免編譯錯(cuò)誤:
- 調(diào)用?
?Cstr::from_ptr(char_ptr)?
?? 來將 C 字符串轉(zhuǎn)化為 Rust 字符串并返回一個(gè)引用儲(chǔ)存在變量??c_str?
? 中。 - 對(duì)?
?c_str.to_str()?
?? 的調(diào)用確保了??c_str?
? 是所有者。
Rust 代碼不會(huì)增加從 ??mktime?
?? 返回的整型值的易讀性,這一部分留作課外作業(yè)給感興趣的人去探究。Rust 模板 ??chrono::format?
?? 也有一個(gè) ??strftime?
? 函數(shù),它可以被當(dāng)作 C 的同名函數(shù)來使用,兩者都是獲取時(shí)間的文字表達(dá)。
使用 FFI 和 bindgen 調(diào)用 C
Rust FFI 和工具 ??bindgen?
?? 都能夠出色地協(xié)助 Rust 調(diào)用 C 庫,無論是標(biāo)準(zhǔn)庫還是第三方庫。Rust 可以輕松地與 C 交流,并透過 C 與其他語言交流。對(duì)于調(diào)用像 ??sqrt?
? 一樣簡(jiǎn)單的庫函數(shù),Rust FFI 表現(xiàn)直截了當(dāng),這是因?yàn)?Rust 的原始數(shù)據(jù)類型覆蓋了它們?cè)?C 中的對(duì)應(yīng)部分。
對(duì)于更為復(fù)雜的交流 —— 特別是 Rust 調(diào)用像 ??asctime?
?? 和 ??mktime?
?? 一樣,會(huì)涉及到結(jié)構(gòu)體和指針的 C 庫函數(shù) —— ??bindgen?
?? 工具是優(yōu)秀的幫手。這個(gè)工具會(huì)生成支持代碼以及所需要的測(cè)試。當(dāng)然,Rust 編譯器無法假設(shè) C 代碼對(duì)內(nèi)存安全的考慮會(huì)符合 Rust 的標(biāo)準(zhǔn);因此,Rust 必須在 ??unsafe?
? 區(qū)域內(nèi)調(diào)用 C。