Typed keys as dyn-dispatchable method receivers

A lot of Rust game engines rely on deferring actions or on interior mutability, and one of the main reasons is:

trait GameComponent {
    fn tick(&mut self, world: &mut World) {}
}

This can't compile in a useful way. the component lives inside the world, so self and world are overlapping mutable borrows. The borrow checker is right to complain.

Enums sidestep this since the match hands you the concrete type, so you control borrow scopes yourself and can re-look-up after touching the world. But an engine library can't use enums. Users of a game engine library have to define their own behaviors, and enums are a closed set. So you eventually need trait objects, and then you're back at the borrow conflict, since you have to have a reference to the trait object to make it work, which is a part of World, which disqualifies &mut World access since ur already borrowing the trait object that is a part of the world. The only way in stable rust to avoid this issue is defering game actions till later, or using ECS

The idea: the receiver is a key, not a borrow

I built a slotmap variant where keys are typed, upcastable, and carry vtable metadata, so you can do dynamic dispatch through the key while &mut World stays free:

rust

#![feature(ptr_metadata, coerce_unsized, unsize, dispatch_from_dyn,
           arbitrary_self_types, arbitrary_self_types_pointers)]
use cast_slotmap::{BoxCastMap, TypeTaggedBox, CastKey, DefaultKey};
use std::any::Any;

struct Dog { name: String }

let mut map: BoxCastMap<DefaultKey, dyn Any> = BoxCastMap::new();

// Insert a concrete type into a `dyn Any` map; the key comes back typed.
let dog_key: CastKey<Dog> = map.insert_sized(TypeTaggedBox::new(Dog { name: "Rex".into() }));
assert_eq!(map.get(dog_key).unwrap().name, "Rex");

rust

trait GameComponent: AnyHaver {
    // `self` is a *key*, not a borrow, so `&mut World` is free to coexist.
    fn tick(self: DynKey<'_, Self>, world: &mut World) {}
}

struct World {
    components: BoxCastMap<DefaultKey, dyn GameComponent>,
}

impl GameComponent for Dog {
    fn tick(self: DynKey<'_, Self>, world: &mut World) {
        let me: &mut Dog = world.components.get_mut(self.key()).unwrap();
        me.name.push('!');
        
        // borrow of `me` ends whenever you choose; then touch the rest
        // of the world, look up other components by their keys, etc.
    }
}

fn tick_all(world: &mut World) {
    // snapshot the keys, releasing the map borrow
    let keys: Vec<CastKey<dyn GameComponent>> = world.components.keys().collect();
    for key in keys {
        key.as_dyn().tick(world); // virtual call through the vtable cached in the key
    }
}

Result: no deferral, no RefCell, no reentrancy panics, and World stays Send + Sync.

Status and caveats

  • This is a feasibility proof, not a "use this today" crate. It needs six nightly features (ptr_metadata, coerce_unsized, unsize, dispatch_from_dyn, arbitrary_self_types, arbitrary_self_types_pointers). The point is that Rust has a potential to express this pattern
  • Ergonomics aren't ideal yet: you call as_dyn() before dispatch because DynKey has to borrow the CastKey, due to DispatchFromDyn requiring pointer sized implementirs. Though this ergonomic gap can be resolved with more compiler support
  • Anticipating some replies: RefCell trades the compile error for reentrancy panics, still can't insert while borrowing world, and disqualifies your world from using Send + Sync. ECS avoids this problem differently and is out of scope. this targets non-ECS designs.

Repo: GitHub - izagawd/cast_slotmap · GitHub

The library is generic along several axes, not just the example path, which is why there is a good amount of code:

  • Stored pointer: the CastMap store any TypeTaggedPtr<P> where P can be one of Box, Rc, Arc, &T, &mut T TypeTaggedBox is just an alias for TypeTaggedPtr<Box<T>>
  • Backing storage: BoxCastMap<K, T> is just an alias: CastMapG<SlotMap<K, TypeTaggedBox<T>>>. The real type is CastMapG<M: SlotMapTrait>. You can swap it to use the DenseSlotMap variant

I'd appreciate feedback on the design itself. (Disclosure: Claude assisted with the implementation, so the docs may be verbose.)

I know how the entire library works at the back of my hand, and I have ensured that it follows exactly what I envisioned. as for the documentation, I have already thoroughly reviewed them. I did not just tell claude to write everything and have no understanding of how it works. Your assumptions felt passive aggressive

Perhaps I am being passive aggressive, but I am also 100% serious. I do not want to look at anything Claude (or any other LLM) wrote, period. I don't care how thoroughly you reviewed it.

There's been a lot of low-effort AI content lately, but I don't think all AI-assisted work is bad. Some people use it wisely, but I understand where you are coming from

I think all generative-LLM-assisted work, no matter how much human care and attention has also been put into it, is at best mediocre, and I'm no longer interested in putting any of my own human care or attention into anything that's been capped at mediocre. Life is simply too short.

And therefore, I think asking humans to look at generative-LLM-assisted work is inherently disrespectful of those humans' time, and that's why I'm bothering to call you out on this. The more you do it, the less people will want to help you, or even talk to you at all.

2 Likes

think all generative-LLM-assisted work, no matter how much human care and attention has also been put into it, is at best mediocre

That's technically wrong in a variety of contexts. Short code generated with AI is easy to verify and refine. And not everyone is sending one prompt and shipping it immediately when it works.

And therefore, I think asking humans to look at generative-LLM-assisted work is inherently disrespectful of those humans' time, and that's why I'm bothering to call you out on this.

More and more people are using AI to assist in their code. Saying it is "disrespectful" to someone's time is a huge stretch IMO. It's only wasting someone's time if it was shipped with little to no refinement. I personally think that you are overly critical in this regard, but I am going offtopic from the original post

I disagree, and you will not persuade me otherwise.

If you're also unwilling to be persuaded, perhaps we should leave it there.