Netstack 團隊's Rust 模式

本文件列舉 netstack 團隊已找到的模式:

  • 讓程式碼更能抵禦遠處的行為變化。
  • 更容易審查的程式碼,減少「工作記憶體」。

這些規範視為 Rust API 評分量表的附錄。

此處建議的模式會提供集中式指引和知識。我們建議提供所有類型的貢獻,例如編輯、新增、移除等。

在按照建議指南編寫或查看程式碼的過程中,遇到的邊緣應會回到本文件中,以便我們能夠視需要編寫替代模式。

避免未使用的結果

https://fxrev.dev/510442 起,這主要是透過 rustc 的 unused-results lint 受到機器強制執行。如需原因的說明,請參閱說明文件

捨棄結果時,請對忽略的欄位類型進行編碼,使其成為合約的一部分。

當系統未立即從類型中清除語意含義,也不會影響 drop 語意時,捨棄結果時建議使用以 _ 為開頭的已命名變數。

這些新增的資訊可用來提示作者和評論者:

  • 是否應使用傳回值檢查非變化版本?
  • 這個函式應該完全傳回值嗎?

注意:過去,團隊在指派作業中使用 let () = 表單做為陳述式,表示不會捨棄任何資訊。系統仍然接受這項做法,但導入 Lint 就不再需要該做法。

範例

善用提示

// Bad. The dropped type is unknown without knowing the signature of write.
let _ = writer.write(payload);
let _n = writer.write(payload);

// Okay. The dropped type is locally visible and enforced. Absence of invariant
// enforcement such as a check for partial writes is obvious and can be flagged
// in code review.
let _: usize = writer.write(payload);
let _n: usize = writer.write(payload);

// Good.
let n = writer.write(payload);
assert_eq!(n, payload.len());

採用的捨棄格式

// Encode the type of the ignored return value.
let _: Foo = foo::do_work(foo, bar, baz);

// When destructuring tuples, type the fields not in use.
let (foo, _) : (_, Bar) = foo::foo_and_bar();

// When destructuring named structs, no annotations are necessary, the field's
// type is encoded by the struct.
let Foo{ fi, fo: _ } =  foo::do_work(foo, bar, baz);

// Encode the most specific type possible.
let _: Option<_> = foo.maybe_get_trait_impl(); // Can't name opaque type.
let _: Option<usize> = foo.maybe_get_len(); // Can name concrete type.

// Use best judgement for unwieldy concrete types.
// If the type expands to formatted code that would span >= 5 lines of type
// annotation, use best judgement to cut off the signature.
let _: futures::future::Ready<Result<Buffer<_>>, anyhow::Error> = y();
// Prefer to specify if only a couple of levels of nesting and < 5 lines.
let _: Result<
    Result<(bool, usize), fidl_fuchsia_net_interfaces_admin::AddressRemovalError>,
    fidl::Error,
> = x();

擊敗模式

留意戒除模式:

// Bad, this is a drop that does not encode the type.
std::mem::drop(foo());
// Prefer instead:
let _: Foo = foo();

測試

宣告

為測試命名,其名稱不含 test_ 前置字串。這就是 Rust 標準程式庫採用的模式。

如果測試名稱不足以對測試目標編碼,請在名稱前面或函式主體頂端加上簡短的非文件註解,說明這項測試的運動內容。我們會使用非文件註解,因為我們預期目標對象會是程式碼讀取者,而非公用 API。

測試應一律位於名為 tests 的模組或其子系中。只包含整合測試的 Crate 不需要 tests 模組,例如 network/tests/integrationnetwork/tests/fidl

例子:

// Tests Controller::do_work error returns.
#[test]
fn controller_do_work_errors() { /* ... */ }

測試支援函式應位於名為 testutil 的模組中。如果模組僅供內部使用,則應宣告 pub(crate)#[cfg(test)]。這個模組不應包含任何 #[test] 函式。如果 testutil 模組中的功能需要測試,則應建立名為 tests 的子模組 (例如testutil::tests).

偏好恐慌

不使用使用 Rust 的支援功能傳回結果;這類測試不會自動傳回錯誤,而是自動傳回錯誤。不會發出反向追蹤記錄的測試失敗通常會更難解讀。撰寫本文時,Rust 中的回溯追蹤功能功能不穩定在 Fuchsia Rust 建構設定中停用,但即使啟用後並未包含所有錯誤,仍會包含回溯追蹤記錄;建議您盡可能防堵反向追蹤的外部因素。

匯入項目

下列規則適用於樣式匯入作業。

為每個 Crate 或直接子項模組使用一個 use 陳述式。將這些子項合併到相同的使用陳述式中。

use child_mod::{Foo, Bar};
use futures::{
    stream::{self, futures_unordered::FuturesUnordered},
    Future, Stream,
};

一律為 _ 匯入但未參照的別名。這樣可避免命名空間過於雜亂,並告知讀取器系統並未參照 ID。

use futures::FutureExt as _;

避免將其他 Crate 中的符號帶入範圍。尤其是如果事物的名稱都很類似例外狀況適用於廣為使用的 std 類型,例如 HashMapHashSet 等。

