How do you save a reference to an object without taking ownership of it? ie. For Wrapper Struct? Named lifetime parameter?

I am creating some Rapier physics objects for a simulation like they suggest here:
https://www.rapier.rs/docs/user_guides/rust/getting_started/

The trouble is I need to save references to the Collider and RigidBody objects. And I can't take ownership of these because the Rapier system seems to want ownership (the functions take the objects themselves, not Arc or anything else).

So if I have an idea like my own Struct to wrap these elements for what I need to read and manipulate them with:

pub struct BallSphere {

    object_id: String,
    rigid_body: &mut RigidBody,
    collider: &mut Collider
}
impl BallSphere{
    pub fn new(object_id: String)-> Self {
        let mut ball_sphere = BallSphere {
            object_id: object_id,
            rigid_body: None, //need to make option if don't want to initialize with something, or set it in as argument on creation
            collider: None //need to make option if don't want to initialize with something, or set it in as argument on creation
        };
        return ball_sphere;
    }
}

I have thus tried the following:

        // Create the bounding ball. 
        let rigid_body = RigidBodyBuilder::dynamic().translation(vector![0.0, 10.0, 0.0]).build();
        let collider = ColliderBuilder::ball(0.5).restitution(0.7).build();
        
        //save extra references
        let mut_collider = &mut collider;
        let mut_rigid_body = &mut collider;

        //set into Rapier system (it takes ownership)
        let ball_body_handle = physics_unit.rigid_body_set.insert(rigid_body); //WORKS
        physics_unit.collider_set.insert_with_parent(collider, ball_body_handle, &mut physics_unit.rigid_body_set); //WORKS

        //create my own wrapper struct:
        let mut ball_sphere : BallSphere = BallSphere::new("ball1".to_string());
        ball_sphere.collider = mut_collider; //doesn't work
        ball_sphere.rigid_body = mut_rigid_body; //doesn't work

However, this is not working. It tells me:

error[E0106]: missing lifetime specifier
  --> src/lib.rs:78:15
   |
78 |     collider: &mut Collider,
   |               ^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter
   |
72 ~ pub struct BallSphere<'a> {
73 |
...
77 |     rigid_body: &mut RigidBody,
78 ~     collider: &'a mut Collider,
   |

I do not know if I have the right approach here at all. I understand I can't use Arc here on the Collider and RigidBody because that would take ownership of the objects (and then they can't be passed into the base Rapier functions that want to take ownership of them).

How do I keep a second mutable reference to these objects in case I want to manipulate them from my struct wrapper after they are created?

Thanks for any help.

