Retrieving components as a concrete type from an ECS


#1

Hi everyone,

I’ve been stuck trying to solve this issue for quite a while now so I think I should reach out here for some advice. I’m trying to write a simple Entity Component System in Rust and as such I have an Entity object which stores a vec of different Components which all have a Component trait. The goal is that I’d like to call get_component along with an argument of the type that I would like to find and return that type from the entity if it is found. I’m also heavily depending on Rc<RefCell> to store and pass my types around.

The problem is that just after I call get_component and get an Rc<RefCell> I then try to borrow_mut that object (See the “main” fn of the code below) and I get a runtime error telling me that it’s already mutably borrowed “thread ‘main’ panicked at ‘already borrowed: BorrowMutError’”. The issue does not occur when I simply call borrow instead of a mut borrow.

So I think there is somewhere in my code where a borrow_mut has not gone out of scope or been un-borrowed so that I can borrow it again in the main function. Any ideas? I’m also happy for my approach to change as long as the basic concept of searching for specific types in a vec and returning them as a concrete type is preserved.

Playground

use std::mem::*;
use std::cell::*;
use std::rc::*;

#[derive(Clone, Copy, PartialEq, Debug)]
pub enum ComponentType {
    Transform,
    MeshBufferGL,
}

pub trait Component
{
    fn get_type(&self) -> ComponentType;
}

struct TransformComponent {}

struct MeshBufferGLComponent {}

impl MeshBufferGLComponent {

    fn unique_method_for_meshbuff(&self) {
        println!("unique_method_for_meshbuff was called");
    }
}

impl Component for TransformComponent {
    fn get_type(&self) -> ComponentType {
        ComponentType::Transform
    }
}

impl Component for MeshBufferGLComponent {
    fn get_type(&self) -> ComponentType {
        ComponentType::MeshBufferGL
    }
}

pub struct Entity {
    components: Vec<Rc<RefCell<Component>>>
}

impl Entity
{
    pub fn new() -> Entity
    {
        Entity {
            components: Vec::new(),
        }
    }

    pub fn add_component<T: 'static>(&mut self, component: Rc<RefCell<T>>) where T: Component {
        self.components.push(component.clone());
    }

    pub fn get_component<T>(&self, component_type: ComponentType) -> Option<Rc<RefCell<T>>> where T: Component {
        let mut result: Option<Rc<RefCell<T>>> = None;

        for comp in self.components.iter() {
            let a = comp.borrow();

            if a.get_type() == component_type {
                println!("'{:?} Type found' -> Downcasting to {:?} with transmute", component_type, component_type);
                unsafe { result = Some(transmute::<_, Rc<RefCell<T>>>(&(comp.clone()))); }
            }
        }

        result
    }


    pub fn num_components(&self) -> usize {
        self.components.len()
    }
}

fn main () {
    let transform_component: TransformComponent = TransformComponent{};
    let meshbuffer_component: MeshBufferGLComponent = MeshBufferGLComponent{};

    let transform_component_rc = Rc::new(RefCell::new(transform_component));
    let meshbuffer_component_rc = Rc::new(RefCell::new(meshbuffer_component));

    let mut entity = Entity::new();
    entity.add_component(transform_component_rc.clone());
    entity.add_component(meshbuffer_component_rc.clone());

    let mesh_buffer_retrieved = entity.get_component::<MeshBufferGLComponent>(ComponentType::MeshBufferGL).unwrap();

    //let borrow = mesh_buffer_retrieved.borrow(); // NOTE: Using this standard borrow instead of the mut borrow below does not cause an error.
    let borrow = mesh_buffer_retrieved.borrow_mut(); // thread 'main' panicked at 'already borrowed: BorrowMutError' ...

    borrow.unique_method_for_meshbuff();
}

Thank you


#2

Don’t use transmute. The conversion you’re doing is invalid. Seriously, never use transmute.

You might be able to accomplish what you want with the Any trait and Box::downcast. Or use the typemap crate.


#3

To elaborate on what @jethrogb said, you have the following bogus code:

  1. transmute called on a reference (i.e. pointer) to a local that’s going to be dropped. It’s morally equivalent to just doing transmute::<_, Rc<RefCell<T>>>(123456789u64). It just so happens that Rc<RefCell<T>> size is 8 and that’s also the size of the thin reference (pointer) you get from &(comp.clone()). In addition, the refcount isn’t properly reflecting how many owners there are (because the temp is dropped). So even if this code was somehow correct in all other aspects, you could end up with a dangling reference.

  2. The Rc<RefCell<Component>> is storing an unsized (i.e. erased) type. When you add those concrete Rc<RefCell<...>> to the vec, the compiler unsizes them by storing a fat pointer (essentially a trait object). This means what’s inside there isn’t a &T (but a fat pointer).

Here is an example of how you might achieve what you want, sticking with Rc and RefCell. Note that the RefCell in there makes the code noisier than it would be otherwise.

You may want to rethink your design to not rely on (effectively) reflection. Perhaps you can store your components in an enum?


#4

I can’t recommend storing components in a heterogenous list. Instead, the Entity should be an id and for each component you have a homogeneous list. So the entities are basically just indices for the component storages. That is easier to implement and more efficient :slight_smile:

Are you sure you don’t want to take a look at another ECS? As maintainer of Specs I can give you a completely unbiased recommendation :wink: Make sure to check out our Gitter channel if you have any questions.


#5

Thanks Jethrog, I’ll try to steer clear of transmute and try those other options!


#6

Thanks Vitalyd, looks like have made a spectacular example of how to shoot yourself in the foot at not quite know it. I’ve been playing around with your example and wondering if there is some way to do the downcast within the get_component call so that it doesn’t have to be done after each call to get_component in any calling code.

Anyway I very appreciate the safe example that you have given me that doesn’t use transmute.


#7

Thanks torkleyy, I have heard of that homogeneous approach and it’s probably what I should be doing too. Although I may persist with this worse re-invention of the wheel for a bit just to become more familiar with Rusts type system and down-casting.

Thanks for the link to your Specs lib. Looks really cool. I’ll take it for a spin some time :slight_smile: