How to do RAII pattern with Rust

Hello,

I try to find a way to force a RAII struct returned by a function to have a named.
#[must_use] raised a warning but I need an error!

For example :

#[must_use]
struct TempErrorMode{
    error_mode: SEM,
}

impl Drop for TempErrorMode {
    fn drop(&mut self){
        SetErrorMode(self.error_mode);
    }
}

fn change_error_mode_temporary(error_mode: SEM) -> TempErrorMode {
    TempErrorMode{
        error_mode : SetErrorMode(error_mode),
    }
}

impl T{
  pub fn new() -> Result<()>{
        change_error_mode_temporary(SEM::value); // Here Rust accept that but the return value is directly dropped. #[must_use] make a warning but here an error must be raised!
        // let guard = change_error_mode_temporary(SEM::value);  // Only this must be compiled
        this_function_can_failed(filename)?;
        Ok(())
    }
}

Thank you!

I don't think there's a way to do it. If you really really have to prevent user from just dropping the guard, convert your api to take a closure that will be run with the changed mode.

3 Likes

Even if you could force an error on must_use, one could still "use" it in a call to mem::forget to discard it without running your Drop, or use any other way to leak a value. You might be interested in reading about the Leakpocalypse and why mem::forget was made a safe function at all.

5 Likes

Well thank you, this was very instructive.
What I understand is that Rust allow you to leak destructors so you should never considering using destructor like C++ RAII pattern.
It is a shame when you want to use the ? operator don't you think?
What I finally done is something like this :

let old_error_mode = SetErrorMode(SEM::SEM_NOOPENFILEERRORBOX);
let res = LoadLibrary(filename);
SetErrorMode(old_error_mode);
match res {
    Ok(handle) => {
        Ok(DllLoader {
            handle,
        })
    },
    Err(err_code) => Err(err_code),
}

But I not very satisfying of the manually temporary let res statement and the Err(err_code) => Err(err_code), Do you know some rust syntax to simplify this? Thank a lot!

For those who recognize, SetErrorMode and LoadLibrary are C FFI of the Win32 API, I can't change that.

You can and should use Rust with RAII. The caveat is that you can't rely on destructors for safety properties, which C++ doesn't even try to address at all. That blog post describes in detail how missing Drop could be a disaster if you haven't allowed the possibility of leaking. In other cases it's fine -- take a Mutex for example. When you lock that, you get a RAII MutexGuard to release the lock when it's dropped. If you leak that guard, the mutex will remain locked forever. That's undesirable, but it's not a problem for memory safety.

2 Likes

Thank you!

It might be interesting to have a way to inform the compiler that the drop must be called at the end of the scope where it was created (Aka: The lifetime of the RAII ends at the end of the scope where it was declared and not before)

Does this imply that it's forbidden to move this value into the inner scope (including calling mem::drop on it)? If the value isn't moved, AFAIK it will be dropped at the end of the scope (modulo possible optimizations), not taking into account the actual end of usage.

1 Like

Yes somethings like "this item is not movable or copyable".
We can do the same things on C++ by deleting or making them private default,move and copy operators and constructors.
This type of pattern can greatly simplify some code and allow early returns even if we need to unlock some sync or free some file handle automatically, the compiler add it automatically for us with drop.
For example the code above:

let old_error_mode = SetErrorMode(SEM::SEM_NOOPENFILEERRORBOX);
let res = LoadLibrary(filename);
SetErrorMode(old_error_mode);
match res {
    Ok(handle) => {
        Ok(DllLoader {
            handle,
        })
    },
    Err(err_code) => Err(err_code),
}

Can be simplify and generalized to this:

#[not_movable] // Maybe an attributes that do not allow move except when RVO is possible ( Not NRVO )! also do not allowed Clone and Copy traits
struct Guard{
    old_value : i32,    
}

impl Guard{
    fn new(new_value: i32) -> Guard{
        Guard {
            old_value : SetValue(new_value),
        }
    } // Guard is anonymous, move elision should be possible with "RVO" optimisation ( Not NRVO ) 
}

impl Drop for Guard{
    fn drop(&mut self){
        SetValue(self.old_value);
    }
}

fn fct_can_return_err() -> Result<(),()>{
    Err(())
}

fn load_somethings() -> Result<(),()> {
    let _guard = Guard::new(777);
    fct_can_return_err()?; // Here drop(&mut _guard) is called if Err()
    
    // ...
    // Do something that need fct_returning_err() -> Ok
    // ...
    mem::forget(_guard); // Error move not possible
    mem::drop(_guard); // Error move not possible
    other_dev_fct(_guard); // Error move not possible
    
    Ok(())
    //drop(&mut _guard) is called also here 
}

fn main(){
    let _result = load_somethings(); 
}

If I have to use the guard in a lot of place it's cool! I don't even have to thinks about releasing stuff and allow me to use ? without fear.

The one way to do this, is to scope it via a closure like so:

fn change_error_mode_temporary<T>(error_mode: SEM, closure: impl FnOnce() -> T) -> T {
    SetErrorMode(error_mode);
    let result = closure();
    SetErrorMode(old_error_mode_or_so); // you may want some guard still, as this wouldn't run on panic
    result
}

impl T {
    pub fn new() -> Result<()> {
        change_error_mode_temporary(SEM::value, || {
            this_function_can_failed(filename)?;
            Ok(())
        })
    }
}
2 Likes

A big thank you!
Some pattern is not natural when you come from C/C++ :smiley:
I finally do something acceptable:

fn change_error_mode_temporary(
    error_mode: SEM,
    closure: impl FnOnce() -> Result<HMODULE>,
) -> Result<HMODULE> {
    let old_error_mode = SetErrorMode(error_mode);
    let res = closure();
    SetErrorMode(old_error_mode);
    res
}

pub struct DllLoader {
    handle: HMODULE,
}

impl DllLoader {
    pub fn new(filename: &[u8]) -> Result<DllLoader> {
        Ok(DllLoader {
            handle: change_error_mode_temporary(
                SEM::SEM_NOOPENFILEERRORBOX,
                || -> Result<HMODULE> { Ok(LoadLibrary(filename)?) },
            )?,
        })
    }
}

Edit: I renamed the thread to "How to do RAII pattern with Rust".

1 Like

It wasn't stated super clearly, but this is very important:

There is no significant difference between C++ and Rust here: destructors were never guaranteed to run. This is an extremely common misconception, but it is just a misconception. C++ and Rust both call destructors reliably on scope exit for simple scoped variables, but neither language ever guaranteed destructors will always get executed under any circumstances, because it's impossible to specify what that means when lifetimes aren't tied to scopes. The language differences that do exist are fairly technical things like C++'s lifetime extension rules for temporary variables. In the big picture, you can leak things in C++ and Rust in exactly the same ways: allocate some raw heap memory without putting the pointer in an RAII type, create a cycle of shared_ptrs/Arcs, etc.

Rust does have to be more upfront about this than C++, but only because the Rust language has a safe/unsafe split and fairly high standards for a safe API with unsafe implementation, so people often end up wanting to provide APIs that rely on knowing when something has "finished", and (understandably) aren't willing to take a C++-y "stop hurting yourself" attitude toward client code that happens to leak something. AFAIK, passing a function/closure/lambda into such an API is the proper solution in both languages.

4 Likes