A toolkit providing convenient abstractions for trait object operations that aren't supported by rust's trait system directly, such as cloning, comparison, and conversion
| Feature | Status | Description |
|---|---|---|
clone_box |
✅ Stable | clone_box pattern |
obj_eq |
🚧 Unstable | equality comparisons for trait objects |
as_super |
🚧 Unstable | a shorthand for as_foo(&self) -> &dyn Foo |
as_any |
🚧 Unstable | downcasting to Any for trait objects |
This crate provides procedural macros that enhance rust traits by enabling operations that aren't natively supported for trait objects. Currently, the sole ready feature is the
clone_box attribute, which enables cloning of trait objects with minimal abstraction overhead beyond the unavoidable dynamic dispatch.
use objkit::clone_box;
#[clone_box]
pub trait MyTrait {}Here's a simple example showing how to use the clone_box attribute to create clonable trait objects without the boilerplate:
use objkit::clone_box;
#[clone_box]
pub trait Animal {
fn speak(&self) -> String;
}
#[derive(Clone)]
struct Dog {
name: String,
}
impl Animal for Dog {
fn speak(&self) -> String {
format!("{} says: Woof!", self.name)
}
}
#[derive(Clone)]
struct Cat {
name: String,
}
impl Animal for Cat {
fn speak(&self) -> String {
format!("{} says: Meow!", self.name)
}
}
fn main() {
// create a vector of trait objects
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog { name: "Buddy".to_string() }),
Box::new(Cat { name: "Whiskers".to_string() }),
];
// clone the vector of trait objects
// (this is where the clone_box pattern comes in handy)
let cloned_animals = animals.clone();
// both vecs work as you'd expect
for animal in &animals {
println!("Original: {}", animal.speak());
}
for animal in &cloned_animals {
println!("Cloned: {}", animal.speak());
}
}You can use the clone_box method directly or access it through the standard Clone trait:
// using standard clone trait (which the macro handles for you)
let cloned = my_trait_object.clone();
// using the explicit method
let cloned = my_trait_object.clone_box();For those new to rust's trait object limitations:
In rust, trait objects (dyn Trait) have fundamental limitations due to type erasure and rust's object safety rules. Specifically:
-
Trait objects cannot automatically implement marker traits like
Clone,Eq, orHash, even when every possible implementor satisfies these bounds, because:- The concrete type information is erased at runtime (stored only as a vtable pointer)
- The compiler cannot verify at compile time that all current and future implementors will satisfy these bounds
- rust's trait object design intentionally limits which methods are accessible through the vtable
-
The
Box<dyn Animal>cannot be cloned even when all concrete types implementClonebecause:- The
Cloneimplementation would need to know the concrete type to call its specific clone method - The trait object vtable only contains entries for methods explicitly defined in the trait itself
- The
-
Traditional workarounds require:
- Manual auxiliary traits with explicit
clone_box-style methods - Complex trait bounds and blanket implementations
- Careful attention to object safety concerns
- Sometimes unsafe code for downcasting via
Anyor similar mechanisms (with potential performance penalties)
- Manual auxiliary traits with explicit
These limitations can make working with trait objects cumbersome in scenarios where operations like cloning (currently handled with the
clone_box pattern macro), comparison (todo), or conversion (todo) are needed.
Click to expand initial listing (not necessarily accurate at this point anymore)
-
Type-system friendly: Creates auxiliary trait implementations that work with rust's type system to keep static dispatch for concrete types, only using dynamic dispatch at trait object boundaries where it's unavoidable.
-
Static type guarantees: Maintains, where possible, rust's type system through trait bounds, for example for the clone_box pattern, by enforcing implementors be
Clone + 'staticwithout runtime checks. -
Minimal overhead abstractions:
Introduces no overhead beyond the inherent dynamic dispatch required when working with trait objects. Avoids additional indirection layers or heap allocations that would degrade performance compared to a manually written implementation.NOTE: right now this is a work in progress and does not necessarily hold true -
Reduces manual boilerplate: Replaces error-prone manual auxiliary traits, blanket implementations, and explicit method forwarding typically needed for the clone_box pattern.
-
Optimized dispatch implementation: Implements patterns like clone_box using direct trait method calls rather than type erasure techniques such as
Anydowncasting. This approach produces more analyzable IR for compiler backends, avoiding additional optimization barriers beyond the inherent limitations of trait objects. -
Centralized implementation: Consolidates some potentially complex trait implementation details in a single location, eliminating duplicated logic across different traits requiring the same pattern (and potential for user error/inconsistent implementations because of that).
-
Focus on preserving object safety: Avoids self-referential methods, associated types without bounds, or other features that would violate object safety requirements.
-
Procedural Macro Dependency: Adds a procedural macro dependency to your project, which will increase compile times, even if only slightly. They can easily build up, so be mindful of that.
-
Additional Generated Traits: Creates auxiliary traits in your codebase that could potentially lead to name conflicts or increase the binary size.
-
Implicit Code Generation: The auto-generated implementations may make it less obvious what's happening under the hood compared to manual implementations. But that's also a pro. It's a two-edged sword.
-
Still Developing Features: Currently only implements the clone_box pattern, with other patterns still in planning.
-
Trait Object Limitations: Still bound by rust's fundamental trait object constraints. Not a magic bullet, just a convenience for some common patterns.
-
Strict Trait Bounds: Imposes specific trait bounds (like
Clone + 'static) which might be more restrictive than a bespoke manual implementation for some cases. -
Learning Curve for Debugging: Requires understanding the underlying pattern to effectively work through possible issues. Some of the quirks that come with the territory may not be immediately obvious to those who don't know the pattern, which can cause frustration.
Note: Performance benchmarks comparing this implementation to manual approaches will be added before the first minor release
0.1. Current "minimal overhead" claims are based on analysis of the generated code rather than quantitative measurements.
This crate requires rust 1.64.0 or later.
For practical reasons, we pin the msrv there to utilize cargo's stabilized
workspace-inheritance feature, but also to remain fairly compatible.
Minor versions may have breaking changes, which can include bumping msrv.
Patch versions are backwards compatible, so using version specifiers such as ~x.y or ^x.y.0 is safe.
Whether you use this project, have learned something from it, or just like it, please consider supporting it by buying me a coffee, so I can dedicate more time on open-source projects like this :)
The project is licensed under the Mozilla Public License 2.0.
SPDX-License-Identifier: MPL-2.0
You can check out the full license here
