Netstack 团队 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 的模块中。如果该模块仅供在 crate 中使用,则应将其声明为 pub(crate)#[cfg(test)]。此模块不应包含任何 #[test] 函数。如果 testutil 模块中的功能需要测试,则应创建一个名为 tests 的子模块(即testutil::tests).

优先使用 panic

请勿使用 Rust 对会返回 Result 的测试的支持;此类测试不会自动发出回溯,而依靠错误本身来执行回溯。不会发出回溯的测试失败通常更难以解释。在撰写本文时,Rust 中的回溯功能不稳定,并在 Fuchsia Rust build 配置中停用,但即使启用了,并非所有错误都包含回溯;最好恐慌以避免在回溯中依赖外部因素。

导入

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

每个 crate 或直接子模块使用一个 use 语句。将这些子项组合到同一个 use 语句中。

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

始终将已导入但未引用的 trait 设置为 _。避免让命名空间杂乱,并告知读取器没有引用该标识符。

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
  • 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>避免使用默认的类型参数会使开发者负责确保所有 impl、通用型或其他方式都能涵盖一组正确的类型。

日志记录

对开发者来说,日志是重要的调试资源,但遗憾的是,日志记录存在一些问题,导致其价值和信号降低。如果记录过于频繁或严重级别过高,日志会变得嘈杂,导致对给定问题真正重要的调试信号减少。相反,日志记录过于频繁或严重级别过低,意味着调试信号一开始可能不会进入日志。为了生成实用的日志,保持适当的平衡至关重要。

Netstack3 使用跟踪 crate 进行日志记录,该 crate 公开了多个日志级别。在深入了解 Netstack3 组件的日志记录指南之前,请务必了解日志严重性的一些默认配置:

  • 在正式版设置中,最低日志严重级别(即,低于此值的日志级别被抑制)会设置为 info;在使用调试 netstack 组件时(例如在测试中),会设置为 debug。在本地运行 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 中来回切换。回退到分组会议或面对面会话(最好是公开会议),并将共识编码回评论会话。
  • 如有必要,可以在跟进提案中提及争议性要点并加以解决。
  • 差异投票用于衡量对某些模式进行编码的感知优势。强烈的无差信号可解释为提示,指出正在讨论的点不值得编码为模式。