一律允許從相同 Crate 匯入符號,包括遵循宣告 Crate-local ErrorResult 類型的模式。

部分 Crate 極度依賴外部類型。如果預期可在整個 Crate 期間使用

// DON'T. Parsing this module quickly grows out of hand since it's hard to make
// sense where types are coming from.
mod confusing_mod {
    use packet_formats::IpAddress;
    use crate::IpAddr;
    use fidl_fuchsia_net::Ipv4Address;
}

// DO. Import only types from this crate.
mod happy_mod {
    use crate::IpAddress;

    fn foo() -> packet_formats::IpAddress { /* .. */ }
    fn bar() -> IpAddress { /* .. */ }
    fn baz() -> fidl_fuchsia_net::Ipv4Address { /* .. */ }
}

某些知名的 Crate 採用別名或別名建立規則。這些轉換動作包括:

  • 可將 fuchsia_async 的別名設為 fasync
  • 可將 fuchsia_zircon 的別名設為 zx
  • 可將 fidl_fuchsia_* 前置字串的別名設為 f*,例如:
    • use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin;
    • use fidl_fuchsia_net_routes as fnet_routes;

強烈建議不要匯入 *。從 super 匯入的測試模組為可接受的例外狀況;一般假設在父項中並未宣告所有符號,測試模組將最常使用。

系統一律允許在函式主體中執行匯入類型,因為這樣很容易在本機判斷類型來源。

fn do_stuff() {
    use some::deeply::nested::{Foo, bar::Bar};
    let x = Foo::new();
    let y = Bar::new();
    /* ... */
}

偏好完全相符的項目

盡可能詳盡比對,避免使用全部接收的模式。比對時,在審查期間可以提供更多本機背景資訊、在列舉更新更新時提示使用者再次檢查,且需要以更明確的形式捨棄資訊。

有些模式會間接擊敗完整的相符項目。使用這類註解時請務必謹慎:

  • 避免使用 if let 模式,因為這是非完全比對的比對。我們推出例外狀況,允許繫結至 Option<T>Some 值,因為這是已知的類型,且 None 變化版本不包含任何資訊。請注意,如果 T 是列舉,則 if let Some(Foo::Variant) 並非有效的例外狀況,這是因為其側重 T 進行了詳盡比對。
  • 避免列舉接收器上的方法,用於指出變數但未比對的子類,例如:
    • is_foo(&self) -> bool 列舉方法,例如 Result::is_okOption::is_none
    • foo(self) -> Option<T> 列舉方法 (例如 Result::ok),可做為容易在審查中遺漏的單一變化版本擷取輔助工具。

Rust 提供 non_exhaustive 屬性,可對比對意圖進行徹底的比對,flexible FIDL 類型則會加註該屬性。處理這類類型時,嘗試全面比對很容易過時,類型可以在不破壞程式碼的情況下演變類型,因此應當避免。

// Don't attempt to match exhaustively, exhaustive enumeration is prone to
// becoming stale and misleading future readers if `Foo` takes more variants.
fn bad_flexible_match(foo: fidl_foo::FlexibleFoo) {
    match foo {
        fidl_foo::FlexibleFoo::Bar => { /* ... */ },
        foo @ fidl_foo::FlexibleFoo::Baz |
        foo => panic!("unexpected foo {:?}", foo)
    }
}

// Use the catch-all pattern instead when the type is non_exhaustive.
fn good_flexible_match(foo: fidl_foo::FlexibleFoo) {
    match foo {
        fidl_foo::FlexibleFoo::Bar => { /* ... */ },
        foo => panic!("unexpected foo {:?}", foo)
    }
}

// Note that if the type was not marked non_exhaustive we'd prefer to match
// exhaustively.
fn strict_match(foo: fidl_foo::StrictFoo) {
    match foo {
        fidl_foo::StrictFoo::Bar => { /* ... */ },
        foo @ fidl_foo::StrictFoo::Baz |
        foo @ fidl_foo::StrictFoo::Boo => panic!("unexpected foo {:?}", foo)
    }
}

