人體工學檢查

本指南說明 fuchsia_inspect_derive 程式庫的使用方式,並假設您已熟悉檢查功能,且具備 fuchsia_inspect 程式庫的基本使用經驗。

總覽

fuchsia_inspect_derive 程式庫在 fuchsia_inspect 程式庫周圍提供符合人體工學巨集、特徵和智慧指標,方便您透過下列方式整合 Rust 程式碼集:

  • 擁有來源資料,並檢查相同 RAII 類型下的資料
  • 展現慣用風格。第一類支援基元、常見內部可變動模式和非同步。
  • 產生重複的樣板程式碼
  • 提供統一的附加類型來檢查
  • 支援與現有程式碼集逐步整合,包括尚未支援檢查功能的程式碼集,以及直接整合 fuchsia_inspect 的程式碼集。
  • 支援缺乏檢查整合的外類型。如要瞭解使用方式和限制,請參閱 IDebug<T>

同時,系統會透過以下方式保留手動檢查整合作業的效能和語意:

  • 修訂精細檢查樹狀結構修改,其中的邏輯分葉節點會獨立更新。
  • 僅套用靜態分派功能,避免產生額外的執行階段負擔。
  • 未使用任何額外的同步處理基本功能。

注意事項

將 Rust 程式碼集與這個程式庫整合時,請注意下列事項:

  • 這個程式庫會反映 Rust 程式的內部類型階層。系統支援有限的結構修改,例如重新命名、整併和省略欄位 (類似 Serde)。如果所需的檢查樹狀結構結構與類型階層有很大的差異,請考慮直接使用 fuchsia_inspect
  • 系統尚未支援部分功能,需要您手動實作 Inspect
    • 延遲節點、直方圖及檢查陣列。
    • Option<T> 和其他列舉。
    • 集合類型,例如向量和地圖。
    • StringReferences
  • 程式庫會宣傳「自訂智慧指標」,從而建立另一層資料包裝。

快速入門

本節舉例說明如何擷取現有資料結構,並對該結構套用檢查。先來看看一個簡單的 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();

讓我們將獨木舟設為可供檢查。請特別注意以下幾點:

  • 曝光目前的頭髮
  • 展示雅克刮鬍的次數
  • 信用卡號碼「請勿」公開

現在,使用 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
    }
}

現在,在主要程式 (或單元測試中) 建構獨木舟,並附加至檢查樹狀結構:

// 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

derive(Inspect) 可新增至任何已命名的結構體,但其每個欄位也必須實作 Inspect (inspect_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 擴充功能特徵方法時,應該在執行個體化後立即附加一次檢查類型:

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 呼叫。而是自動以 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> 會以 DoubleProperty 表示,IValue<i16> 則以 IntProperty 表示。

原始的 IValue 會經歷與直接使用純檢查屬性相同的結構。那麼,為什麼要使用 IValue?如果您只需要寫入或增加值,您可以使用純檢查屬性。如果也需要讀取值,則應使用 IValue

IDebug<T>

IDebug<T> 智慧型指標會納入可進行偵錯的類型,並將 T 的偵錯表示法維持為 StringProperty。這對於:

  • 外國類型無法加入檢查實作
  • 偵錯,快速驗證程式的某些狀態

請避免在正式版程式碼中使用偵錯表示法,因為這類表示法有下列問題:

  • 偵錯表示法會在每次檢查更新時寫入,這可能會導致不必要的效能負擔。
  • 偵錯表示法可能會耗盡檢查 VMO 的空間,導致整個檢查狀態遭到截斷。
  • 偵錯表示法無法與隱私管道整合:如果偵錯字串中包含任何 PII,則整個欄位都必須視為 PII。您可以管理自己的結構化資料,藉此精細遮蓋含有 PII 的欄位。

Unit 特徵

Unit 特徵會說明類型的檢查表示法、如何初始化及更新類型。應實作做為邏輯分葉節點的類型,且「不支援」個別欄位更新。這個程式庫提供大多數原始的 Unit 實作。舉例來說,u8u16u32u64 都會以 UintProperty 表示。

IValue 中的用量

Unit 類型應納入 IValue<T: Unit> (請見上文),以取得 RAII 管理的可檢查類型。不建議直接呼叫 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"):請改用其他名稱。根據預設,系統會使用欄位名稱。