Netstack 團隊's Rust 模式

本文件列舉 Netstack 團隊發現的模式:

  • 更靈活地在遠處變更行為。
  • 因為程式碼的「工作記憶體」較少,較容易審查。

這些指南可視為 Rust API 評分標準的補充項目。

這裡提供的模式可提供集中式指引和知識。歡迎提供所有類型的貢獻,包括編輯、新增、移除等。

在編寫程式碼或審查提議下進行程式碼時,遇到一些邊緣 符合相關規範,才能找到本文件 視需要編寫替代模式

避免未使用的結果

https://fxrev.dev/510442 起,這項功能大多是透過 rustc 的 unused-results lint 機器強制執行。請參閱說明文件,瞭解推理方式。

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

捨棄結果時,「偏好」使用前置字串為 _ 的已命名變數 當語意意義無法立即從類型中清除, 會影響 drop 語意

新增的資訊可做為作者和審查人員的提示:

  • 是否有應使用 return 檢查的變異數?
  • 這個函式是否應該傳回值?

注意:以往團隊在指派作業時,會使用表單 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 的模組中。如果模組 僅供 In-Crate 使用,應宣告 pub(crate),且 #[cfg(test)]。這個模組不應包含任何 #[test] 函式。如果需要為 testutil 模組中的功能進行測試,則應建立名為 tests 的子模組 (即testutil::tests)。

偏好恐慌

不要使用 Rust 支援的會傳回 Result 的測試;這類測試不會自動發出回溯記錄,而是依賴錯誤本身攜帶回溯記錄。測試未預期的錯誤 反向追蹤通常較難解讀。在本文撰寫期間 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 本機 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
  • 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 模式,因為這實際上是一種非完整比對。三 達成例外狀況,以允許繫結到Some Option<T>,因為這是眾所周知的類型,且 None 變體不適用 或任何資訊請注意,如果 T 是枚舉,if let Some(Foo::Variant) 就不是例外的有效用法,因為它會避開對 T 的完整比對。
  • 避免在枚舉接收器上使用方法,以指出未相符的變化版本,例如:

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 會使用 trace crate 進行記錄,並公開多個記錄層級。在深入瞭解 Netstack3 元件的記錄指南之前,請務必先瞭解記錄嚴重性設定的幾項預設值:

  • 在正式版設定中,最低記錄嚴重性 (即低於此值的記錄層級會遭到抑制) 會設為 info,使用偵錯 netstack 元件 (例如在測試中) 時則會設為 debug。您可以在 開發人員會選擇在本機執行 Netstack3 的情況。
  • 測試最高記錄嚴重性 (此值上方的記錄層級會導致 否則傳遞測試為失敗的測試) 設為 warn: 預設值。個別測試套件可以調高或調低這個門檻。

那麼,您應該在何時使用各個記錄層級?

  • error:保留給最有可能出現明顯出現的嚴重故障情況 在使用者可見性問題中範例:
    • 移除介面,因為基礎端口意外關閉。
    • 觀察到 PEER_CLOSED 以外的 FIDL 錯誤。
  • warn:事件或失敗情形不嚴重,不符合 error 的使用情境,但仍可能出現在使用者可見的問題或其他元件的問題。示例:
    • 已成功從未實作的 API 傳回結果。
    • 拒絕含有語意錯誤的 API 要求 (例如無效的引數)。
    • 觀察基礎連接埠的實體狀態已變更為離線。
  • info:值得注意的事件和便宜的偵錯信號。範例
    • 網路狀態變更:例如新增/移除位址、路徑或 存取 API
    • 有趣的網路事件 (例如「重複地址偵測」失敗)。
  • debug:只與網路堆疊相關的事件和偵錯信號 開發人員。
    • 控管來自網路的事件,例如路由器通告。
    • 傳入的 API 呼叫 (例如通訊端作業或 FIDL 方法) 並傳回給呼叫端的結果
    • 開始/停止定期工作,例如「鄰近資料表的垃圾」 收集器。
  • trace:在玩具環境 (例如單元測試) 以外,偵錯信號的成本過高。示例
    • 極度頻繁的郵件,例如與傳送/接收的郵件 資料路徑封包,或與高頻率計時器啟動搭配使用的訊息。

我們的目的是是為了程式碼中的 errortrace 例項 極少數的情況,在 warn 例項中也很罕見。大部分的記錄例項應為 infodebug

請將 debug 設為預設記錄層級選項。決定將訊息升級為 info 之前,請先想想訊息的預期頻率為何,以及這則訊息在篩選欄位問題時會提供哪些價值。決定將訊息升級為 warn 前,請先想想這項訊息對其他團隊來說是否代表問題 (也就是說,如果其他團隊在記錄檔中看到這項警告,是否適合指派給您處理這個錯誤)。

本頁面異動程序

歡迎所有人針對 Netstack 團隊採用的模式提出變更建議。在討論過程中達成共識後,團隊會接受或拒絕建議的模式,並回歸以簡單的多數表決方式決定是否採用。

請按照下列步驟提出變更建議。

  1. 編寫並發布變更這個頁面的 CL。
  2. [選用] 與小組互動並進行迭代。
  3. 透過電子郵件和即時通訊要求整個團隊進行審查。非 Google 員工可以 請透過討論群組與我們聯絡。
  4. 根據審查意見和離線工作階段,重複執行 CL。請記得將離線工作階段的成果公布回 CL。
  5. 團隊成員可以表達支持+1、反對 -1 或異議。 Gerrit 透過單一註解執行緒指出差異性, 成員狀態差異在 CL 合併前,該執行緒將維持未解決狀態。團隊成員隨時可以變更投票結果。
  6. 提案審查最多需要 2 週。系統會在第一週結束時發出最後通話通知。如果整個團隊都已投票,時間軸可能會中斷。
  7. 如果無法達成共識,團隊將會計算票數,並 決定該採用或不要只使用單純大多數

注意事項:

  • 作者和主管會透過這個程序做出變更。
  • 尊重他人;單靠自己的專業地址
  • 避免冗長的留言;以簡潔的論點表達不同意的立場。
  • 不建議在 CL 上來回討論。改回使用分組討論室 現場會議 (建議公開) 並將共識編碼為 留言執行緒
  • 您可以放棄有爭議的內容,並在必要時於後續提案中加以處理。
  • 無差別投票是用來評估編碼某些模式的效益。強烈的無差異訊號會解讀為提示,表示所討論的點不值得做為模式編碼。