Skip to content
/ facet Public

Rust const and no_std reflection — the last derive you'll ever need

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

facet-rs/facet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Facet logo - a reflection library for Rust   facet

experimental free of syn crates.io documentation MIT/Apache-2.0 licensed

Thanks to all individual and corporate sponsors, without whom this work could not exist:

Ko-fi GitHub Sponsors Patreon Zed

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.

Use case: inspection, pretty printing, debugging, specialization

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?

Use case: (de)serialization

The facet-peek and facet-poke allow 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:

// outer code: we know the type of `FooBar` — we pass `poke`
let (poke, guard) = Poke::alloc::<FooBar>();

{
    // 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", OpaqueConst::new(&42u64))
        .unwrap();

    {
        let bar = String::from("Hello, World!");
        poke.set_by_name("bar", OpaqueConst::new(&bar))
            .unwrap();
        // bar has been moved out of
        core::mem::forget(bar);
    }
}

// outer code: we know the type of `FooBar` again, we can
// move out of the `Poke`
let foo_bar = poke.build::<FooBar>(Some(guard));

The inner code here is the kind of code you would write in a deserializer, for example.

Use cases: parsing CLI arguments

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

Use cases: beyond

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

Ecosystem

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 (mostly libstd)
    • The Shape struct along with various vtables and the whole Def tree
    • Type-erased pointer helpers like OpaqueUninit, OpaqueConst, and Opaque
    • Autoderef specialization trick needed for facet-derive
  • facet-derive, which implements the Facet derive attribute as a fast/light proc macro powered by unsynn

For struct manipulation, the following are available:

  • facet-peek, which allows reading arbitrary Facet types
  • facet-poke, which allows building/altering arbitrary Facet types

facet supports deserialization from multiple data formats through dedicated crates:

Additionally:

License

Licensed under either of:

at your option.

About

Rust const and no_std reflection — the last derive you'll ever need

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Sponsor this project

 

Languages