Expand description
§Hacking Guide to Facet
§The Facet Trait and Its Purpose
The Facet trait is the cornerstone of our reflection system. It provides a way to access type information at both compile time and runtime, enabling powerful meta-programming capabilities while maintaining Rust’s safety guarantees.
pub unsafe trait Facet: Sized {
/// The shape of this type
const SHAPE: &'static Shape;
}§Core Concept
The Facet trait allows any implementing type to expose its structural information through a static Shape object. This enables introspection of types at compile time, powering serialization, deserialization, debugging, and other operations that need to understand the structure of data.
§Specialization via Auto-Deref
Facet uses a technique called “auto-deref-based specialization” to enable trait-like specialization on stable Rust. This approach allows us to conditionally implement functionality based on what traits a type implements, all without requiring the unstable specialization feature.
§How Auto-Deref Specialization Works
The specialization technique uses Rust’s method resolution rules to our advantage. As described by Lukas Kalbertodt:
Autoderef-based specialization works by (ab)using the fact that method resolution prefers resolving to methods which require fewer type coercion of the receiver over methods that require more coercions.
For example, in our code:
// Wrapper struct for the specialization trick
struct Spez<T>(T);
// Trait for types that implement Debug
trait SpezDebugYes {
fn spez_debug(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error>;
}
// Trait for types that don't implement Debug
trait SpezDebugNo {
fn spez_debug(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error>;
}
// For types that implement Debug
impl<T: Debug> SpezDebugYes for &Spez<T> {
fn spez_debug(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
Debug::fmt(&self.0, f)
}
}
// Fallback for types that don't implement Debug
impl<T> SpezDebugNo for Spez<T> {
fn spez_debug(&self, _f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
unreachable!()
}
}When we use these traits, the compiler will prefer the first implementation if the type implements Debug, and fall back to the second one otherwise. This enables a form of specialization without requiring the unstable feature.
§Limitations of Auto-Deref Specialization
It’s important to understand when this specialization technique can and cannot be used:
-
Only works in macros and non-generic contexts: Auto-deref specialization is primarily useful in macros (like those in
facet-derive) and for non-generic, scalar types likei32,u32,String, etc. -
Not suitable for generic types: For types with generic parameters (like
HashMap<K, V>), this approach doesn’t work well because the specialization cannot be done based on properties of the generic parameters. -
Alternative for generic types: For generic types, we instead leverage the fact that
SHAPEis aconstassociated value that can be queried inconstcontexts. This allows us to perform compile-time checks and conditional logic based on properties of the generic parameters.
For example, in a generic implementation like HashMap<K, V>, we directly access the marker traits of K::SHAPE and V::SHAPE at compile time to determine what traits to implement for the containing type.
This pattern is used throughout the codebase for various traits like Debug, Display, Clone, Hash, and more, with different specialization approaches depending on whether we’re dealing with non-generic or generic types.
§Concrete Example: PartialOrd Specialization
Let’s examine how the PartialOrd trait is conditionally implemented using both approaches:
§Generic Type Example: Array Implementation
For arrays like [T; 1], we need to check if the inner type T implements PartialOrd. Since this is a generic type, we use compile-time evaluation of SHAPE:
fn create_array_shape<T: Facet>() {
let vtable = {
// Implementation of partial_ord for arrays
let partial_ord = if T::SHAPE.vtable.partial_ord.is_some() {
Some(|a: PtrConst, b: PtrConst| {
let a = unsafe { a.get::<[T; 1]>() };
let b = unsafe { b.get::<[T; 1]>() };
unsafe {
(T::SHAPE.vtable.partial_ord.unwrap_unchecked())(
PtrConst::new(&a[0]),
PtrConst::new(&b[0]),
)
}
})
} else {
None
};
// Rest of vtable implementation...
};
}Here’s what’s happening:
- We check if
T::SHAPE.vtable.partial_ordisSome, which tells us ifTimplementsPartialOrd - If it does, we provide a
partial_ordimplementation that:- Extracts arrays from opaque pointers
- Gets the first element from each array
- Delegates to the inner type’s
partial_ordimplementation
- If
Tdoesn’t implementPartialOrd, we setpartial_ordtoNone
§Non-Generic Type: Using value_vtable Macro
For non-generic types, we use the value_vtable macro which leverages auto-deref specialization:
let partial_ord = if facet::spez::impls!($type_name: core::cmp::PartialOrd) {
Some(|left: PtrConst, right: PtrConst| {
use facet::spez::*;
(&&Spez(unsafe { left.get::<$type_name>() }))
.spez_partial_cmp(&&Spez(unsafe { right.get::<$type_name>() }))
})
} else {
None
};Here’s what’s happening:
- The
impls!macro uses auto-deref specialization to check if$type_nameimplementsPartialOrd - If it does, we create a function that:
- Extracts the values from opaque pointers
- Wraps them in
Spez(specialization helper) - Calls
spez_partial_cmpwhich uses method resolution to pick the right implementation
- If it doesn’t implement
PartialOrd, we setpartial_ordtoNone
§Key Differences
These examples highlight the two approaches to specialization in the Facet codebase:
- Generic approach: Directly inspects
T::SHAPEat compile time for trait information - Non-generic approach: Uses the
impls!macro with auto-deref trick for specialization
Both approaches have the same goal: conditionally implement functionality based on trait implementations, but they use different mechanisms based on whether we’re dealing with generic or non-generic types.
§Working with Characteristics and MarkerTraits
The Characteristic enum represents various traits that a type can implement, including both marker traits (like Send, Sync, Copy) and functionality traits (like Debug, Clone, PartialEq).
pub enum Characteristic {
// Marker traits
Send,
Sync,
Copy,
Eq,
// Functionality traits
Clone,
Debug,
PartialEq,
PartialOrd,
Ord,
Hash,
Default,
}