Netstack 团队 Rust 模式

本文档列出了网络栈团队发现会产生以下问题的模式:

  • 代码更能适应远处的行为变化。
  • 代码更易于审核,需要的“工作内存”更少。

这些准则是对 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 的模块中。如果模块仅用于 crate 中,则应声明为 pub(crate)#[cfg(test)]。此模块不应包含任何 #[test] 函数。如果需要对 testutil 模块中的功能进行测试,则应创建一个名为 tests 的子模块(即testutil::tests)。

首选恐慌

切勿使用 Rust 对返回结果的测试 结果;此类测试不会自动发出回溯, 依赖错误本身进行回溯测试失败时 emit 回溯通常很难解释。在撰写本文时, Rust 中的回溯功能不稳定,并且 已在 Fuchsia Rust build 配置中停用,但 即使已启用,并非所有错误都包含回溯;最好不要惊慌, 依赖外部因素进行回溯。

导入

以下规则适用于样式导入。

每个 crate 或直接子模块使用一个 use 语句。合并 将它们放到同一个 use 语句中

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

始终将导入但未引用的特征别名为 _。这样可以避免 命名空间,并通知读取器未引用该标识符。

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
  • 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
  • 避免在枚举接收器上指示未匹配的变体的方法,例如:
    • 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 non_exhaustive_omitted_patterns lint 稳定后提供相关指南。

避免使用默认类型参数

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 netstack 组件(例如,在测试中)。在本地运行 Netstack3 时,开发者可以将此值配置得更低。
  • 测试的最大日志严重性(即高于此值的日志级别会导致 否则,将通过测试报告为已失败)设置为 warn 默认值。各个测试套件可以调高或调低此阈值。

那么,在什么情况下应该使用各个日志级别呢?

  • error:预留用于严重失败,并且很有可能表现为用户可见的问题。示例:
    • 移除接口,因为底层端口意外关闭。
    • 观察 PEER_CLOSED 以外的 FIDL 错误。
  • warn:虽然不太严重,以至于证明 error 的合理性的事件或失败情况, 可能仍然会体现在用户可见的问题或其他 组件。示例:
    • 从未实现的 API 成功返回。
    • 拒绝语义不正确的 API 请求(例如参数无效)。
    • 观察底层端口的实际状态是否已更改为离线。
  • info:重要事件和低成本调试信号。示例
    • 网络状态的更改:例如添加/移除地址、路由或接口。
    • 有趣的网络事件(例如重复地址检测失败)。
  • debug:仅与 Netstack 开发者相关的事件和调试信号,或者噪声过大,不适合在生产设备上使用。
    • 控制来自网络的事件,例如路由器通告。
    • 传入的 API 调用(例如套接字操作或 FIDL 方法),以及 返回给调用方的结果。
    • 启动/停止定期任务,例如邻居表垃圾回收器。
  • trace:调试玩具以外的代价非常高昂的信号 环境(即单元测试)。示例
    • 极其频繁的消息,例如与发送/接收数据路径数据包相关的消息,或与高频率计时器触发相关的消息。

目的是让代码中 errortrace 的实例 极其罕见,而 warn 的实例则并不常见。大多数日志实例应为 infodebug

debug 视为默认日志级别选项。在决定进行宣传之前 向 info 发送消息,问问自己预计收到消息的频率是多少 以及此消息在对字段问题进行分类时提供的值。 在决定向warn宣传某条消息之前,请先问问自己这样做的可能性有多大 向其他团队反映问题(例如,是否适合其他团队) 团队给您分配一个 bug,因为他们在日志中看到了此警告?)。

此页面的更改流程

欢迎所有人就 Netstack 团队采用的模式提出更改建议。在通过讨论达成共识后,团队会接受或拒绝提议的模式,如果无法达成共识,则采用简单的多数投票方式决定是否接受。

如需提出更改建议,请按以下步骤操作。

  1. 编写并发布用于更改此页面的 CL。
  2. [可选]与小群体交流并迭代。
  3. 通过电子邮件和聊天请求整个团队进行审核。非 Google 员工可以通过讨论群组与我们联系。
  4. 根据评价评论和线下会话对 CL 进行迭代改进。 请务必将线下会话的结果发布回 CL。
  5. 团队成员可以表达支持 +1、反对 -1 或不介意。 通过 Gerrit 上的单个评论会话表达了不同的看法 成员状态的差异化该会话会保持未解决状态,直到 CL 合并。团队成员可以随时更改投票。
  6. 提案的审核期最长为 2 周。最后一次通话通知是 。如果出现以下情况,时间轴可能会短路: 整个团队已投票。
  7. 如果无法达成共识,该团队将统计投票结果,并根据简单多数决定是否继续。

注意事项:

  • 作者和主管将引导您完成此流程。
  • 尊重他人;仅针对其优点提出意见。
  • 避免发表长篇大论;简要说明您对相关论据的不同看法。
  • 不建议在 CL 中来回切换。回退到分组会议或 线下会议(最好是公开会议),并将共识编码回 评论串。
  • 如果存在以下情况,则可以忽略争议性点并在后续提案中予以解决 。
  • 不介意投票用于衡量编码某些模式的预期效益。将强烈的差异信号解释为 都不适合作为模式进行编码。