Supporting 'janitorial' style RAII

I would like to make the case that Rust is missing a fundamental capability that C++ has which is a very powerful and simple means to avoid logic errors. That is the concept of 'janitorial RAII'. Unlike 'normal' RAII, which Rust has obviously, where an object cleans itself up on Drop, janitors are designed to apply some change to a member of 'self' on a scoped basis, possibly in a nested fashion.

So something like:

struct Foo
{
     exploder_mode : bool;
}

impl Foo
{
     pub fn SomeCall()
     {
           // some stuff
          {
               // Enter exploder mode for this scope
               let exploder_jan : BoolJan = BoolJan::new(&mut exploder_mode, true);

              // Exploder mode gets put back to original value here
           }
     }
}

A trivial example but it demonstrates the point. This is seemingly fundamentally impossible in Rust as it stands, because such an object requires mutable access and keeps it until it itself is dropped. That leaves the called structure inaccessible and hence useless.

I think that this could be dealt with without too much undue craziness, and it would be such a benefit. Such a type can only do three things. It can have associated functions to create it, it can implement Drop, and it can 'orphan' whatever it's been given (so it does nothing on Drop.)

All that is needed is an indication to the compiler than this object takes a mutable reference but cannot use until it is destroyed. Orphaning doesn't require access to the object being 'sanitized'. The only use of the mutable reference will be in the Drop.

So that would provide a straightforward way to allow this, but to still insure at compile time that the object legally has mutable access, since the compiler knows exactly when it will be dropped. So it would be no different than anything else attempting to get a mutable ref at that point.

There are endless applications for this concept. In my very large C++ code base I use them all over the place to excellent effect. Given that Rust has decided (unwisely IMO) to not support exceptions, anything that requires undoing local changes is a huge burden because it means no more use of ? style convenient error returns, since there's no way to undo these types of scoped changes if that is used. And the manual error propagation of Rust is brutally tedious.

Given that Rust is all about making it hard to do the wrong thing, this simple concept avoids a whole raft of possible logic errors where changes made and intended to be undone are not.

No other scheme I can think of (or anyone else on the Rust Reddit either) would be anywhere near as straightforward to use. You could jump through a bunch of hoops to do it in some way using what's there now, but it would be hacky at best. Keep in mind that it must support nested applications as well, so some of those hacky methods won't even work in that case. A nested scope may create a janitor object for the same method, or a call from method A (which creates one) to method B may create another one and A has no way to know that.

This simple scheme makes all those problems go away in a safe way.

Another simple variation of this that would work via the same principle is the opposite of this, which is the 'Committer' type object, which will store up changes and commit them on Drop, if it has been told to commit, else it does nothing. The two could easily be represented via a pair of trivial traits and use the same mechanisms.

I guess the primary issue is whether there's a means to limit a type implementing these traits to just that very fundamental functionality. Even if it required a new 'lang-item' I think it would be worth it and not one of the particularly complex ones.

2 Likes

It's by no means very easy to use, but scopeguard can handle some aspects of this, and even has several examples in the docs, which are exactly about "putting things back in order" on scope exit. See scopeguard examples

As another thing, if mutable/exclusive access does not work well with some particular piece of code, there are always the interior mutability options, foremost Cell or atomics, which can help.

5 Likes

Interior mutability isn't so useful because it's not the thing itself doing that change, it's the janitorial object apply the change to it. And it's not reasonable to have every class plan for every member to be used in this way if it should be desired by downstream code.

And of course a fundamental point of this is that must be easy to use and trivial for the user to implement them, or it's sort of semi-useless. In C++ I can do a new janitor in a couple handfuls of lines of code in a lot of cases, and do generic ones pretty easily that serve a family of objects.

As demonstrated on r/rust it seems possible to build this as a library type, see this playground link.

The actual Janitor being:

pub struct Janitor<T, F>
where
    T: DerefMut,
    F: for<'a> Fn(&'a mut T::Target),
{
    value: T,
    on_scope_end: F,
}

impl <T, F> Janitor<T, F>
where
    T: DerefMut,
    F: for<'a> Fn(&'a mut T::Target),
{
    fn new(value: T, on_scope_end: F) -> Self {
        Self { value, on_scope_end }
    }
}

impl <T, F> fmt::Debug for Janitor<T, F>
where
    T: DerefMut + fmt::Debug,
    F: for<'a> Fn(&'a mut T::Target),
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("Janitor")
            .field(&self.value)
            .finish()
    }
}

impl <T, F> Deref for Janitor<T, F>
where
    T: DerefMut,
    F: for<'a> Fn(&'a mut T::Target),
{
    type Target = T::Target;
    
    fn deref(&self) -> &Self::Target {
        self.value.deref()
    }
}

impl <T, F> DerefMut for Janitor<T, F>
where
    T: DerefMut,
    F: for<'a> Fn(&'a mut T::Target),
{
    fn deref_mut(&mut self) -> &mut Self::Target {
        self.value.deref_mut()
    }
}

