Logo by Misiasart
Thanks to all individual and corporate sponsors, without whom this work could not exist:
facet provides "const fn" reflection for Rust.
The Facet
trait is meant to be derived for every single type in the Rust
ecosystem, and can be used to replace many other derive macros.
pub unsafe trait Facet: Sized {
const SHAPE: &'static Shape;
// (other fields ignored)
}
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
- VTable for various standard traits:
- Display, Debug, Clone, Default, Drop etc.
The Debug
trait is severely limited because it cannot be specialized.
facet-pretty
provides pretty printing of any type that implements Facet
:
let address = Address {
street: "123 Main St".to_string(),
city: "Wonderland".to_string(),
country: "Imagination".to_string(),
};
let person = Person {
name: "Alice".to_string(),
age: 30,
address,
};
println!("Default pretty-printing:");
println!("{}", person.pretty());
facet on main [!] via 🦀 v1.86.0
❯ cargo run --example basic_usage
Compiling facet-pretty v0.1.2 (/Users/amos/bearcove/facet/facet-pretty)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
Running `target/debug/examples/basic_usage`
Default pretty-printing:
Person {
name: Alice,
age: 30,
address: Address {
street: 123 Main St,
city: Wonderland,
country: Imagination,
},
}
(Note: the default pretty-printing shows ANSI colors).
Facet knows the type inside the T
, so it's able to format it:
use facet_pretty::FacetPretty;
#[derive(Debug, Facet)]
struct Person {
name: String,
}
fn main() {
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 various_vecs
Compiling facet-pretty v0.1.2 (/Users/amos/bearcove/facet/facet-pretty)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
Running `target/debug/examples/various_vecs`
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:
/// A formatter for pretty-printing Facet types
pub struct PrettyPrinter {
indent_size: usize,
max_depth: Option<usize>,
color_generator: ColorGenerator,
use_colors: bool,
// ⬇️ here
list_u8_as_bytes: bool,
}
This is just a pretty printer, but an imaginative mind could come up with...
- A fully inspectable program state, through a browser interface?
- A modern debugger, exposing all the standard traits and then some instead of a bag of pointers?
The facet-reflect
crate allows reading and writing (constructing,
initializing) any type that implements Facet
— this makes it trivial to
write deserializers, see facet-json
, facet-yaml
, facet-urlencoded
, etc.
Say we have this struct:
use facet::Facet;
#[derive(Debug, PartialEq, Eq, Facet)]
struct FooBar {
foo: u64,
bar: String,
}
We can build it fully through reflection:
# use facet::Facet;
# #[derive(Debug, PartialEq, Eq, Facet)]
# struct FooBar {
# foo: u64,
# bar: String,
# }
use facet_reflect::PokeUninit;
fn main() {
// outer code: we know the type of `FooBar` — we pass `poke`
let (poke, guard) = PokeUninit::alloc::<FooBar>();
let foo_bar;
{
// inner code: all we have is a `poke` — our function is not generic,
// `Poke` is not generic.
let mut poke = poke.into_struct();
poke.set_by_name("foo", 42u64).unwrap();
{
let bar = String::from("Hello, World!");
poke.set_by_name("bar", bar).unwrap();
}
foo_bar = poke.build::<FooBar>(Some(guard));
}
// outer code: we know the type of `FooBar` again, we can
// move out of the `Poke`
println!("{}", foo_bar.bar);
}
The inner code
here is the kind of code you would write in a deserializer, for example.
Here, we cheated because we (the human) knew the structure of Poke, but in a deserializer,
you would match against the Poke
variant, you would inspect a StructDef
and its Field
s, etc.
Facet allows arbitrary attributes (WIP) so you can use it for specifying whether a CLI argument should be positional or named, for example:
use facet::Facet;
#[derive(Facet)]
struct Args {
#[facet(positional)]
path: String,
#[facet(named, short = 'v')]
verbose: bool,
#[facet(named, short = 'j')]
concurrency: usize,
}
let args: Args = facet_args::from_slice(&["--verbose", "--concurrency", "14", "example.rs"]);
eprintln!("args: {}", args.pretty());
facet on args [!] via 🦀 v1.86.0
❯ RUST_LOG=info nt run --no-capture test_arg_parse
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.02s
────────────
Nextest run ID a8ab7183-8333-465f-a94f-6036576f19c2 with nextest profile: default
Starting 1 test across 30 binaries (66 tests skipped)
START facet-args::simple test_arg_parse
[INFO simple] Logging and color backtrace initialized
running 1 test
args: Args {
path: example.rs,
verbose: true,
concurrency: 14,
}
test test_arg_parse ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
PASS [ 0.005s] facet-args::simple test_arg_parse
────────────
Summary [ 0.005s] 1 test run: 1 passed, 66 skipped
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.
For example, suppose we have:
#[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::opaque::OpaqueConst, facet_core::opaque::OpaqueConst) -> 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.
The core crates, facet-trait
, facet-types
etc. are nostd-friendly.
The main facet
crate re-exports symbols from:
- facet-core, which defines the main components:
- The
Facet
trait and implementations for foreign types (mostlylibstd
) - The
Shape
struct along with various vtables and the wholeDef
tree - Type-erased pointer helpers like
OpaqueUninit
,OpaqueConst
, andOpaque
- Autoderef specialization trick needed for
facet-derive
- The
- facet-derive, which implements the
Facet
derive attribute as a fast/light proc macro powered by unsynn
For struct manipulation and reflection, the following is available:
- facet-reflect, which allows reading from and writing to shapes implementing the
Facet
trait. This crate combines the functionality of the formerfacet-peek
andfacet-poke
crates, providing a unified interface for reflection and manipulation ofFacet
types.
facet supports deserialization from multiple data formats through dedicated crates:
- facet-json: JSON deserialization
- facet-yaml: YAML deserialization
- facet-msgpack: MessagePack deserialization
- facet-urlencoded: URL-encoded form data deserialization
- facet-args: CLI arguments (a-la clap)
Additionally:
- facet-pretty is able to pretty-print Facet types.
- facet-codegen is internal and generates some of the code of
facet-core