This guide covers the usage of the
fuchsia_inspect_derive
library, and assumes that you are familiar with
Inspect
and have basic experience with the
fuchsia_inspect
library.
Overview
The fuchsia_inspect_derive
library provides ergonomic macros, traits and
smart pointers around the fuchsia_inspect
library, that makes it easier to
integrate inspect with your Rust code base, by:
- Owning source data and inspect data under the same RAII type
- Being idiomatic. First class support for primitives, common interior mutability patterns and async.
- Generating repetitive boilerplate code
- Providing a unified way to attach a type to inspect
- Supporting gradual integration with existing code bases, both those that
don't yet support inspect, and the ones that are integrated with
fuchsia_inspect
directly. - Supporting foreign types that lack inspect integration. See
IDebug<T>
for usage and constraints.
At the same time, it preserves the performance and semantics of a manual inspect integration, by:
- Committing granular inspect tree modifications, where logical leaf nodes are updated independently.
- Applying static dispatch only, to avoid additional runtime overhead.
- Not using any additional synchronization primitives.
Caveats
When you integrate your Rust code base with this library, be aware that:
- The library mirrors the internal type hierarchy of the Rust program. Limited
structural modifications such as renaming, flattening and omitting fields are
supported (similar to Serde). If the desired inspect tree
structure is vastly different from the type hierarchy, you should consider
using
fuchsia_inspect
directly. - Some features are not yet supported, requiring you to
implement
Inspect
manually:- Lazy nodes, histograms and inspect arrays.
Option<T>
and other enums.- Collection types, such as vectors and maps.
- StringReferences
- The library promotes custom smart pointers, which creates another layer of data wrapping.
Quick start
This section shows an example where you take an existing data structure and apply inspect to that structure. Let's start with a simple example, a 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;
}
}
Then, consider this construction site:
let mut yak = Yak::new();
yak.shave();
Let's make the yak inspectable. In particular:
- Expose the current hair length
- Expose the number of times the Yak has been shaved
- The credit card number should NOT be exposed
Now use fuchsia_inspect_derive
to make this Yak inspectable:
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
}
}
Now, in your main program (or in a unit test), construct the yak and attach it to the inspect tree:
// 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: {});
Now you have integrated a simple program with Inspect. The rest of this guide describes the types, traits and macros of this library, and how to apply them to real world programs.
Derive Inspect
derive(Inspect)
can be added to any named struct, but each of its fields
must also implement Inspect
(except for inspect_node
and skipped fields).
The library provides implementations of Inspect
for several types:
- The
IOwned
smart pointers - Many common interior mutability wrappers
- All inspect properties (
UintProperty
,StringProperty
, etc) except for arrays and histograms - Other
Inspect
types. See the section on nesting.
If you add a type that isn't Inspect
, you get a compiler error:
#[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
Nested Inspect
Types
Inspect
types can be freely nested, like so:
// 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,
}
Fields and Attributes
All fields, except for skipped fields and inspect_node
, must implement
Inspect
, either for &mut T
or &T
.
If an inspect_node
field is present, instances will have its own node in the
inspect tree. It must be a 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.
If inspect_node
is absent, fields will be attached directly to the parent node
(meaning that the name provided to with_inspect
will be ignored):
#[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.
If your type needs to add or remove nodes or properties dynamically, it should own an inspect node. The inspect node is needed when nodes or properties are added or removed after the initial attachment.
derive(Inspect)
supports the following field attributes:
inspect(skip)
: The field is ignored by inspect.inspect(rename = "foo")
: Use a different name. By default, the field name is used.inspect(forward)
: Forwards the attachment to an innerInspect
type, omitting one layer of nesting from the inspect hierarchy. All other fields should not have any inspect attributes. The type must NOT have aninspect_node
field. Useful for wrapper types. For example:
#[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.
Manually Managed Inspect Types
If you are integrating with a code base that uses fuchsia_inspect
directly,
its types are not be aware of fuchsia_inspect_derive
. Do not add such
manually managed types as fields to an Inspect
type directly. Instead,
implement Inspect
manually for the type.
Avoid attaching manually outside of the Inspect
trait,
since attachment in fuchsia_inspect_derive
occurs after construction.
Attaching in a constructor can silently cause its inspect state to be
absent.
Attaching to the Inspect Tree
An inspect type should be attached once, and immediately after instantiation,
using the with_inspect
extension trait method:
let yak = Yak::new().with_inspect(inspector.root(), "my_yak")?;
assert_data_tree!(inspector, root: { my_yak: { name: "Lil Sebastian", age: 3u64 }});
If you have a nested Inspect
structure, you should only attach the top-level
type. The nested types are attached implicitly:
// 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 }}});
Note that when a Yak
is constructed from within a Stable
, there is no
with_inspect
call present. Instead, the Yak
is automatically attached as a
child of the Stable
. However, you can still attach a Yak
when it is the
top-level type, such as in the unit tests for Yak
. This allows you to test any
Inspect
type in isolation.
You can optionally choose to supply inspect nodes in constructors instead of
explicitly calling with_inspect
at the construction sites. First, ensure that
the type is NOT nested under another Inspect
type (as this would cause
duplicate attachments). Sedondly, make sure to document this fact clearly,
so the calling user is aware of your attachment convention.
Interior mutability
In Rust (and particularly async
Rust), it is common to use interior
mutability. This library provides Inspect
implementations for several smart
pointers and locks:
std
:Box
,Arc
,Rc
,RefCell
,Mutex
andRwLock
- Note that
Cell
does NOT work. Instead, upgrade to aRefCell
.
- Note that
parking_lot
:Mutex
andRwLock
futures
:Mutex
Generally, interior mutability within a derive(Inspect)
type just works:
#[derive(Inspect)]
struct Stable {
- yak: Yak,
+ yak: Arc<Mutex<Yak>>,
- horse: Horse,
+ horse: RefCell<Horse>,
inspect_node: fuchsia_inspect::Node,
}
Make sure to put your smart pointers inside your mutability wrapper:
struct Yak {
- coins: IValue<Rc<RwLock<u32>>>, // Won't compile
+ coins: Rc<RwLock<IValue<u32>>>, // Correct
}
If an inner type is behind a lock, attachment will fail if the lock is acquired by someone else. Hence, always attach immediately after instantiation.
Implement Inspect
Manually
The derive(Inspect)
derive-macro generates an
impl Inspect for &mut T { .. }
. Oftentimes, this works fine, but in
some cases you may need to implement Inspect
manually. Fortunately,
the Inspect
trait is quite simple:
trait Inspect {
/// Attach self to the inspect tree
fn iattach(self, parent: &Node, name: AsRef<str>) -> Result<(), AttachError>;
}
Do not return an AttachError
for structural errors in the data.
Instead, report the error using logs or an inspect node.
AttachError
is reserved for irrecoverable invariant errors that
fail the entire attachment.
IOwned
Smart Pointers
Smart pointers may sound scary, but you probably use them everyday already. For
instance, Arc
and Box
are smart pointers. They are statically dispatched,
and have first-class support in Rust (through deref coercion). This makes them
minimally invasive.
fuchsia_inspect_derive
comes with a few useful smart pointers that implement
Inspect
and can be used to wrap primitives, debuggable types, and more. They
all share the same behavior: An IOwned<T>
smart pointer owns a generic
source type T
and some associated inspect data.
Here is a demonstration of the 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.
An IOwned<T>
smart pointer should not be instantiated directly, but rather one
of its variants:
IValue<T>
The IValue<T>
smart pointer wraps a primitive (or any type T: Unit
).
For example, an IValue<f32>
is represented as a DoubleProperty
, and
an IValue<i16>
is represented as an IntProperty
.
An IValue
of a primitive results in the same structure as using a plain
inspect property directly. So, why would you use an IValue
? If you only
need to write or increment a value, you can use a plain inspect property. If you
also need to read the value, you should use an IValue
.
IDebug<T>
The IDebug<T>
smart pointer wraps a debuggable type, and maintains the debug
representation of T
as a StringProperty
. This is useful for:
- Foreign types, where adding an inspect implementation is infeasible
- Debugging, to quickly verify some state about your program
Avoid using debug representations in production code, since they come with the following issues:
- Debug representations are written on every inspect update, which can result in unnecessary performance overhead.
- Debug representations can exhaust the space of the inspect VMO, causing truncation of the entire inspect state.
- Debug representations cannot be integrated with the privacy pipeline: if any PII is exposed as part of the debug string, the entire field must be considered PII. Managing your own structured data allows to granularly redact fields containing PII.
The Unit
Trait
The Unit
trait describes the inspect representation of a type, how to
initialize it, and how to update it. It should be implemented for types that act
as a logical leaf node, and does NOT support per-field updates. This library
provides implementations of Unit
for most primitives. For example, u8
,
u16
, u32
and u64
are represented as a UintProperty
.
Usage in IValue
A Unit
type should be wrapped in an IValue<T: Unit>
(see above), for a RAII
managed inspectable type. It is NOT recommended to call methods on Unit
directly.
Derive Unit
Sometimes a logical Unit
is a composite type. Unit can be derived for a named
struct, as long as its fields also implement Unit
. For example:
// Represented as a Node with two properties, `x` and `y`, of type UintProperty
#[derive(Unit)]
struct Point {
x: f32,
y: f32,
}
Unit
can be nested, but keep in mind that all fields are still written at
the same time:
// 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,
}
Attributes
derive(Unit)
supports the following field attributes:
inspect(skip)
: The field is ignored by inspect.inspect(rename = "foo")
: Use a different name. By default, the field name is used.