Implementing Runtime Polymorphism in Mojo

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:

  1. Type Erase concrete structs into a generic Object container.

  2. Store heterogeneous types (e.g., Foo and Bar) in a single List.

  3. Perform Dynamic Dispatch via manually constructed VTables.

  4. dyn_cast objects 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 Dict that 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:

  1. Borrowed/Zero-Overhead: Wrapping pointers to stack objects (as seen above).

  2. 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!

3 Likes

This is really cool, it’s great to see this come together!

Thanks! It wouldn’t come together so easily without the recent updates on global constants and default trait methods. The mojo team is really amazing!