Item 19: Avoid reflection

Programmers coming to Rust from other languages are often used to reaching for reflection as a tool in their toolbox. They can waste a lot of time trying to implement reflection-based designs in Rust, only to discover that what they're attempting can only be done poorly, if at all. This Item hopes to save that time wasted exploring dead ends, by describing what Rust does and doesn't have in the way of reflection, and what can be used instead.

Reflection is the ability of a program to examine itself at runtime. Given an item at runtime, it covers these questions:

  • What information can be determined about the item's type?
  • What can be done with that information?

Programming languages with full reflection support have extensive answers to these questions. Languages with reflection typically support some or all of the following at runtime, based on the reflection information:

  • Determining an item's type
  • Exploring its contents
  • Modifying its fields
  • Invoking its methods

Languages that have this level of reflection support also tend to be dynamically typed languages (e.g., Python, Ruby), but there are also some notable statically typed languages that also support reflection, particularly Java and Go.

Rust does not support this type of reflection, which makes the advice to avoid reflection easy to follow at this level—it's just not possible. For programmers coming from languages with support for full reflection, this absence may seem like a significant gap at first, but Rust's other features provide alternative ways of solving many of the same problems.

C++ has a more limited form of reflection, known as run-time type identification (RTTI). The typeid operator returns a unique identifier for every type, for objects of polymorphic type (roughly: classes with virtual functions):

  • typeid: Can recover the concrete class of an object referred to via a base class reference
  • dynamic_cast<T>: Allows base class references to be converted to derived classes, when it is safe and correct to do so

Rust does not support this RTTI style of reflection either, continuing the theme that the advice of this Item is easy to follow.

Rust does support some features that provide similar functionality in the std::any module, but they're limited (in ways we will explore) and so best avoided unless no other alternatives are possible.

The first reflection-like feature from std::any looks like magic at first—a way of determining the name of an item's type. The following example uses a user-defined tname() function:

let x = 42u32;
let y = vec![3, 4, 2];
println!("x: {} = {}", tname(&x), x);
println!("y: {} = {:?}", tname(&y), y);

to show types alongside values:

x: u32 = 42
y: alloc::vec::Vec<i32> = [3, 4, 2]

The implementation of tname() reveals what's up the compiler's sleeve: the function is generic (as per Item 12), and so each invocation of it is actually a different function (tname::<u32> or tname::<Square>):

#![allow(unused)]
fn main() {
fn tname<T: ?Sized>(_v: &T) -> &'static str {
    std::any::type_name::<T>()
}
}

The implementation is provided by the std::any::type_name<T> library function, which is also generic. This function has access only to compile-time information; there is no code run that determines the type at runtime. Returning to the trait object types used in Item 12 demonstrates this:

let square = Square::new(1, 2, 2);
let draw: &dyn Draw = &square;
let shape: &dyn Shape = &square;

println!("square: {}", tname(&square));
println!("shape: {}", tname(&shape));
println!("draw: {}", tname(&draw));

Only the types of the trait objects are available, not the type (Square) of the concrete underlying item:

square: reflection::Square
shape: &dyn reflection::Shape
draw: &dyn reflection::Draw

The string returned by type_name is suitable only for diagnostics—it's explicitly a "best-effort" helper whose contents may change and may not be unique—so don't attempt to parse type_name results. If you need a globally unique type identifier, use TypeId instead:

#![allow(unused)]
fn main() {
use std::any::TypeId;

fn type_id<T: 'static + ?Sized>(_v: &T) -> TypeId {
    TypeId::of::<T>()
}
}
println!("x has {:?}", type_id(&x));
println!("y has {:?}", type_id(&y));
x has TypeId { t: 18349839772473174998 }
y has TypeId { t: 2366424454607613595 }

The output is less helpful for humans, but the guarantee of uniqueness means that the result can be used in code. However, it's usually best not to use TypeId directly but to use the std::any::Any trait instead, because the standard library has additional functionality for working with Any instances (described below).

The Any trait has a single method type_id(), which returns the TypeId value for the type that implements the trait. You can't implement this trait yourself, though, because Any already comes with a blanket implementation for most arbitrary types T:

impl<T: 'static + ?Sized> Any for T {
    fn type_id(&self) -> TypeId {
        TypeId::of::<T>()
    }
}

The blanket implementation doesn't cover every type T: the T: 'static lifetime bound means that if T includes any references that have a non-'static lifetime, then TypeId is not implemented for T. This is a deliberate restriction that's imposed because lifetimes aren't fully part of the type: TypeId::of::<&'a T> would be the same as TypeId::of::<&'b T>, despite the differing lifetimes, increasing the likelihood of confusion and unsound code.

Recall from Item 8 that a trait object is a fat pointer that holds a pointer to the underlying item, together with a pointer to the trait implementation's vtable. For Any, the vtable has a single entry, for a type_id() method that returns the item's type, as shown in Figure 3-4:

let x_any: Box<dyn Any> = Box::new(42u64);
let y_any: Box<dyn Any> = Box::new(Square::new(3, 4, 3));
The diagram shows a stack on the left, some concrete items in the middle, and some vtables and code objects on the
right hand side.  The stack part has two trait objects labelled x_any and y_any. The x_any trait object has an
arrow to a concrete item with value 42 in the middle; it also has an arrow to an Any for u64 vtable on the right hand
side. This vtable has a single type_id entry, which in turn points to a code object labelled
return TypeId::of::<u64>. The y_any trait object has an arrow to a concrete item that is composite, with components
top_left.x=3, top_left.y=4 and size=3; it also has an arrow to an Any for Square vtable on the right hand side.
This vtable has a single type_id entry, which in turn points to a code object labelled
return TypeId::of::<Square>.

