- Lab
- Core Tech

Guided: Rust's Advanced Type System
Enhance your Rust skills by diving into its advanced type system features—trait objects, associated types, and higher-rank trait bounds. In this hands-on lab, you’ll build a flexible plug-in architecture that showcases these features. This lab is designed to help Rust developers write safer, more flexible code by leveraging Rust’s type system to its full potential.

Path Info
Table of Contents
-
Challenge
Introduction
Overview
This Code Lab explores advanced features of the Rust type system, including:
- Associated types for abstracting implicit type definitions for trait implementations
- Trait objects for dynamic polymorphism (via dispatch) and object-safety
- Higher-ranked Trait Bounds (HRTBs) for working with generic function signatures with closures
Scenario
For this lab, you will start with a minimally bootstrapped Rust project built with Cargo. Your task is to design an extensible plugin framework in which each plugin is a modular component with its own behavior, accepts different types of input, and can even call back into your system via closures for additional functionality.
Additional Information
There is a
solution
directory which you can reference at any time to check your implementation. You can run the project as you progress with thecargo run
command within the Terminal. -
Challenge
Understanding Associated Types and Trait Objects
Before you define your
Plugin
trait, it's crucial to understand two of the key Rust features that underpin this exercise: associated types and trait objects.
Associated Types
Associated types are a way for traits to declare placeholder types that implementors can later specify. Instead of requiring a trait to be generic over multiple types (
T
,U
, etc.), you declare associated types inside the trait definition itself. This keeps trait signatures cleaner and shifts type specification to the implementation.// generic trait Example<T, U> {} // function must also be generic over T, U fn example<T, U, E: Example<T, U>>(){} // associated types trait Example { type T type U } fn example<E: Example>(){}
Trait Objects
Trait objects enable runtime polymorphism using dynamic dispatch, letting you write code that can operate on multiple types through a shared trait. This is in contrast to static dispatch, where all type information is resolved at compile time.
trait Greeter { fn greet(&self); } // static dispatch. Compile-time resolution. Faster and allows inlining, but each monomorphized version increases binary size. fn greet_static<T: Greeter>(g: T) { g.greet(); // compiled directly to the correct method } // dynamic dispatch. Resolved at runtime via vtable. More flexible, but incurs a runtime cost and prevents some optimizations. Utilizes `dyn` keyword. fn greet_dynamic(g: &dyn Greeter) { g.greet(); // resolved via vtable }
Object Safety
Not all traits can be turned into trait objects. A trait object must be object-safe, meaning it:
- Must not have any associated types with generics
- Methods cannot be generic nor return
Self
- Any methods that defy the point above must be made explicitly non-dispatchable (defined with a bound so that it cannot be used with dynamic dispatch)
-
Challenge
Implementing Plugins
At the core of this exercise is the Plugin trait, which provides a unified interface for all plugins in the system. Each plugin defines its own input and output types via associated types, and implements two methods:
name(&self) -> &str
: Identifies the pluginexecute(&self, input: Self::Input) -> Self::Output
: Defines the plugin's primary behavior
This enables flexible plugin behavior with strong type guarantees, without requiring generic parameters at each use site.
Also note that although the
execute
method usesSelf::Output
(an associated type), this does not violate object safety.Self::Output
refers to a concrete output type known at the time the trait is used, not a generic type parameter. This is not to be mistaken with returningSelf
, which would violate object-safety.
Implementing
Plugin
Head over to
src/plugin.rs
and implement thePlugin
trait.Plugin Instructions
1. Define a `trait` called `Plugin` and mark it as public with `pub`. 2. Give it two associated types called `Input` and `Output`. 3. Define the `name` and `execute` function signatures as specified in the previous section.
Implementing
LoggerPlugin
Now head to
src/plugins/logger.rs
and implement thePlugin
trait forLoggerPlugin
.LoggerPlugin Instructions
1. Import `Plugin` with `use crate::plugin::Plugin;`. 2. Define an `impl Plugin` block. 3. Define `Input` as a `String` and `Output` as `()`. 4. Return `"LoggerPlugin"` within the `name` function and `println!("[{}] {}", "Logger Output", input);` within the `execute` function.
Implement
MathPlugin
Now head to
src/plugins/math.rs
and implement thePlugin
trait forMathPlugin
.MathPlugin Instructions
1. Import `Plugin` with `use crate::plugin::Plugin;`. 2. After the `impl MathPlugin` block, define an `impl Plugin` block. 3. Define `Input` as a tuple of 2 `i32`s and `Output` as an `i32`. 4. Return `"MathPlugin"` within the `name` function. 5. Define a 2 value tuple set to the `input` parameter and do a `match` statement for `self.operation`. 6. If `self.operation` is an add operation, return the sum of the values. If it's a multiply operation, return the product of the values.
Enabling Plugins in
main
Now head to
src/main.rs
and enable your plugins to be used in themain
method to display their functionality.Instructions
1. Import `Plugin` with `use plugin::{Plugin}` at the top. 2. Uncomment all the `println!` statements underneath the `LoggerPlugin` and`MathPlugin` comment sections. 3. Before the `println!` under the `LoggerPlugin` section, define a `logger_obj` variable typed as `Box>`. Set its value to `Box::new(LoggerPlugin::new(LogLevel::Info))`. 4. After the logger `println!`, call `execute()` on `logger_obj` and pass in `"lorem ipsum via trait object\n".to_string()`, or any other `String` of your choosing. 5. In the `MathPlugin` section, create a `sum` variable set to the value of `adder.execute()` and pass in a tuple with values 5 and 7. This variable should be defined between the two `println!` statements. 6. Do the same as step 5 for the multiplier instance of `MathPlugin`. After enabling these plugins, run the program with
cargo run
and you should see the following spoiler in your Terminal.Spoiler
Running plugin (trait object): LoggerPlugin [Logger Output] lorem ipsum via trait object Running plugin: MathPlugin Sum result: 12 Running plugin: MathPlugin Product result: 35
-
Challenge
Higher-Rank Trait Bounds
In this step, you will introduce higher-ranked trait bounds (HRTBs) into your plugin framework. HRTBs allow you to write function signatures that accept closures (or other generic types) that must be valid for all possible lifetimes—a crucial feature when working with borrowed data whose lifetime cannot be known in advance.
Consider a plugin that wants to call a user-provided callback with an internal string like its name or description. The plugin holds this string internally, meaning the callback must accept a borrowed string. The callback must then assume a specific lifetime, but you don’t necessarily know what that lifetime is—which abstraction and makes borrowing difficult.
To solve this, you can use the HRTB syntax via
for<'a>
. This tells the compiler: “This closure must accept a &str of any lifetime.” Now, whether the plugin's internal string is a long-lived reference or a temporary borrow, it safely compiles and works correctly.
Implementing
PluginWithCallback
Head over to
src/plugin.rs
and implement thePluginWithCallback
trait.PluginWithCallback Instructions
1. Define a `trait` called `PluginWithCallback` and mark it as public with `pub`. 2. Define a `with_callback_mut` method with the following method signature: `fn with_callback_mut(&self, callback: F) where F: for<'a> FnMut(&'a str);`
Implementing
EchoPlugin
Now head to
src/plugins/echo.rs
and implement thePlugin
andPluginWithCallback
trait forEchoPlugin
.EchoPlugin Instructions
1. Import `Plugin` and `PluginWithCallback` using `use crate::plugin::{Plugin, PluginWithCallback};`. 2. Define a `impl Plugin` block and implement the `Plugin` trait similar to the previous plugins. Use `String` for both input and output types, return `"EchoPlugin"` within the `name()`, and return the `input` parameter within `execute()`. 3. Define a `impl PluginWithCallback` block and implement the `with_callback_mut()` method. Within the method body, define a `message` variable set to `format!("{}{}", self.prefix, "callback triggered");`, then do `callback(&message)`.
Putting it into
main
Now head to
src/main.rs
and incorporate theEchoPlugin
and it's HRTB generic method.Instructions
1. Import `PluginWithCallback` with `use plugin::{Plugin, PluginWithCallback}` at the top. 2. Uncomment all the `println!` statements underneath the `EchoPlugin` comment sections. 3. Beneath the `use callback` comment section, call `with_callback_mut` on the `echo` plugin. 4. Pass in the following closure and callback function: `|msg| { println!("Callback received message: {}", msg); }`After setting up
EchoPlugin
, run the program withcargo run
and you should see the following spoiler in your Terminal.Spoiler
Running plugin (trait object): LoggerPlugin Logger Output] lorem ipsum via trait object Running plugin: MathPlugin Sum result: 12 Running plugin: MathPlugin Product result: 35 Running plugin: EchoPlugin Echoed output: Hello! Callback received message: [Echo] callback triggered
Note: The
with_callback_mut
function that utilizes a HRTB is implemented as a generic method, which means that thePluginWithCallback
trait is not object-safe. Any types that implement this trait cannot be a trait object, and attempting to do so would throw an error saying it isdyn-incompatible
. This only applies to the example shown in this lab as it is possible to implement an HRTB on a method while keeping the trait object-safe. -
Challenge
Conclusion
Great job on completing this lab!
In doing so, you explored advanced type system features in Rust by building a modular plugin framework. You used trait objects with associated types to enable runtime polymorphism and flexible plugin composition. You introduced higher-ranked trait bounds (HRTBs) to support callbacks that accept references with arbitrary lifetimes. Along the way, you examined object safety and contrasted the pros and cons of dynamic vs. static dispatch.
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.