TODO(https://github.com/rust-lang/rust/issues/89554):在 non_exhaustive_omitted_patterns Lint 穩定後,返回查看 non_exhaustive 指南。

避免使用預設類型參數

Rust 支援使用預設類型參數來定義參數化類型。這對於某些參數 (例如只用於覆寫測試行為的參數) 而言很方便。例如:

// This can be used as `Foo<X>` or `Foo<X, Y>`.
struct Foo<A, B = u32>(A, B);

// Blanket impl for all possible `Foo`s.
impl<A, B> MyTrait for Foo<A, B> { /* ... */ }

預設類型參數的問題在於,很容易導致不完整的全面實作。假設 Foo 是透過另一個預設的類型參數擴充:

// Now `Foo<A> = Foo<A, u32> = Foo<A, u32, ()>.
struct Foo<A, B = u32, C = ()>(A, B, C);

impl MyTrait 區塊仍未經修改運作,因此編譯器提供的信號並非覆蓋範圍,因此只涵蓋 Foo<A, B, ()>,而非所有可能的 Foo<A, B, C>。避免使用預設的類型參數時,作者會上蓋,確保使用任何簡化、毯子或其他方式皆涵蓋正確的類型組合。

記錄

記錄是開發人員的重要偵錯資源,但遺憾的是,記錄在記錄方面會出現一些錯誤,以減少其值和信號。記錄頻率過高或嚴重性過高,會導致記錄變得雜訊,減少特定問題實際上重要的偵錯訊號。相反地,記錄次數太少或嚴重性過低,表示偵錯信號可能不會一開始就記錄到記錄中。想要取得有用的記錄,關鍵就在於取得平衡。

Netstack3 利用追蹤 Crate 進行記錄,公開數個記錄層級。在深入瞭解 Netstack3 元件的記錄指南之前,請務必先瞭解幾項記錄檔嚴重性的預設設定:

  • 在正式版設定中,最低記錄嚴重性 (即低於這個值的記錄層級) 會設為 info,而使用偵錯網路堆疊元件 (例如在測試中) 時,嚴重程度會設為 debug。開發人員可以在本機執行 Netstack3 時,依循此設定。
  • 根據預設,測試最高記錄嚴重性 (即高於這個值的記錄層級) 會設定為 warn,否則系統會將測試通過的測試回報為失敗。個別測試套件可以調高或調低這個門檻。

現在,您應該在何時使用每個記錄檔層級?

  • error:適用於嚴重失敗,且很有可能出現在使用者可見的問題中。示例:
    • 基礎通訊埠已意外關閉,因此移除介面。
    • 觀察 PEER_CLOSED 以外的 FIDL 錯誤。
  • warn:事件或失敗情形較不嚴重,不僅能規範 error 的合理性,但仍可能發生在使用者看不見的問題或其他元件中的問題。示例:
    • 成功從未實作的 API 傳回。
    • 拒絕語意錯誤 (例如無效的引數) 的 API 要求。
    • 觀察基礎通訊埠的實體狀態變成離線。
  • info:重要事件,以及平價的偵錯信號。範例
    • 網路狀態變更,即新增/移除位址、路徑或介面。
    • 重要網路事件 (例如,發生重複位址偵測失敗)。
  • debug:這些事件和偵錯信號只與網路堆疊開發人員相關,或過於吵雜,無法合理化在實際工作環境中的裝置。
    • 控制網路的事件,例如路由器公告。
    • 傳入的 API 呼叫 (例如通訊端作業或 FIDL 方法),以及傳回至呼叫端的結果。
    • 開始/停止週期性工作,例如「鄰近資料表垃圾收集器」。
  • trace:對玩具環境外的用電量 (例如單元測試) 進行偵錯。範例
    • 非常頻繁的訊息,例如與傳送/接收資料路徑封包相關的訊息,或與高頻率計時器觸發的訊息。

之所以這麼做,是因為程式碼中的 errortrace 例項很少。此外,warn 例項也會不常見。大多數的記錄執行個體應為 infodebug

請考慮將 debug 設為預設記錄層級選項。在決定將訊息升級為 info 之前,請先仔細思考訊息的預期頻率,以及分類欄位問題時的訊息提供的價值。決定將訊息推送至 warn 之前,請先問自己以這個情況的可能性有多大 (例如,其他團隊的記錄檔中看見這則警告,是否適合再指派錯誤給您)。

對這個網頁的變更程序

所有人員都會受邀更改 Netstack 團隊採用的模式。完成共識建立程序後,團隊會接受或拒絕建議的模式,其結果會回去執行 go/no-go 簡單的多數投票。

如要提議變更,請按照下列步驟操作。

  1. 編寫並發布變更這個頁面的 CL。
  2. [選用] 先與一小群人進行社交互動,並反覆測試。
  3. 透過電子郵件和即時通訊,向整個團隊申請審查。非 Google 員工可以透過討論群組來聯絡。
  4. 根據評論留言和離線工作階段,反覆改進 CL。別忘了將離線工作階段的結果傳回 CL。
  5. 團隊成員可以表達對 +1 的支持、反對 -1 或差異不大。 「差異不大」是透過 Gerrit 上的單一註解執行緒,其中成員狀態差異。在 CL 合併之前,系統不會解析該執行緒。團隊成員可以隨時更改投票決定。
  6. 提案最多需要 2 週接受審核。系統會在第一週結束時傳送最後一次通話公告。如果「整個」團隊已投票,時間軸可能會短路。
  7. 如果無法達成共識,團隊會計算票數,並決定繼續執行或避免使用簡單的多數情況。

注意事項:

  • 作者和待開發客戶可以透過這項程序完成變更。
  • 尊重他人,僅根據他人的優待表達積分。
  • 避免使用冗長的註解,用簡潔的方式表達意見。
  • 不建議在 CL 上來回切換。改回使用分組影片或現場講座 (建議公開發布),並將共識編碼回留言執行緒中。
  • 如有需要,您可以在後續提案中捨棄爭議點並解決問題。
  • 「差異投票」是用來評估編碼部分模式的益處。我們會將強烈差異信號解讀為提示,指出所討論的點不會將編碼視為模式。