Figure 3-4. Any trait objects, each with pointers to concrete items and vtables

Aside from a couple of indirections, a dyn Any trait object is effectively a combination of a raw pointer and a type identifier. This means that the standard library can offer some additional generic methods that are defined for a dyn Any trait object; these methods are generic over some additional type T:

  • is::<T>(): Indicates whether the trait object's type is equal to some specific other type T
  • downcast_ref::<T>(): Returns a reference to the concrete type T, provided that the trait object's type matches T
  • downcast_mut::<T>(): Returns a mutable reference to the concrete type T, provided that the trait object's type matches T

Observe that the Any trait is only approximating reflection functionality: the programmer chooses (at compile time) to explicitly build something (&dyn Any) that keeps track of an item's compile-time type as well as its location. The ability to (say) downcast back to the original type is possible only if the overhead of building an Any trait object has already happened.

There are comparatively few scenarios where Rust has different compile-time and runtime types associated with an item. Chief among these is trait objects: an item of a concrete type Square can be coerced into a trait object dyn Shape for a trait that the type implements. This coercion builds a fat pointer (object + vtable) from a simple pointer (object/item).

Recall also from Item 12 that Rust's trait objects are not really object-oriented. It's not the case that a Square is-a Shape; it's just that a Square implements Shape's interface. The same is true for trait bounds: a trait bound Shape: Draw does not mean is-a; it just means also-implements because the vtable for Shape includes the entries for the methods of Draw.

For some simple trait bounds:

trait Draw: Debug {
    fn bounds(&self) -> Bounds;
}

trait Shape: Draw {
    fn render_in(&self, bounds: Bounds);
    fn render(&self) {
        self.render_in(overlap(SCREEN_BOUNDS, self.bounds()));
    }
}

the equivalent trait objects:

let square = Square::new(1, 2, 2);
let draw: &dyn Draw = &square;
let shape: &dyn Shape = &square;

have a layout with arrows (shown in Figure 3-5; repeated from Item 12) that make the problem clear: given a dyn Shape object, there's no immediate way to build a dyn Draw trait object, because there's no way to get back to the vtable for impl Draw for Square—even though the relevant part of its contents (the address of the Square::bounds() method) is theoretically recoverable. (This is likely to change in later versions of Rust; see the final section of this Item.)

The diagram shows a stack layout on the left, and on the right are rectangles representing vtables and code
methods. The stack layout includes three items; at the top is a composite item labeled square, with contents being
a top_left.x value of 1, a top_left.y value of 2 and a size value of 2. The middle stack item is a trait object,
containing an item pointer that links to the square item on the stack, and a vtable pointer that links to a
Draw for Square vtable on the right hand side of the diagram.  This vtable has a single content marked bounds, pointing
to a rectangle representing the code of Square::bounds().  The bottom stack item is also a trait object, containing an
item pointer that also links to the square item on the stack, but whose vtable pointer links to a Shape for Square
vtable on the right hand side of the diagram.  This vtable has three contents, labelled render_in, render and
bounds.  Each of these vtable contents has a link to a different rectangle, representing the code for
Square::render_in(), Shape::render() and Square::bounds() respectively.  The rectangle representing
Square::bounds() therefore has two arrows leading to it, one from each of the vtables.

Figure 3-5. Trait objects for trait bounds, with distinct vtables for Draw and Shape

Comparing this with the previous diagram, it's also clear that an explicitly constructed &dyn Any trait object doesn't help. Any allows recovery of the original concrete type of the underlying item, but there is no runtime way to see what traits it implements, or to get access to the relevant vtable that might allow creation of a trait object.

So what's available instead?

The primary tool to reach for is trait definitions, and this is in line with advice for other languages—Effective Java Item 65 recommends, "Prefer interfaces to reflection". If code needs to rely on the availability of certain behavior for an item, encode that behavior as a trait (Item 2). Even if the desired behavior can't be expressed as a set of method signatures, use marker traits to indicate compliance with the desired behavior—it's safer and more efficient than (say) introspecting the name of a class to check for a particular prefix.

Code that expects trait objects can also be used with objects having backing code that was not available at program link time, because it has been dynamically loaded at runtime (via dlopen(3) or equivalent)—which means that monomorphization of a generic (Item 12) isn't possible.

Relatedly, reflection is sometimes also used in other languages to allow multiple incompatible versions of the same dependency library to be loaded into the program at once, bypassing linkage constraints that There Can Be Only One. This is not needed in Rust, where Cargo already copes with multiple versions of the same library (Item 25).

Finally, macros—especially derive macros—can be used to auto-generate ancillary code that understands an item's type at compile time, as a more efficient and more type-safe equivalent to code that parses an item's contents at runtime. Item 28 discusses Rust's macro system.

Upcasting in Future Versions of Rust

The text of this Item was first written in 2021, and remained accurate all the way until the book was being prepared for publication in 2024—at which point a new feature is due to be added to Rust that changes some of the details.

This new "trait upcasting" feature enables upcasts that convert a trait object dyn T to a trait object dyn U, when U is one of T's supertraits (trait T: U {...}). The feature is gated on #![feature(trait_upcasting)] in advance of its official release, expected to be Rust version 1.76.

For the preceding example, that means a &dyn Shape trait object can now be converted to a &dyn Draw trait object, edging closer to the is-a relationship of Liskov substitution. Allowing this conversion has a knock-on effect on the internal details of the vtable implementation, which are likely to become more complex than the versions shown in the preceding diagrams.

However, the central points of this Item are not affected—the Any trait has no supertraits, so the ability to upcast adds nothing to its functionality.