#Temporarily passing a mutable reference across contexts in the same thread

13 messages · Page 1 of 1 (latest)

old dome
#

G'day all! I'm working on scripting integration with mlua, and am trying to figure out how to pass a mutable reference to some context that a scripting function needs, without being able to pass it directly in.

That is, I have some initialisation code:

    globals.set(
        "spawn",
        lua.create_function(|_, (object, position): (mlua::Value, mlua::Value)| {
            let ctx: &mut Context = /* obtain from somewhere, noting that this is a closure that I can move state into */;
            spawn(ctx, object, position);
            Ok(())
        })?,
    )?;

and at a later stage, I have

let ctx: &mut Context = /* obtained from outer scope */;
run_scripts(); // no way to pass ctx in here

where run_scripts will call the previously defined spawn (indirectly), all within the same thread, without violating any guarantees about mutable aliasing.

Is there a nice not-unsafe way to "smuggle" the state into the function for the duration of that scope? Something like:

let ctx: &mut Context = /* obtained from outer scope */;
*some_shared_state = Some(ctx);
run_scripts(); // no way to pass ctx in here
*some_shared_state = None;

and

        lua.create_function(move |_, (object, position): (mlua::Value, mlua::Value)| {
            let ctx: &mut Context = some_shared_state.unwrap(); 
            spawn(ctx, object, position);
            Ok(())
        })?,

The naive solution of having a Arc<Mutex<Option<&mut Context>>> doesn't work, because then I have to specify lifetimes all the way up to the root of the program. Ideally, I want to be able to express "I am lending you access to this mutable borrow for this scope", and have some code elsewhere rely on that (with a failure at runtime being fine, because it should never fail).

Appreciate any thoughts!

old dome
#

quick update: I'm using a pointer which I convert back to a reference through unsafe - I believe this should be safe, but I'm still looking for a nicer way to do this

visual pier
old dome
#

passed down from a higher scope - my code driving the scripting system is getting the reference to do with as it pleases, and then releases it by going back up the callstack

#

it's part of an ECS so it's all somewhat automatic / out of my hands

visual pier
#

If you don't have a lot of redesign room for the caller's scope then I guess a restricted usage of pointers like you ended up doing might be reasonable

#

Do consider auditing your unsafe code in #dark-arts though, there are a lot of subtleties with unsafe and pointers

old dome
#

yeah, I know, but all I've done is deref a pointer that I set further up in the callstack on the same thread, and there's no concurrent access - the way I'm justifying it to myself is that I'm basically just "bridging" the parent scope and the grandchild scope, and if it weren't for the intermediate scope (Lua), there'd be an unbroken chain for the reference

#

surprised "temporarily lending a mutable reference elsewhere" isn't something with an obvious solution though, I thought it might've come up more often than it apparently has

quasi matrix
#

?play warn=true

#![deny(unsafe_code)]

fn main ()
{
    let r = SlotRef::<i32>::empty();
    let f = || {
        *r.get() += 27;
    };
    let mut x = 42;
    r.set_to(&mut x, || {
        f();
    });
    dbg!(x);
}

pub use safety_boundary::SlotRef;
mod safety_boundary {
    #![allow(unsafe_code)]
    use {
        ::core::{
            cell::RefCell,
            ptr,
        },
    };

    pub
    struct SlotRef<T : ?Sized> (
        RefCell<Option<ptr::NonNull<T>>>,
    );
    
    impl<T : ?Sized> SlotRef<T> {
        pub
        fn empty ()
          -> Self
        {
            Self(None.into())
        }

        pub
        fn set_to<R> (
            self: &'_ SlotRef<T>,
            r: &'_ mut T,
            scope: impl FnOnce() -> R,
        ) -> R
        {
            self.0.borrow_mut().replace(r.into());
            ::scopeguard::defer! {
                match self.0.try_borrow_mut() {
                    | Ok(mut g) => *g = None,
                    | Err(_) => ::std::process::abort(),
                }
            }
            scope()
        }
        
        pub
        fn get (self: &'_ SlotRef<T>)
          -> impl '_ + ::core::ops::DerefMut<Target = T>
        {
            ::core::cell::RefMut::map(
                self.0.borrow_mut(),
                |mb_null_ptr| unsafe { mb_null_ptr.unwrap().as_mut() },
            )
        }
    }
}
signal plinthBOT
#
[src/main.rs:13] x = 69
quasi matrix
#

And if you replaced the RefCell with a Mutex, you could then add a

unsafe
impl<T : ?Sized> Sync for SlotRef<T>
where
    for<'__>
        Mutex<Option<&'__ mut T>> : Sync
    ,
{}

so as to have something multi-thread friendly, which you could put behind an Arc.

#

The advantage of this convoluted approach is that it will play nicely with multiple successive .set_to() scopes, thanks to the lifetime erasure. For instance, when T : UsableFor<'static> (e.g., T = Context in your case), the SlotRef<T> : UsableFor<'static> too.