Hi everyone!
I’ve been experimenting with Mojo’s low-level capabilities and wanted to share a proof-of-concept library I’ve been working on.
While Mojo has excellent support for static polymorphism (parameterization), I found myself missing the flexibility of runtime polymorphism found in other languages—like Box<dyn Trait> in Rust or std::any in C++.
I decided to implement a Dynamic Interface System that allows for type erasure, heterogeneous collections, and runtime dynamic casting.
What it does
This system allows you to:
-
Type Erase concrete structs into a generic
Objectcontainer. -
Store heterogeneous types (e.g.,
FooandBar) in a singleList. -
Perform Dynamic Dispatch via manually constructed VTables.
-
dyn_castobjects between interfaces at runtime using a global registry.
How it works under the hood
The core logic relies on a few key Mojo features:
-
Object&Interface: A fat-pointer layout storing the data pointer and a pointer to a VTable. -
Comptime VTable Generation: VTables are generated at compile-time as static arrays of function pointers.
-
Global Registry: A runtime
Dictthat maps(InterfaceID, ConcreteTypeID)to the specific VTable, enabling safe runtime casting. -
Trampolines: Helper functions to bridge opaque pointers back to concrete types.
Usage Example
Here is a quick look at how it looks in practice:
# 1. Define a Trait and a Dynamic Wrapper
trait Testable(Copyable, Movable):
fn test(self) -> Int: ...
struct AnyTestable(Interface, Testable):
# ... implementation details (vtable setup, trampolines) ...
# 2. Use it polymorphically
fn main():
var bar = Bar(6)
var foo = Foo(7)
# Store different types in the same list!
var list: List[AnyTestable] = [
AnyTestable(UnsafePointer(to=bar)),
AnyTestable(UnsafePointer(to=foo)),
]
for obj in list:
print(obj.test()) # Dynamic dispatch happens here
# 3. Dynamic Casting
# If we register that Bar implements Stringable...
register_interface[AnyStringable, Bar]()
if dyn_str := list[0].dyn_cast[AnyStringable]():
print(dyn_str.value().__str__()) # Works!
Memory Management
The system supports two modes:
-
Borrowed/Zero-Overhead: Wrapping pointers to stack objects (as seen above).
-
Owned/RAII: Using an owning constructor and
__copyinit__to handle heap allocation and deep copying, effectively acting like a smart pointer.
Code & Feedback
I’m currently refining the API and adding more tests. I’d love to hear your thoughts on this approach or suggestions on how to improve the VTable mechanism!
Thanks!