(I'm not familiar with the crate, I just looked at the docs.)

Moving a value -- for example, giving up ownership into a RigidBodySet -- invalidates any references to it. You need a way to get references to the RigidBodys after they're part of the set instead.

When you insert into the set, it gives you a handle back. You can then use the handle to get a reference to the RigidBody out. Or iterate over all bodies if you need to manipulate more than one at a time. ColliderSet looks to have a similar API.

So you would store the handles, and then when you need to operate on them, get the &muts. Untested, but something like:

pub struct BallSphere {
    object_id: String,
    rigid_body: RigidBodyHandle,
    collider: ColliderHandle,
}

impl BallSphere {
    fn do_something(&self, rset: &mut RigidBodySet, cset: &mut ColliderSet) {
        let rigid_body = rset.get_mut(self.rigid_body).expect("...");
        let collider = cset.get_mut(self.collider).expect("...");
        // ...
    }
}
4 Likes

Thank you! This handle concept seems to be what they have done in the demo code.

THEIR HANDLE USE

It says: https://www.rapier.rs/docs/user_guides/rust/getting_started/

    /* Create the bounding ball. */
    let rigid_body = RigidBodyBuilder::dynamic().translation(vector![0.0, 10.0, 0.0]).build();
    let collider = ColliderBuilder::ball(0.5).restitution(0.7).build();

    //returns RigidBodyHandle:
    let ball_body_handle = rigid_body_set.insert(rigid_body); 

    //returns ColliderHandle:
    collider_set.insert_with_parent(collider, ball_body_handle, &mut rigid_body_set);   

    //then to get a read on this object's position:
    let ball_body = &rigid_body_set[ball_body_handle]; //use the handle to access
    println!("Ball altitude: {}", ball_body.translation().y);

So it appears this code does provide a valid potential handler for both elements (collider and rigid body) in just the above. They are just not saving both handles as not using both.

I was able to figure this all out from what you said. I documented my research and solution below in part for my own future reference, though I have a few minor questions as well if you don't mind. Either way, thanks already.

WHAT IS THE HANDLE TYPE HERE?

The source code for RigidBodySet & ColliderSet shows these handles that are returned are made in a similar way by in each of these pages:

        let handle = ColliderHandle(self.colliders.insert(coll)); //for ColliderSet
        let handle = RigidBodyHandle(self.bodies.insert(rb)); //for RigidBodySet

To trace this further, bodies and colliders are:

    pub(crate) colliders: Arena<Collider>, //for ColliderSet
    pub(crate) bodies: Arena<RigidBody>, //for RigidBodySet

Arena is defined here where it says:

/// The `Arena` allows inserting and removing elements that are referred to by `Index`.
pub struct Arena<T> {
    items: Vec<Entry<T>>, //THE ACTUAL VECTOR HOLDING THE ELEMENTS
    generation: u32,
    free_list_head: Option<u32>,
    len: usize,
}
pub fn insert(&mut self, value: T) -> Index { //THE INSERT COMMAND BEING RUN
        match self.try_insert(value) {
            Ok(i) => i,
            Err(value) => self.insert_slow_path(value),
        }
    }
#[inline]
    pub fn try_insert(&mut self, value: T) -> Result<Index, T> {
        match self.try_alloc_next_index() {
            None => Err(value),
            Some(index) => {
                //INSERT TO ACTUAL VECTOR HERE
                self.items[index.index as usize] = Entry::Occupied { 
                    generation: self.generation,
                    value,
                };
                Ok(index)
            }
        }
    }

So this is where we have the core vector (inside Arena) storing the objects and the insert function returns a Index struct.

The Index struct is defined here, showing it is just a wrapper for a u32 index and generation (unclear significance of generation) int:

pub struct Index {
    index: u32,
    generation: u32,
}

To return the final handle, this Index is then passed into the ColliderHandle here and RigidBodyHandle here, though I can't really understand what it is doing by passing it in here, or what it truly is returning as the handle.

pub struct RigidBodyHandle(pub crate::data::arena::Index);

impl RigidBodyHandle {
    /// Converts this handle into its (index, generation) components.
    pub fn into_raw_parts(self) -> (u32, u32) {
        self.0.into_raw_parts()
    }

    /// Reconstructs an handle from its (index, generation) components.
    pub fn from_raw_parts(id: u32, generation: u32) -> Self {
        Self(crate::data::arena::Index::from_raw_parts(id, generation))
    }

    /// An always-invalid rigid-body handle.
    pub fn invalid() -> Self {
        Self(crate::data::arena::Index::from_raw_parts(
            crate::INVALID_U32,
            crate::INVALID_U32,
        ))
    }
}

What is this in the end then? ie. What is the handle? There seems to be no core handle type in Rust, so what is it actually in this case from the above? I can't make sense of that last step in terms of what the final object is or what its structure looks like.

BOTTOM LINE CONCEPTUALLY

I suppose conceptually the bottom line is that the handle is a conceptual design strategy meant as a way to safely reference the object after it is taken control of by a system (here, Rapier).

The usage here is designed to appear to emulate a simple indexed vector (which is what I thought it was doing). In let ball_body = &rigid_body_set[ball_body_handle];, it appears as if ball_body_handle is a plain int and rigid_body_set is a plain vector. But this is not the case. It is just made to look this way for familiar function.

For example ChatGPT suggests to add then remove a Collider:

fn main() {
    // Create a ColliderSet
    let mut colliders = ColliderSet::new();

    // Create a collider and insert it into the set
    let circle = Circle::new(1.0);
    let handle: ColliderHandle = colliders.insert(circle);

    // Now, remove the collider using its handle
    colliders.remove(handle);

    // Optionally, check if the collider was removed
    if !colliders.contains(handle) {
        println!("Collider successfully removed.");
    }
}

So I don't technically need to know "how this works" inside. They have wrapped the core vectors in all these layers to provide safe control.

CHAT GPT EXPLANATION

Again ChatGPT is too smart for my tastes :). They say:

Key Features of ColliderHandle:
Unique Identifier: Each ColliderHandle is unique within the ColliderSet, meaning you can use it to identify and manipulate a specific collider.
Ownership Management: The handle abstracts away the underlying data structure, allowing you to safely remove or modify colliders without directly dealing with the internals of the ColliderSet.
Type Safety: Using a ColliderHandle ensures that operations on colliders are type-safe, preventing issues that might arise from using raw pointers or indices.

