人体工程学检查

本指南介绍了 fuchsia_inspect_derive 库的用法,并假设您熟悉 Inspect 并基本熟悉 fuchsia_inspect 库。

概览

fuchsia_inspect_derive 库提供了围绕 fuchsia_inspect 库的人体工程学宏、特征和智能指针,通过执行以下操作,可以更轻松地将检查与 Rust 代码库集成:

  • 拥有源数据并检查相同 RAII 类型下的数据
  • 使用惯用语言。对基元、常见内部可变性模式和异步的一类支持。
  • 生成重复的样板代码
  • 提供一种统一的方式来附加要检查的类型
  • 支持与现有代码库逐步集成,包括那些尚不支持检查的代码库以及直接与 fuchsia_inspect 集成的代码库。
  • 支持缺少检查集成的外部类型。如需了解用法和限制条件,请参阅 IDebug<T>

同时,它通过以下方式保留手动检查集成的性能和语义:

  • 提交精细的检查树修改,其中逻辑叶节点会独立更新。
  • 仅应用静态调度,以避免额外的运行时开销。
  • 不使用任何其他同步基元。

注意事项

将 Rust 代码库与此库集成时,请注意:

  • 该库是 Rust 程序的内部类型层次结构的镜像。系统支持有限的结构修改,例如重命名、展平和省略字段(类似于 Serde)。如果所需的检查树结构与类型层次结构截然不同,您应考虑直接使用 fuchsia_inspect
  • 某些功能尚不受支持,您需要手动实现 Inspect
    • 延迟节点、直方图和检查数组。
    • Option<T> 和其他枚举。
    • 集合类型,例如矢量和地图。
    • 字符串引用
  • 该库会提升自定义智能指针,这会创建另一个数据封装层。

快速入门

本部分举例说明了如何选取现有数据结构并对该结构应用检查。我们先来看一个简单的 Yak 示例:

struct Yak {
    // TODO: Overflow risk at high altitudes?
    hair_length: u16,       // Current hair length in mm
    credit_card_no: String, // Super secret
}

impl Yak {
    pub fn new() -> Self {
        Self { hair_length: 5, credit_card_no: "<secret>".to_string() }
    }

    pub fn shave(&mut self) {
        self.hair_length = 0;
    }
}

然后,请考虑以下施工现场:

let mut yak = Yak::new();
yak.shave();

让我们将 yak 设为可检查。具体而言:

  • 暴露当前头发长度
  • 公开麋鹿被剃须的次数
  • 请勿公开信用卡号

现在,使用 fuchsia_inspect_derive 使此 Yak 可检查:

use fuchsia_inspect_derive::{
    IValue,      // A RAII smart pointer that can be attached to inspect
    Inspect,     // The core trait and derive-macro
    WithInspect, // Provides `.with_inspect(..)`
};

#[derive(Inspect)]
struct Yak {
    #[inspect(rename = "hair_length_mm")] // Clarify that it's millimeters
    hair_length: IValue<u16>, // Encapsulate primitive in IValue

    #[inspect(skip)] // Credit card number should NOT be exposed
    credit_card_no: String,
    shaved_counter: fuchsia_inspect::UintProperty, // Write-only counter
    inspect_node: fuchsia_inspect::Node,           // Inspect node of this Yak, optional
}

impl Yak {
    pub fn new() -> Self {
        Self {
            hair_length: IValue::new(5), // Or if you prefer, `5.into()`
            credit_card_no: "<secret>".to_string(),

            // Inspect nodes and properties should be default-initialized
            shaved_counter: fuchsia_inspect::UintProperty::default(),
            inspect_node: fuchsia_inspect::Node::default(),
        }
    }

    pub fn shave(&mut self) {
        self.hair_length.iset(0); // Set the source value AND update the inspect property
        self.shaved_counter.add(1u64); // Increment counter
    }
}

现在,在您的主程序(或单元测试)中,构建 yak 并将其附加到检查树:

// Initialization
let mut yak = Yak::new()
    .with_inspect(/* parent node */ inspector.root(), /* name */ "my_yak")?;

assert_data_tree!(inspector, root: {
    my_yak: { hair_length_mm: 5u64, shaved_counter: 0u64 }
});

// Mutation
yak.shave();
assert_data_tree!(inspector, root: {
    my_yak: { hair_length_mm: 0u64, shaved_counter: 1u64 }
});

// Destruction
std::mem::drop(yak);
assert_data_tree!(inspector, root: {});

现在,您已将一个简单的程序与 Inspect 集成。本指南的其余部分将介绍此库的类型、特征和宏,以及如何将它们应用于实际程序。

派生 Inspect

derive(Inspect)可以添加到任何已命名结构体中,但其每个字段还必须实现Inspectinspect_node和跳过的字段除外)。该库为多种类型提供了 Inspect 的实现:

如果您添加的类型不是 Inspect,则会遇到编译器错误:

