編碼規範¶
本文件描述瞭如何在核心中編寫 Rust 程式碼。
樣式 & 格式¶
程式碼應使用 rustfmt 格式化。 這樣,偶爾為核心做貢獻的人就不需要學習和記住另一個風格指南。 更重要的是,審閱者和維護者不再需要花費時間指出樣式問題,因此可能需要更少的補丁往返才能提交更改。
注意
關於註釋和文件的約定不受 rustfmt 的檢查。 因此,仍然需要注意這些。
使用 rustfmt 的預設設定。 這意味著遵循慣用的 Rust 風格。 例如,縮排使用 4 個空格而不是製表符。
指示編輯器/IDE 在輸入、儲存或提交時進行格式化很方便。 但是,如果由於某種原因需要在某個時候重新格式化整個核心 Rust 原始碼,則可以執行以下命令
make LLVM=1 rustfmt
也可以檢查所有內容是否已格式化(否則列印差異),例如對於 CI,可以使用
make LLVM=1 rustfmtcheck
就像核心其餘部分的 clang-format 一樣,rustfmt 對單個檔案起作用,並且不需要核心配置。 有時它甚至可以處理損壞的程式碼。
程式碼文件¶
Rust 核心程式碼的文件記錄方式與 C 核心程式碼不同(即透過 kernel-doc)。 相反,使用了記錄 Rust 程式碼的常用系統:rustdoc 工具,它使用 Markdown(一種輕量級標記語言)。
要學習 Markdown,有很多可用的指南。 例如,在
這是一個文件完善的 Rust 函式的樣子
/// Returns the contained [`Some`] value, consuming the `self` value,
/// without checking that the value is not [`None`].
///
/// # Safety
///
/// Calling this method on [`None`] is *[undefined behavior]*.
///
/// [undefined behavior]: https://doc.rust-lang.org/reference/behavior-considered-undefined.html
///
/// # Examples
///
/// ```
/// let x = Some("air");
/// assert_eq!(unsafe { x.unwrap_unchecked() }, "air");
/// ```
pub unsafe fn unwrap_unchecked(self) -> T {
match self {
Some(val) => val,
// SAFETY: The safety contract must be upheld by the caller.
None => unsafe { hint::unreachable_unchecked() },
}
}
此示例展示了一些 rustdoc 功能和核心中遵循的一些約定
第一段必須是一個簡單的一句話,簡要描述記錄的專案的作用。 進一步的解釋必須放在額外的段落中。
不安全的函式必須在其
# Safety部分下記錄其安全前提條件。雖然此處未顯示,但如果函式可能發生 panic,則必須在
# Panics部分下描述發生這種情況的條件。請注意,panic 應該非常罕見,並且僅在有充分理由時才使用。 在幾乎所有情況下,都應使用易錯方法,通常返回
Result。如果提供用法示例可以幫助讀者,則必須將它們寫在一個名為
# Examples的部分中。必須適當地連結 Rust 專案(函式、型別、常量...)(
rustdoc將自動建立一個連結)。任何
unsafe塊必須以// SAFETY:註釋開頭,描述為什麼內部的程式碼是健全的。雖然有時原因可能看起來微不足道,因此不需要,但編寫這些註釋不僅是記錄已考慮事項的好方法,而且最重要的是,它提供了一種知道沒有 *額外的* 隱式約束的方法。
要了解有關如何編寫 Rust 文件和額外功能的更多資訊,請檢視 rustdoc 書籍,網址為
此外,核心支援建立相對於源樹的連結,方法是在連結目標前面加上 srctree/。 例如
//! C header: [`include/linux/printk.h`](srctree/include/linux/printk.h)
或
/// [`struct mutex`]: srctree/include/linux/mutex.h
C FFI 型別¶
Rust 核心程式碼使用類型別名(例如 c_int)引用 C 型別(例如 int),這些類型別名可從 kernel prelude 中輕鬆獲得。 請不要使用來自 core::ffi 的別名——它們可能無法對映到正確的型別。
這些別名通常應透過其識別符號直接引用,即作為單個段路徑。 例如
fn f(p: *const c_char) -> c_int {
// ...
}
命名¶
Rust 核心程式碼遵循常見的 Rust 命名約定
當現有的 C 概念(例如宏、函式、物件...)被包裝到 Rust 抽象中時,應使用盡可能接近 C 端的名字,以避免混淆,並在 C 和 Rust 端來回切換時提高可讀性。 例如,來自 C 的宏(例如 pr_info)在 Rust 端使用相同的名稱。
話雖如此,應調整大小寫以遵循 Rust 命名約定,並且不應在專案名稱中重複由模組和型別引入的名稱空間。 例如,包裝像這樣的常量時
#define GPIO_LINE_DIRECTION_IN 0
#define GPIO_LINE_DIRECTION_OUT 1
Rust 中的等效項可能如下所示(忽略文件)
pub mod gpio {
pub enum LineDirection {
In = bindings::GPIO_LINE_DIRECTION_IN as _,
Out = bindings::GPIO_LINE_DIRECTION_OUT as _,
}
}
也就是說,GPIO_LINE_DIRECTION_IN 的等效項將稱為 gpio::LineDirection::In。 特別是,它不應命名為 gpio::gpio_line_direction::GPIO_LINE_DIRECTION_IN。
Lints¶
在 Rust 中,可以本地 allow 特定警告(診斷、lints),使編譯器忽略給定函式、模組、塊等中給定的警告例項。
它類似於 C 中的 #pragma GCC diagnostic push + ignored + pop [1]
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"
static void f(void) {}
#pragma GCC diagnostic pop
但是簡潔得多
#[allow(dead_code)]
fn f() {}
憑藉這一優點,它可以舒適地預設啟用更多診斷(即在 W= 級別之外)。 特別是,那些可能有少量誤報,但在其他方面非常有用,可以保持啟用以捕獲潛在的錯誤。
最重要的是,Rust 提供了 expect 屬性,它更進一步。 如果未產生警告,它會使編譯器發出警告。 例如,以下將確保,當在某處呼叫 f() 時,我們將不得不刪除該屬性
#[expect(dead_code)]
fn f() {}
如果我們不這樣做,我們會收到來自編譯器的警告
warning: this lint expectation is unfulfilled
--> x.rs:3:10
|
3 | #[expect(dead_code)]
| ^^^^^^^^^
|
= note: `#[warn(unfulfilled_lint_expectations)]` on by default
這意味著當不需要 expect 時,它們不會被遺忘,這可能發生在幾種情況下,例如
開發時新增的臨時屬性。
編譯器、Clippy 或自定義工具中的 lint 改進,可能會消除誤報。
由於預計會在某個時候刪除 lint,因此當不再需要 lint 時,例如上面的
dead_code示例。
它還提高了其餘 allow 的可見性,並減少了錯誤應用一個的機會。
因此,除非出現以下情況,否則請首選 expect 而不是 allow
條件編譯在某些情況下會觸發警告,但在其他情況下則不會。
如果與總案例數相比,只有少數案例觸發(或不觸發)警告,則可以考慮使用條件
expect(即cfg_attr(..., expect(...)))。 否則,很可能只是使用allow更簡單。在宏內部,當不同的呼叫可能建立擴充套件程式碼,該程式碼在某些情況下會觸發警告,而在其他情況下則不會。
當代碼可能為某些架構觸發警告,而為其他架構觸發警告時,例如
as強制轉換為 C FFI 型別。
作為一個更完善的例子,例如考慮以下程式
fn g() {}
fn main() {
#[cfg(CONFIG_X)]
g();
}
在這裡,如果未設定 CONFIG_X,則函式 g() 是 dead code。 我們可以在這裡使用 expect 嗎?
#[expect(dead_code)]
fn g() {}
fn main() {
#[cfg(CONFIG_X)]
g();
}
如果設定了 CONFIG_X,這將發出一個 lint,因為它在該配置中不是 dead code。 因此,在這種情況下,我們不能按原樣使用 expect。
一種簡單的可能性是使用 allow
#[allow(dead_code)]
fn g() {}
fn main() {
#[cfg(CONFIG_X)]
g();
}
另一種選擇是使用條件 expect
#[cfg_attr(not(CONFIG_X), expect(dead_code))]
fn g() {}
fn main() {
#[cfg(CONFIG_X)]
g();
}
這將確保,如果有人在某處引入對 g() 的另一個呼叫(例如,無條件),那麼就會發現它不再是 dead code。 但是,cfg_attr 比簡單的 allow 更復雜。
因此,當涉及多個或兩個以上的配置,或者由於非本地更改(例如 dead_code)可能觸發 lint 時,可能不值得使用條件 expect。
有關 Rust 中診斷的更多資訊,請參閱
錯誤處理¶
有關 Rust for Linux 特定錯誤處理的一些背景資訊和指南,請參閱
註釋¶
“普通”註釋(即
//,而不是以///或//!開頭的程式碼文件)以 Markdown 編寫,就像文件註釋一樣,即使它們不會被呈現。 這提高了程式碼一致性,簡化了規則,並允許更輕鬆地在這兩種註釋之間移動內容。 例如此外,就像文件一樣,註釋在句首大寫,並以句點結尾(即使它是一個句子)。 這包括
// SAFETY:,// TODO:和其他“標記”註釋,例如// FIXME: The error should be handled properly.註釋不應用於文件目的:註釋旨在用於實現細節,而不是使用者。 即使原始檔的讀者既是 API 的實現者又是使用者,這種區別也很有用。 事實上,有時同時使用註釋和文件是有用的。 例如,對於
TODO列表或評論文件本身。 對於後一種情況,可以將註釋插入中間; 也就是說,更靠近要註釋的文件行。 對於任何其他情況,註釋都寫在文件之後,例如這適用於公共和私有專案。 這增加了與公共專案的一致性,允許以更少的更改參與更改可見性,並且將允許我們潛在地為私有專案生成文件。 換句話說,如果為私有專案編寫文件,則仍應使用
///。 例如一種特殊的註釋是
// SAFETY:註釋。 這些必須出現在每個unsafe塊之前,並且它們解釋了為什麼塊內的程式碼是正確的/健全的,即為什麼它在任何情況下都不會觸發未定義的行為,例如// SAFETY:註釋不應與程式碼文件中的# Safety部分混淆。# Safety部分指定了呼叫者(對於函式)或實現者(對於特徵)需要遵守的約定。// SAFETY:註釋顯示了為什麼呼叫(對於函式)或實現(對於特徵)實際上尊重# Safety部分或語言參考中陳述的先決條件。