impl <T, F> Drop for Janitor<T, F>
where
    T: DerefMut,
    F: for<'a> Fn(&'a mut T::Target),
{
    fn drop(&mut self) {
        (self.on_scope_end)(self.value.deref_mut());
    }
}
4 Likes

All that is needed is an indication to the compiler than this object takes a mutable reference but cannot use until it is destroyed.

Interesting. These borrows would need to be fairly restricted. It may be sound to do a direct "delayed borrow" of a field in a struct that outlives the borrower; however, it wouldn't be safe to borrow values within enums or behind pointers held by that struct. For example:

let _jan = Janitor::new(self.some_option.as_mut().unwrap());
// The Option could be set to None after this, invalidating _jan.

or:

let _jan = Janitor::new(&mut self.some_vec[0]);
// The Vec may reallocate, moving its contents and invalidating _jan.

or:

let _jan: Janitor<T> = Janitor::new(&mut self.some_box);
// The Box<T> could be reassigned, dropping the value that _jan points to.
3 Likes

As demonstrated on it seems possible to build this as a library type, see [this playground link

As I replied there, this is still pretty sup-optimal. It required repetitively providing a closure for every usage. It precludes creating pre-fab janitors that do particular things and do them safely and in one place where they can be modified and all uses are changed. The point of these things is reducing the opportunity for mistakes, but this adds back in a lot of that opportunity.

Not that these closure types ones don't have a place, they are useful, but they can really only be one such option in a good solution.

The way you do this is to move the object or a mutable reference to it into the "janitor" and then access it from inside the "janitor".

The issue of not being able to "access the whole structure" is solved by putting a reference to the whole structure inside the janitor rather than a single field.

Regarding the use of '?', you can use a try block in nightly, or use an immediately invoked closure or nested function.

1 Like

Option 1:

// somewhere
fn central_cleanup_definition(this: &mut This) {
    // cleanup
}

// anywhere
let this = Janitor::new(this, central_cleanup_definition);

Option 2:

Create a simple macro to define a custom janitor type that encapsulates the Deref,DerefMut,Drop boilerplate.

Example

macro_rules! janitor {
    {
        $vis:vis $janitor:ident($this:ident: &mut $This:ty $(,$pat:ident: $ty:ty)* $(,)?) { $($tt:tt)* }
    } => {
        #[derive(Debug)]
        $vis struct $janitor<'a> {
            $this: &'a mut $This,
            $($pat: std::mem::ManuallyDrop<$ty>,)*
        }

        #[allow(non_snake_case)]
        $vis fn $janitor($this: &mut $This $(,$pat: $ty)*) -> $janitor<'_> {
            $janitor {
                $this,
                $($pat: std::mem::ManuallyDrop::new($pat),)*
            }
        }

        impl std::ops::Deref for $janitor<'_> {
            type Target = $This;
            fn deref(&self) -> &$This { &self.$this }
        }

        impl std::ops::DerefMut for $janitor<'_> {
            fn deref_mut(&mut self) -> &mut $This { &mut self.$this }
        }

        impl Drop for $janitor<'_> {
            fn drop(&mut self) {
                let $janitor { $this $(,$pat)* } = self;
                let $this = &mut **$this; // &mut &mut $This -> &mut $This
                $(let $pat = unsafe { std::mem::ManuallyDrop::take($pat) };)*
                { $($tt)* }
            }
        }
    };
    {
        $vis:vis $janitor:ident($this:ident: &$This:ty $(,$pat:ident: $ty:ty)* $(,)?) { $($tt:tt)* }
    } => {
        #[derive(Debug)]
        $vis struct $janitor<'a> {
            $this: &'a $This,
            $($pat: std::mem::ManuallyDrop<$ty>,)*
        }

        #[allow(non_snake_case)]
        $vis fn $janitor($this: &$This $(,$pat: $ty)*) -> $janitor<'_> {
            $janitor {
                $this,
                $($pat: std::mem::ManuallyDrop::new($pat),)*
            }
        }

        impl std::ops::Deref for $janitor<'_> {
            type Target = $This;
            fn deref(&self) -> &$This { &self.$this }
        }

        impl Drop for $janitor<'_> {
            fn drop(&mut self) {
                let $janitor { $this $(,$pat)* } = self;
                let $this = & **$this; // &mut &$This -> &$This
                $(let $pat = unsafe { std::mem::ManuallyDrop::take($pat) };)*
                { $($tt)* }
            }
        }
    };
}

janitor! {
    pub ResetJanitor(flag: &mut bool, val: bool) {
        *flag = val;
    }
}
janitor! {
    pub TestJanitor(_flag: &bool, _val: bool) {}
}

fn main() {
    let mut b = true;
    {
        let b = ResetJanitor(&mut b, false);
        dbg!(b);
    }
    dbg!(b);
}

3 Likes