#[derive(Inspect)]
struct Yakling {
    name: String, // Forgot to wrap, should be `name: IValue<String>`
}

// error[E0599]: no method named `iattach` found for struct
// `std::string::String` in the current scope

嵌套 Inspect 类型

Inspect 类型可以自由嵌套,如下所示:

// Stable is represented as a node with two child nodes `yak` and `horse`
#[derive(Inspect)]
struct Stable {
    yak: Yak,     // Yak derives Inspect
    horse: Horse, // Horse implements Inspect manually
    inspect_node: fuchsia_inspect::Node,
}

字段和属性

所有字段(跳过的字段和 inspect_node 除外)都必须针对 &mut T&T 实现 Inspect

如果存在 inspect_node 字段,实例将在检查树中拥有自己的节点。它必须是 fuchsia_inspect::Node

#[derive(Inspect)]
struct Yak {
    name: IValue<String>,
    age: IValue<u16>,
    inspect_node: fuchsia_inspect::Node, // NOTE: Node is present
}

// Yak is represented as a node with `name` and `age` properties.

如果 inspect_node 不存在,则字段将直接附加到父节点(这意味着提供给 with_inspect 的名称将被忽略):

#[derive(Inspect)]
struct YakName {
    title: IValue<String>, // E.g. "Lil"
    full_name: IValue<String>, // E.g. "Sebastian"
                           // NOTE: Node is absent
}

// YakName has no separate node. Instead, the `title` and `full_name`
// properties are attached directly to the parent node.

如果您的类型需要动态添加或移除节点或属性,它应该拥有一个检查节点。在初始连接之后添加或移除节点或属性时,需要使用检查节点。

derive(Inspect) 支持以下字段属性:

  • inspect(skip):检查会忽略该字段。
  • inspect(rename = "foo"):请使用其他名称。默认情况下,系统会使用字段名称。
  • inspect(forward):将连接转发到内部 Inspect 类型,省略检查层次结构中的一个嵌套层。所有其他字段不应具有任何检查属性。该类型不得包含“inspect_node”字段。对于封装容器类型来说很有用。 例如:
#[derive(Inspect)]
struct Wrapper {
    // key is not included, because inspect has been forwarded.
    _key: String,
    #[inspect(forward)]
    inner: RefCell<Inner>,
}

#[derive(Inspect)]
struct Inner {
    name: IValue<String>,
    age: IValue<u16>,
    inspect_node: fuchsia_inspect::Node,
}

// Wrapper is represented as a node with `name` and `age` properties.

手动管理的检查类型

如果您要与直接使用 fuchsia_inspect 的代码库集成,则其类型无法感知 fuchsia_inspect_derive。请勿直接将此类手动管理的类型(如字段)添加到 Inspect 类型中。请改为针对该类型手动实现 Inspect。请避免在 Inspect 特征之外手动附加,因为 fuchsia_inspect_derive 中的附加操作是在构造完成后发生的。附加到构造函数中可能会静默地导致其检查状态不存在。

附加到检查树

检查类型应附加一次,并在实例化后立即使用 with_inspect 扩展 trait 方法附加:

let yak = Yak::new().with_inspect(inspector.root(), "my_yak")?;
assert_data_tree!(inspector, root: { my_yak: { name: "Lil Sebastian", age: 3u64 }});

如果您有嵌套的 Inspect 结构,应仅附加顶级类型。嵌套类型是隐式附加的:

// Stable owns a Yak, which also implements Inspect.
let stable = Stable::new().with_inspect(inspector.root(), "stable")?;
assert_data_tree!(inspector,
    root: { stable: { yak: { name: "Lil Sebastian", age: 3u64 }}});

请注意,从 Stable 中构建 Yak 时,不存在 with_inspect 调用。相反,Yak 会自动附加为 Stable 的子级。不过,当 Yak 是顶级类型时(例如在 Yak 的单元测试中),您仍然可以附加 Yak。这样,您就可以单独测试任何 Inspect 类型。

您可以选择在构造函数中提供检查节点,而不是在施工现场明确调用 with_inspect。首先,请确保该类型未嵌套在另一个 Inspect 类型下(因为这会导致重复的连接)。不过,请务必清楚地记录这一事实,以便发起调用的用户了解您的附件惯例。

内部可变性

在 Rust(尤其是 async Rust)中,内部可变性很常见。此库为多个智能指针和锁提供了 Inspect 实现:

  • stdBoxArcRcRefCellMutexRwLock
    • 请注意,Cell 不起作用。请改为升级到 RefCell
  • parking_lotMutexRwLock
  • futuresMutex

通常,derive(Inspect) 类型的内部可变性仅起作用:

#[derive(Inspect)]
struct Stable {
-   yak: Yak,
+   yak: Arc<Mutex<Yak>>,
-   horse: Horse,
+   horse: RefCell<Horse>,
    inspect_node: fuchsia_inspect::Node,
}

