If we don't have guaranteed RAII, can we have finally

C++ don’t have finally (some variants do have their own though), but have RAII principle (not guaranteed; need care). Other languages like C# and Java have finally. In Rust, we do have some RAII, but again not guaranteed. Yet we don’t have finally.

Do we need it? In many simple or complicated scenario, a finally block can reduce a lot of code.

fn foo() -> Result<(),Error> {
    let result = receive_data();
...
    try! {
        do_something()?;
        do_something_else()?;
...
        Ok(())
    } finally! {
        send_response(); //should run even when the try operators fail
    }
...
   
}

I guess today I would have to write a wrapper function to do things like the above. A RAII object will help here and some handy things exist in the crates, but not in std. And it is not guaranteed so we don’t have a good thread::scoped. However if we can write finally blocks we can do better.

Guaranteed in what circumstances? In a function like the one you wrote:

fn foo() -> Result<(), Error> {
  let result: DataHolder = data_holder();
  do_something()?;
  do_something_else()?;
  Ok(())
}

DataHolder::drop is guaranteed to be called if it unwinds out of foo(). do_something_else could prevent it from running by calling os::exit or by entering a loop {}, but that’s exactly the same with a finally block.

Destructors can be prevented from running if the owner of the value does something to it, like putting it in an Rc loop or calling mem::forget on it. In the particular case of thread::scroped, an abstraction was being used that required the destructor to be run regardless of what the owner did.

But… the owner of the value, in by above example, is exactly the same code that would have needed to use a finally-block. So where’s the difference? How can you trust foo() to put in a correct finally block, but not trust foo() to avoid putting your RAII token into an Rc?

8 Likes

I think we need something in std like

fn drop_guard(f: FnOnce()) -> impl Drop {
    struct DropGuard<F>(Option<F>);
    impl<F> Drop for DropGuard<F> {
          fn drop(&mut self) {
              if let Some(f) = self.0.take() {
                   f()
              }
          }
    }
    DropGuard(f)
}
1 Like

defer offers this today if you want a crate to do it.

5 Likes

finally isn’t guaranteed either, even in things you mentioned like C#.

4 Likes

As you can see, this is almost trivial, adding a crate reference just for it seems silly (as the lines I need for adding the references may longer than the actual function definition),I would rather duplicate it in my own crate. So why not just move it to core?

This is standard policy for Rust: if it can be pushed into an external crate, it should be. Exceptions should only be made for things used by the overwhelming majority of code, or things that only core/std can provide.

Rust is not "batteries included", by design.

If you want this in std, it's up to you to demonstrate that it would have extremely broad uptake, probably by showing that a significant number of people are willing to depend on a crate providing this functionality.

defer has 144 downloads over all time. I suspect you may face an uphill battle.

9 Likes

As in my argument this does not count. Because people would like to solve this simple thing in their own code, rather than refering a crate that have to be heard of and having the documents read and understood it is exactly the thing they want.

Being part of std however, makes it become a standard point to learn and it is a way to promote a specific way to write code.

So now the question is: for the example above, what is the best coding style in today's Rust that the community wants people to learn?

  1. Wrapper function

  2. Add crate reference to defer. If this is the option, at least it sould be mentioned in one of the official books. After all, C#/Java users will find it useful to migrate their code.

  3. Ad-hoc RAII objects

  4. ...

I don't think that's it either, since the response then is "well, obviously the crate is successful as-is so people can just keep using it".

2 Likes

"Because I want this" isn't very compelling. My point was what you could do to find some actual evidence that stands a better chance of convincing people.

I've seen a few iterator methods transition from itertools to std. Clearly, the existence of a crate providing the functionality does not preclude it being added to std.

2 Likes

There’s also scope_guard that’s much more popular.

I’ve used it for calling free() in very C-like low-level Rust code, but I’m planning to remove and replace the whole thing with a newtype with Drop.

7 Likes

I count it as an evidence in my side. First, 2XXK download is convincing enough already. Second, the lack of its existence in std is already results in crates with duplicated functionalities.


Just a quick check, actually my own crate is referencing it indirectally. I am crating an dependency graph to check further.

In your comment there’s implication that libstd is for commonly used functionality, and to prevent crates from reinventing it.

But are these the right reasons to include things in libstd? The criteria could be different, e.g.

  • functionality that has to be in libstd, because it can’t be elsewhere. For example, if the implementation needs to hook into the compiler internals, or couldn’t be added by other crates due to orphan rules.

  • functionality that’s necessary for interoperability. While anyone could define String, having one true string is helpful for crates to talk to each other.

In such case defer! that works fine as a 3rd party macro, and isn’t exposed as a cross-crate interface, is fine as a 3rd party crate.

To be clear, I don’t think there are official criteria like that. libstd has a bunch of things for convenience, but OTOH it also doesn’t have random number generation, lazy_static, bitfields, regexes. Top Rust crates are usually built-in in other languages.

