Whereas crates like serde derive code using the heavy syn, facet derives
data with the light and fast unsynn.
That data does not make compile times balloon due to heavy monomorphization. It
can be used to reason about types at runtime — which even allows doing
specialization.
The SHAPE associated constant fully describes a type:
Whether it’s a struct, an enum, or a scalar
All fields, variants, offsets, discriminants, memory layouts
use facet_pretty::FacetPretty;
#[derive(Debug, Facet)]
struct Person {
name: String,
}
let alice = Person {
name: "Alice".to_string(),
};
let bob = Person {
name: "Bob".to_string(),
};
let carol = Person {
name: "Carol".to_string(),
};
println!("{}", vec![alice, bob, carol].pretty());
facet on main [$!] via 🦀 v1.86.0
❯ cargo run --example vec_person
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/examples/vec_person`
Vec<Person> [
Person {
name: Alice,
},
Person {
name: Bob,
},
Person {
name: Carol,
},
]
Because we know the shape of T, we can format different things differently,
if we wanted to:
let mut file = std::fs::File::open("/dev/urandom").expect("Failed to open /dev/urandom");
let mut bytes = vec![0u8; 128];
std::io::Read::read_exact(&mut file, &mut bytes).expect("Failed to read from /dev/urandom");
println!("{}", bytes.pretty());
facet on main [!] via 🦀 v1.86.0
❯ cargo run --example vec_u8
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/examples/vec_u8`
Vec<u8>
aa c5 ce 2a 79 95 a6 c6 63 ca 69 5f 12 d5 7e fc
f4 40 60 48 c4 ee 10 7c 12 a2 67 3d 2f 9a c4 ca
b3 7e 91 5c 67 16 41 35 92 31 22 0f 23 6a ad c1
f4 b3 c2 60 38 13 02 47 25 7e f9 48 9b 11 b5 0e
cb 5d c6 b1 43 23 bd a7 8c 6c 7d e6 7b 72 b7 26
1a 2c e2 b8 e9 1a a6 e7 f6 b2 9b c7 88 76 d2 be
59 79 27 00 0b 3e 88 a3 ce 8a 14 ec 72 f9 eb 23
d4 36 93 a5 e9 b9 00 de 6a 3f 64 b8 49 05 3f 22
And because we can make this decision at runtime, it can be an option on the pretty-printer itself:
The facet-reflect crate allows reading (peek) and constructing/initializing/mutating (poke) arbitrary
values without knowing their concrete type until runtime. This makes it trivial to
write deserializers, see facet-json, facet-yaml, facet-urlencoded, etc.
use facet::Facet;
#[derive(Debug, PartialEq, Eq, Facet)]
struct FooBar {
foo: u64,
bar: String,
}
We can build it fully through reflection using the slot-based initialization API:
use facet::Facet;
use facet_reflect::Wip;
#[derive(Debug, PartialEq, Eq, Facet)]
struct FooBar {
foo: u64,
bar: String,
}
let foo_bar = Wip::alloc::<FooBar>()
.field_named("foo")?
.put(42u64)?
.pop()?
.field_named("bar")?
.put(String::from("Hello, World!"))?
.pop()?
.build()?
.materialize::<FooBar>()?;
// Now we can use the constructed value
println!("{}", foo_bar.bar);
The reflection API maintains type safety by validating types at each step and tracks which fields have been initialized.
This approach is particularly powerful for deserializers, where you need to incrementally build objects without knowing their full structure upfront. Inside a deserializer, you would first inspect the shape to understand its structure, and then systematically initialize each field.
By default the Facet derive macro creates and exports
a global static variable {UPPER_CASE_NAME}_SHAPE referencing the
Shape of the derived Facet trait.
Furthermore, Shape and all nested fields are #[repr(C)].
This information can be used by external processes (like debuggers) to access the
layout and vtable data.
#[derive(Debug, Facet)]
struct TestStruct {
field: &'static str,
}
static STATIC_TEST_STRUCT: TestStruct = TestStruct {
field: "some field I would like to see",
};
By default, printing this in lldb returns the lengthy:
(lldb) p STATIC_TEST_STRUCT
(simple_test::TestStruct) {
field = "some field I would like to see" {
[0] = 's'
[1] = 'o'
[2] = 'm'
[3] = 'e'
[4] = ' '
[5] = 'f'
[6] = 'i'
[7] = 'e'
[8] = 'l'
[9] = 'd'
... (and so on)
}
However, the TestStruct::SHAPE constant is available at TEST_STRUCT_SHAPE:
(lldb) p TEST_STRUCT_SHAPE
(facet_core::types::Shape *) 0x00000001000481c8
And so we can instead build a simple helper function that takes in a pointer
to the object and it’s debug fn and prints out the Debug representation:
(lldb) p debug_print_object(&STATIC_TEST_STRUCT, &TEST_STRUCT_SHAPE->vtable->debug)
TestStruct {
field: "some field I would like to see",
}
In this case, debug_print_object is needed because the debug function requires a Formatter
which cannot be constructed externally. But for other operations like Eq, you can resolve it
without needing external methods (but with some additional shenanigans to make lldb happy):
(lldb) p TEST_STRUCT_SHAPE->vtable->eq
(core::option::Option<unsafe fn(facet_core::ptr::PtrConst, facet_core::ptr::PtrConst) -> bool>) {
value = {
0 = 0x0000000100002538
}
}
(lldb) p (*((bool (**)(simple_test::TestStruct* , simple_test::TestStruct*))(&TEST_STRUCT_SHAPE->vtable->eq)))(&STATIC_TEST_STRUCT, &STATIC_TEST_STRUCT)
(bool) true
This could be extended to allow RPC, there could be an analoguous derive for traits,
it could export statics so that binaries may be inspected — shapes would then be available
instead of / in conjunction with debug info.
HTTP routing is a form of deserialization.
This is suitable for all the things serde is bad at: binary formats (specialize
for Vec<u8> without a serde_bytes hack), it could be extended to support formats
like KDL/XML.
I want the derive macros to support arbitrary attributes eventually, which will also
be exposed through Shape.
The types are all non_exhaustive, so there shouldn’t be churn in the
ecosystem: crates can do graceful degradation if some types don’t implement the
interfaces they expect.
If you have questions or ideas, please open a GitHub issue or discussion — I’m
so excited about this.