Summary
ColliderHandle is used to reference colliders within a ColliderSet.
It provides a safe and efficient way to manage colliders without dealing with raw pointers.
It ensures that each collider can be uniquely identified and manipulated.

This design pattern promotes safety and encapsulation, which are core principles in Rust.

FOR MY PURPOSES (WRAP THE HANDLE) - WORKING METHOD

My goal to wrap the objects means I must instead wrap the handles and store those or references to those instead. It seems to be working okay.

This seems to work to replace their demo code:

pub struct BallSphere {
    object_id: String,
    rigid_body: RigidBodyHandle,
    collider: ColliderHandle
}
impl BallSphere{
    pub fn new(object_id: String, body_handle: RigidBodyHandle, collider_handle: ColliderHandle)-> Self {
        let mut ball_sphere = BallSphere {
            object_id: object_id,
            rigid_body: body_handle, 
            collider: collider_handle 
        };
        return ball_sphere;
    }
}

Then usage:

       // Create the bounding ball. 
        let rigid_body = RigidBodyBuilder::dynamic().translation(vector![0.0, 10.0, 0.0]).build();
        let collider = ColliderBuilder::ball(0.5).restitution(0.7).build();
        
        //set into rapier system & get handles
        let ball_body_handle = physics_unit.rigid_body_set.insert(rigid_body); //WORKS
        let ball_collider_handle = physics_unit.collider_set.insert_with_parent(collider, ball_body_handle, &mut physics_unit.rigid_body_set); //WORKS

        //save handle references into my wrapper
        let ball_sphere : BallSphere = BallSphere::new("ball1".to_string(), ball_body_handle, ball_collider_handle);
        physics_unit.ball_spheres.push(ball_sphere);

        //then to get a read on this object's position:
        let ball_body = &physics_unit.rigid_body_set[ball_sphere.body_handle]; //use the handle to access
        println ("Ball altitude: {}", ball_body.translation().y);
 

What's interesting to me is it looks like we are allowed to have multiple copies of the handle references. I think this is because they are not mut. I think you can have multiple immutable refs but only one mutable ref and this is why. We see this, because the ball_body_handle is passed into collider_set.insert_with_parent which saves a copy inside, but I am also able to save a copy in my BallSphere wrapper without conflict. So I presume that is what is happening. Correct?

Anyway, either way, thanks, as that got me on the right track and seemed to work. Appreciated yet again. :smiley:

Their handles are basically indices into a Vec. The generation is to identify when a slot has been reused by giving every assignment a new generation number. If you pass in a handle with a mismatched generation, it will consider it a stale handle to a resource which is no longer present. The documentation links to this code comment for more information. It's still up to you to use the correct indices with the correct arena (though if you only have one RigidBodySet, etc, the handle wrapper around the Index is probably sufficient to avoid mistakes).

More generally, they are a non-borrowing token that identifies a resource. This is a common pattern in Rust, due to the ownership and borrowing model.[1] References are for short-term, contiguous borrows; some sort of identifier lookup is one way to enable generating references on demand.

They've exposed a somewhat surprising amount of implementation detail IMO, but ultimately that's how I would think of these handles: a non-borrowing object lookup mechanism. Like a file descriptor or file handle is to your OS, say.

It's because they are non-borrowing types; it's just a couple of integers underneath. You can have as many copies of 0 as you want and use all of them to index into a Vec. Same idea with these handles. They're not references at all, so the considerations about "multiple shared references (&_) or a single exclusive reference (&mut _)" don't directly apply. Those considerations come up when you generate actual references somehow (by calling .get_mut(handle) for example).

As far as mut goes... &mut _ and &_ are very different types, but beyond that, there is no inherent "mutness" to types. Local variables can be declared without mut to prevent overwriting them or taking a &mut _ to them, but it doesn't change the type of the variable or anything like that. If you take a compiling program and make every variable binding mut, it doesn't change the semantics of the program.

So "the handles are not mut" isn't really a meaningful statement. Unless perhaps you meant "the handles don't contain a &mut _", which is true -- and if they did it's also true that you couldn't have multiple copies of them. (But they don't contain a &_ either; they're non-borrowing types.)


  1. (Shared ownership -- Rc, Arc -- is another.) ↩ī¸Ž

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.