2 Likes

Though these lines I would argue that defer or scope_guard is a basic control flow building block, and it is essential to join multiple existing points. In other languages this is built-in and have a keyword, showing its importance.

Just an example. Drop is an important trait, may be more than Clone because it is magical. Yet we don’t have a module in std regarding Drop (but we do have clone module). I think we should have it and scopeguard is natually fit there. std::mem::drop or std::mem::free or ManuallyDrop should move there, ideally.

What else to put there? Some combinators I think. The reason why you need to replace scopeguard with a newtype is probably you need to combind things that a simple FnOnce is not enough. In that case, some combinators might help.

dbg! is the best analogue for adding something like this. Not big enough to add a crate dependency for, but decently huge as a stdlib item. There’s no reason dbg! couldn’t just be an external crate except for its universal benefit from being available in the prelude.

(I was unaware of scope_guard; I might actually pull that in for something in the future where I need a defer.)

There is one spot where a closure-on-Drop approach doesn’t work, actually: when the cleanup needs access to some mutable state (captures &mut) and the local work also needs that mutable state. finally or an actually built-in defer wouldn’t have that problem as theoretically it could know it only needs exclusive access on exiting scope.

But the question is: how often does this need happen? And how often is it when the code isn’t already using raw pointers (thus already unsafe and not having to fall back to unsafe pointer usage just for the scope guard).

3 Likes

The use case I demostrated is implementing network/RPC protocols: you need to receive some user requests, process it, and regardless you would be successful or not, produce a proper response back.

One thing to note is that either the way scope_guard and defer were doing are not optimal: they all required to run at least one test before executing the closure. The optimal version would be

pub fn drop_guard(f: impl FnOnce()) -> impl Drop {
    struct DropGuard<F>(MaybeUninit<F>)
    where
        F: FnOnce();
    impl<F> Drop for DropGuard<F>
    where
        F: FnOnce(),
    {
        fn drop(&mut self) {
            let f = std::mem::replace(&mut self.0, MaybeUninit::uninitialized());
            (unsafe{ f.into_inner() })()
        }
    }
    DropGuard(MaybeUninit::new(f))
}

but it requires unsafe.

If this is currently a bottleneck in your code, you could open an issue in scope_guard; I’m pretty sure it could be improved and thus the unsafe would be encapsulated instead of you needing to repeat it in your own code.

There is an interesting article (well, rebuttal to a C++ proposal) which among other things attempts to answer this question for C++. He lists five main categories for what should go into std:

  • Hooks into the compiler/language/runtime, useful things that can't actually be implemented outside of std.
  • Vocabulary (string, vec, ...) without which it would be difficult for non-std libraries to talk to each other.
  • Portability functions, platform agnostic ways of talking to the fs, to the network, basic concurrency primitives, and the various underlying platform specific bases.
  • CS primitives for sorting, searching, filtering. I believe defer falls into this category. Simple enough that I would implement it myself before looking for a crate, but complex enough that that's a papercut.
  • Batteries. Big unicode libraries, random number generation libraries, matrix libraries. The author points out that the standard C++ libraries for this are often not the best and that a proper dependency system would make these unnecessary. Rust has such a system so large domain-specific libraries should definitely be crates where possible.

I think in rust a lot of the latter two categories (mostly the last category) can and should be broken out into crates (itertools is a good example of a collection of "CS 101" solutions to problems). But unless defer easily falls into some utility crate I would definitely support adding it to std.

3 Likes

I don't think myself, or anyone I know would have a bottleneck like this. However, I was just saying defer or scope_guard are not zero-cost. And unfotunately, defer is more light-weighted than scope_guard.

You can easily optimize defer in the way I demostrated, but for scope_guard it do have other cost so eliminate this one does not make it optimized. However scope_guard are more flexible and have more use cases (I didn't dig deep enough).

As zero-cost is one of the big target of Rust, I think this is a good argument to have higher level of support because the optimized implimentation requires unsafe.

You are actually wrong about ::scope_guard::ScopeGuard<_, _, ::scope_guard::Always> (the one you gave an implementation for) not being "zero cost",

since

        if <S as ::scope_guard::Strategy>::should_run() {
            dropfn(value);
        }

becomes

        if true {
            dropfn(value);
        }

when S = Always, and then the if true { ... } gets optimized into { ... }.

(That is, by the way, the whole point of using structs generic over a trait instead of structs having bool-like fields.)

With the other Strategy-s, there is a runtime check since it does have to know whether it is panicking or not. That case could be optimised by forcing the end user to silently drop the closure after the "panicable section" (e.g. by using a macro). I was thinking of releasing such a crate because I had the same "idea" as the OP, but as usual, someone had already done it (c.f. ::scope_guard).

3 Likes