请确保将智能指针放在可变性封装容器中:

struct Yak {
-   coins: IValue<Rc<RwLock<u32>>>,  // Won't compile
+   coins: Rc<RwLock<IValue<u32>>>,  // Correct
}

如果某个内部类型位于锁之后,则在其他人获取该锁时,连接将失败。因此,应始终在实例化后立即附加。

手动实现 Inspect

derive(Inspect) 派生宏会生成 impl Inspect for &mut T { .. }。通常这没什么问题,但在某些情况下,您可能需要手动实现 Inspect。幸运的是,Inspect 特征非常简单:

trait Inspect {
    /// Attach self to the inspect tree
    fn iattach(self, parent: &Node, name: AsRef<str>) -> Result<(), AttachError>;
}

不针对数据中存在结构错误返回 AttachError。请改为使用日志或检查节点报告错误。AttachError 专用于使整个连接失败且不可恢复的不变错误。

IOwned 智能指针

智能指针可能听起来很吓人,但其实您可能已经在日常使用。例如,ArcBox 是智能指针。它们是静态分派的,并在 Rust 中具有一流的支持(通过 deref 强制转换)。这使它们的侵扰性微乎其微。

fuchsia_inspect_derive 附带一些可实现 Inspect 的实用智能指针,可用于封装基元、可调试类型等。它们具有相同的行为:IOwned<T> 智能指针拥有通用来源类型 T 和一些关联的检查数据

以下是 IOwned API 的演示:

let mut number = IValue::new(1337u16) // IValue is an IOwned smart pointer
    .with_inspect(inspector.root(), "my_number")?; // Attach to inspect tree

// Dereference the value behind the IValue, without mutating
assert_eq!(*number, 1337u16);
{
    // Mutate value behind an IOwned smart pointer, using a scope guard
    let mut number_guard = number.as_mut();
    *number_guard = 1338;
    *number_guard += 1;

    // Inspect state not yet updated
    assert_data_tree!(inspector, root: { my_number: 1337u64 });
}
// When the guard goes out of scope, the inspect state is updated
assert_data_tree!(inspector, root: { my_number: 1339u64 });

number.iset(1340); // Sets the source value AND updates inspect
assert_data_tree!(inspector, root: { my_number: 1340u64 });

let inner = number.into_inner(); // Detaches from inspect tree...
assert_eq!(inner, 1340u16); // ...and returns the inner value.

IOwned<T> 智能指针不应直接实例化,而应实例化其变体之一:

IValue<T>

IValue<T> 智能指针封装了一个基元(或任何类型的 T: Unit)。例如,IValue<f32> 表示为 DoublePropertyIValue<i16> 表示为 IntProperty

基元的 IValue 会生成与直接使用普通检查属性相同的结构。那么,为什么要使用 IValue?如果您只需写入或递增值,则可以使用普通检查属性。如果您还需要读取该值,则应使用 IValue

IDebug<T>

IDebug<T> 智能指针封装了一个可调试类型,并将 T 的调试表示形式维护为 StringProperty。此功能适用于:

  • 添加检查实现不可行的外部类型
  • 调试,用于快速验证程序的某些状态

请避免在生产代码中使用调试表示法,因为此类表示法存在以下问题:

  • 每次更新检查时都会写入调试表示形式,这可能会导致不必要的性能开销。
  • 调试表示法可能会用尽检查 VMO 的空间,导致整个检查状态被截断。
  • 调试表示法不能与隐私流水线集成:如果任何个人身份信息作为调试字符串的一部分公开,则整个字段必须被视为个人身份信息。通过管理自己的结构化数据,您可以精确隐去包含个人身份信息的字段。

Unit 特征

Unit trait 描述了某个类型的检查表示法、如何对其进行初始化以及如何更新。应对充当逻辑叶节点且不支持按字段更新的类型实现此 API。此库为大多数基元提供了 Unit 的实现。例如,u8u16u32u64 表示为 UintProperty

IValue 中的用法

对于 RAII 管理的可检查类型,Unit 类型应封装在 IValue<T: Unit>(见上文)中。建议不要直接在 Unit 上调用方法。

派生 Unit

有时,逻辑 Unit 是复合类型。可以派生命名结构体的单位,前提是其字段也实现了 Unit。例如:

// Represented as a Node with two properties, `x` and `y`, of type UintProperty
#[derive(Unit)]
struct Point {
    x: f32,
    y: f32,
}

Unit 可以嵌套,但请注意,所有字段仍会同时写入:

// Represented as a Node with two child nodes `top_left` and `bottom_right`
#[derive(Unit)]
struct Rect {
    #[inspect(rename = "top_left")]
    tl: Point,

    #[inspect(rename = "bottom_right")]
    br: Point,
}

属性

derive(Unit) 支持以下字段属性:

  • inspect(skip):检查会忽略该字段。
  • inspect(rename = "foo"):请使用其他名称。默认情况下,系统会使用字段名称。