The way you do this is to move the object or a mutable reference to it into the "janitor" and then access it from inside the "janitor".

A number of folks have suggested something similar but can you move self into something from within self? Anyway, it's also yet again limited. What if you want to apply two different janitors to self that do different things? That's not uncommon either. Everything suggested so far is either limiting, or a bit hacky, or something.

Hopefully maybe we could get this kind of thing supported in a natural, simple way that doesn't have these limitations.

Well here's another option, which I believe satisfies more of your requirements. Its' usage is about as simple as I could imagine it being (and don't worry, it's zero cost).

struct Bomb {
    primed: bool,
}
impl Bomb {
    fn light_fuse(&self) {
        if self.primed {
            println!("Boom!");
        } else {
            println!("Fsss...");
        }
    }
    fn arm(&mut self) -> impl Guard<Self> {
        guarded_state(self, |this| &mut this.primed, true)
    }
    fn detonate(&mut self) {
        let this = self.arm();
        this.light_fuse();
    }
}

fn main() {
    let mut foo = Bomb { primed: false };
    foo.light_fuse(); // Fsss...
    foo.detonate(); // Boom!
    foo.light_fuse(); // Fsss...
}

A demo on the playground

2 Likes

Well here's another option, which I believe satisfies more of your requirements. Its' usage is about as simple as I could imagine it being (and don't worry, it's zero cost).

I'm not sure if that was intended to be sarcastic? If not, it's not really what I'm describing. A lot of folks keep making this mistake. The janitor has to operate on a member of a struct not on its own internal state, while not preventing further use of self in mutable calls while in that scope.

Maybe I'm misunderstanding you, but I don't think you are addressing that issue.

Great, I've been following a few of the solutions and that was what I thought ^^

  • Operate on a member of a struct: The guard modifies and resets the primed field of Bomb :+1:
  • Allow self to continue to be used: My example calls this.light_fuse! No problem! :+1:
    • Note that my example doesn't require mutability, but would work just as well with a &mut self receiver

It's operating on its own contents, not the contents of another struct, unless I'm just totally blind, which has happened of course. And maybe I don't understand the Rust'isms involved.

Look at the original example. The janitor object is not the object that was called. The method is called on one struct, which uses a different type, scoped to that method (or possibly a subscope in side there) to modify one if the called structure's members.

Is that what yours is doing? It doesn't look like it.

The way that scopeguard/janitor style cleanup (currently) works is that any further use of the type has to be proxied through the janitor, because of the rules around mutable borrows. Specifically,

let this: &mut Object;
let guard: Guard<&mut Object, impl FnOnce(&mut Object)> = guard(this, cleanup);
let this: &mut Object = &mut guard;

This is equivalently expressive to a "deferred borrow", while being much clearer around what is (must be) prohibited and why.

4 Likes

Can you do an example like my original as to how that would work? I'm not just creating a type that has built in support. I'm using something on a type that has no idea that it's be 'sanitized'. This can't work if the actual types themselves have to be specifically designed to support it from the inside.

Does it help if I say that all the this variables in my example are Bombs? Rust just doesn't let you bind to self, so it's the name I used for it. arm can be rewritten to make it more visible

fn arm(&mut self) -> impl Guard<Self> {
    guarded_state(self, |Bomb { primed }| primed, true)
}

The value passed into light_fuse is a totally normal &mut Bomb. The "proxying" only exists within the body of detonate, and is important because it basically acts as that "indication to the compiler than this object takes a mutable reference but cannot use until it is destroyed".

The only example I can see is the one in the post, here's what that would look like:

struct Foo {
     exploder_mode : bool
}

impl Foo {
     pub fn some_call(&mut self) {
           // some stuff
          {
               // Enter exploder mode for this scope
               let jan = guarded_state(self, |Foo { exploder_mode }| exploder_mode, true);

              // Exploder mode gets put back to original value here
           }
     }
}

Is this the example you're looking for?

extern crate scopeguard; // 1.1.0

pub struct Foo {
    exploder_mode: bool,
    // other fields
}

impl Foo {
    pub fn some_call(&mut self) {
        self.other_call();
        {
            let mut guard = scopeguard::guard(&mut *self, |this| this.exploder_mode = false);
            let this: &mut Self = &mut guard;
            this.other_call(); // works
            // self.other_call(); // does not work; cannot work with the current borrow model
        }
        self.other_call();
    }
    
    pub fn other_call(&mut self) {}
}
3 Likes

OK, I see what you are saying. But this one suffers from the same problem as the other one above. It requires providing closures to do the work, which I don't think is really optimal. And it's not what I'd call readable or convenient. It could be done I guess, but I'm hoping for a more natural solution such as the one I suggested, which would be a lot simpler to use and read and write.

Hah, I guess it makes sense that this has already been written. I'd already written off scopeguard when it was rejected before.

Minor nit that the let this line is totally unnecessary. You can just call the guard this, and it'll do the job for you