Skip to content

Add core::mem::DropGuard #144236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open

Conversation

yoshuawuyts
Copy link
Member

1.0 Summary

This PR introduces a new type core::mem::DropGuard which wraps a value and runs a closure when the value is dropped.

use core::mem::DropGuard;

// Create a new guard around a string that will
// print its value when dropped.
let s = String::from("Chashu likes tuna");
let mut s = DropGuard::new(s, |s| println!("{s}"));

// Modify the string contained in the guard.
s.push_str("!!!");

// The guard will be dropped here, printing:
// "Chashu likes tuna!!!"

2.0 Motivation

A number of programming languages include constructs like try..finally or defer to run code as the last piece of a particular sequence, regardless of whether an error occurred. This is typically used to clean up resources, like closing files, freeing memory, or unlocking resources. In Rust we use the Drop trait instead, allowing us to never having to manually close sockets.

While Drop (and RAII in general) has been working incredibly well for Rust in general, sometimes it can be a little verbose to setup. In particular when upholding invariants are local to functions, having a quick inline way to setup an impl Drop can be incredibly convenient. We can see this in use in the Rust stdlib, which has a number of private DropGuard impls used internally:

3.0 Design

This PR implements what can be considered about the simplest possible design:

  1. A single type DropGuard which takes both a generic type T and a closure F.
  2. Deref + DerefMut impls to make it easy to work with the T in the guard.
  3. An impl Drop on the guard which calls the closure F on drop.
  4. An inherent fn into_inner which takes the type T out of the guard without calling the closure F.

Notably this design does not allow divergent behavior based on the type of drop that has occurred. The scopeguard crate includes additional on_success and on_onwind variants which can be used to branch on unwind behavior instead. However in a lot of cases this doesn’t seem necessary, and using the arm/disarm pattern seems to provide much the same functionality:

let guard = DropGuard::new((), |s| ...);  // 1. Arm the guard
other_function();                         // 2. Perform operations
guard.into_inner();                       // 3. Disarm the guard

DropGuard combined with this pattern seems like it should cover the vast majority of use cases for quick, inline destructors. It certainly seems like it should cover all existing uses in the stdlib, as well as all existing uses in crates like hashbrown.

4.0 Acknowledgements

This implementation is based on the mini-scopeguard crate which in turn is based on the scopeguard crate. The implementations only differ superficially; because of the nature of the problem there is only really one obvious way to structure the solution. And the scopeguard crate got that right!

5.0 Conclusion

This PR adds a new type core::mem::DropGuard to the stdlib which adds a small convenience helper to create inline destructors with. This would bring the majority of the functionality of the scopeguard crate into the stdlib, which is the 49th most downloaded crate on crates.io (387 million downloads).

Given the actual implementation of DropGuard is only around 60 lines, it seems to hit that sweet spot of low-complexity / high-impact that makes for a particularly efficient stdlib addition. Which is why I’m putting this forward for consideration; thanks!

@rustbot
Copy link
Collaborator

rustbot commented Jul 21, 2025

r? @Mark-Simulacrum

rustbot has assigned @Mark-Simulacrum.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jul 21, 2025
@workingjubilee
Copy link
Member

this appears to be revisiting rust-lang/libs-team#622

@rust-log-analyzer

This comment has been minimized.

@yoshuawuyts
Copy link
Member Author

yoshuawuyts commented Jul 21, 2025

this appears to be revisiting rust-lang/libs-team#622

Oh you mean the naming? I guess I missed that issue since it's just a few days old. Most of the names come from the mini-scopeguard crate I put together in preparation for this PR earlier this year. To give some rationale for the names:

  • core::mem::DropGuard groups nicely with core::mem::drop and core::mem::ManuallyDrop. It would be the third drop-related operation we're putting in mem, and it's worth making it sound both similar yet distinctly identifiable.
  • core::mem::DropGuard functions very similarly to all the other guards we have in the stdlib. I think of the "guard" suffix as "does interesting things on drop", which checks out.
  • In the stdlib we've been using the name DropGuard for many years now, with at least 10 instances of it. That seems like pretty good precedent, and a decent indicator that the name is memorable.
  • The ScopeGuard crate uses the ScopeGuard::into_inner method to take a value without running the destructors. Its signature is also nearly identical to all the other into_inner methods we have.

This to me seemed like a reasonable starting point for a contribution. The part I'm least sure about is the into_inner method, since it does something interesting that might be worth calling attention to in a way that into_inner might not. Since this is only an initial impl, I'm not sure we should block on that though. Instead I think we should probably keep that as an open question?

@rust-log-analyzer

This comment has been minimized.

@jieyouxu jieyouxu added the S-waiting-on-ACP Status: PR has an ACP and is waiting for the ACP to complete. label Jul 21, 2025
@rust-log-analyzer

This comment has been minimized.

@orlp
Copy link
Contributor

orlp commented Jul 21, 2025

Some things missing in this PR from my ACP (which makes sense since it was apparently done in parallel):

  1. The function type in my ACP is defaulted to = fn(T):
pub struct WithDrop<T, F: FnOnce(T) = fn(T)> {

This is similar to how it is defaulted in LazyLock to make writing static declarations easier.

  1. new is not marked const. There's no reason it shouldn't be.
  2. new is not marked #[must_use]. This is important to prevent accidental instant-dropping.
  3. Clone is not derived. I think this somewhat makes sense if you think of this type as exclusively a guard, but I think of this more generally, and under that umbrella there is no reason Clone shouldn't be derived.

There are also some differences:

  1. The name of the type, I proposed WithDrop, but this is best discussed in the ACP, and I placed a comment there.
  2. Debug is not implemented, but rather derived. This gives rather ugly results.
  3. I intentionally did not implement PartialEq, Eq, PartialOrd, Ord, Hash, but this PR does (through derives). It's not clear to me that these traits should be implemented for this type as it's unclear whether these relationships include the on_drop function to compare or not. One user might expect equality to only include the payload, and another might expect it to include the function. As it's written right now it's also dangerous because Deref coercion might change equality results (since post-Deref only the value is compared, but pre-Deref both the value and function).

@yoshuawuyts
Copy link
Member Author

Thanks @orlp, @purplesyringa, @bjorn3, @lukaslueg - you've raised some really good points. I'll try and address your comments either later today or tomorrow. But I wanted to take a moment to say thank you before then ^^

@rust-log-analyzer

This comment has been minimized.

F: FnOnce(T),
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&**self, f)
Copy link
Contributor

@purplesyringa purplesyringa Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little wary of making wrappers transparent in Debug. orlp's suggestion in rust-lang/libs-team#622 was to print DropGuard(...) here via debug_tuple, and personally I find that more reasonable. Did you implement Debug transparently deliberately or is this just an omission?

Copy link
Member Author

@yoshuawuyts yoshuawuyts Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't necessarily love this either - but this is what we seem to be doing for most other wrapper types in the stdlib. I lifted this impl from Mutex, but for example the Pin type does the same thing.

If we want to change this, I think we should probably argue it for all wrapper types in the stdlib. I'd rather not break precedent on an addition like this, even if I agree that I prefer your suggestion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took inspiration from LazyLock which does a debug print with a tuple wrapper.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please correct me if I'm wrong, but I think the rule of thumb here is "Debug is transparent if the wrapper type has no runtime behavior". E.g. types like Mutex, LazyLock, ManuallyDrop, etc. implement Debug non-transparently, and Pin, Unique are transparent. I think DropGuard is closer to the former than the latter so we wouldn't break any conventions here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the rules here are honestly, but I like where you're going with this. While the the impl for Mutex is intransparent, the impl for MutexGuard is transparent. I think it's fair to say that MutexGuard does have its own runtime behavior. How would that fit into this framework?

@rust-log-analyzer

This comment has been minimized.


// This will run the closure, which will panic when dropped. This should
// run the destructor of the value we passed, which we validate.
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding this failure correctly, this line isn't actually catching the unwind the way it should.

Or maybe the test runner in CI mode is getting confused that we're unwinding and catching it. I'm not actually sure what is happening?

Copy link
Member Author

@yoshuawuyts yoshuawuyts Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried recreating this locally using ./x.py test library/coretests, and I can't seem to. All tests are passing, and I'm not quite sure what's different. I'm now re-running all tests locally to see if that makes a difference.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't reproduce locally (not as part of coretest, standalone). The panic still gets printed as we haven't installed a panic_handler but it should get caught.

Copy link
Contributor

@purplesyringa purplesyringa Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, you're just testing on cranelift with panic = "abort". Add #[cfg(panic = "unwind")] to the test. Sorry, noticed this CI failure earlier but thought you'd resolve it yourself 😅

@rust-log-analyzer
Copy link
Collaborator

The job aarch64-gnu-llvm-19-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
....................................................................................... 783/2112
....................................................................................... 870/2112
....................................................................................... 957/2112
................................... 992/2112
mem::drop_guard_always_drops_value_if_closure_drop_unwinds --- FAILED
.........................................................ii............................ 1080/2112
....................................................................................... 1167/2112
....................................................................................... 1254/2112
....................................................................................... 1341/2112
....................................................................................... 1428/2112
---
....................................................................................... 2037/2112
...........................................................................
failures:

---- mem::drop_guard_always_drops_value_if_closure_drop_unwinds stdout ----
---- mem::drop_guard_always_drops_value_if_closure_drop_unwinds stderr ----

thread 'main' panicked at coretests/tests/mem.rs:830:44:
explicit panic
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::panicking::panic_explicit
   3: coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#1}::panic_cold_explicit
             at /checkout/library/core/src/panic.rs:88:13
   4: coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#1}
             at ./tests/mem.rs:830:44
   5: <core::mem::drop_guard::DropGuard<(), coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#1}> as core::ops::drop::Drop>::drop
             at /checkout/library/core/src/mem/drop_guard.rs:136:9
   6: core::ptr::drop_in_place::<core::mem::drop_guard::DropGuard<(), coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#1}>>
             at /checkout/library/core/src/ptr/mod.rs:804:1
   7: core::ptr::drop_in_place::<coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#2}>
             at /checkout/library/core/src/ptr/mod.rs:804:1
   8: <core::mem::drop_guard::DropGuard<core::mem::drop_guard::DropGuard<(), coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#0}>, coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#2}>>::into_inner
             at /checkout/library/core/src/mem/manually_drop.rs:256:18
   9: coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#3}
             at ./tests/mem.rs:839:9
  10: <core::panic::unwind_safe::AssertUnwindSafe<coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#3}> as core::ops::function::FnOnce<()>>::call_once
             at /checkout/library/core/src/panic/unwind_safe.rs:272:9
  11: std::panicking::catch_unwind::do_call::<core::panic::unwind_safe::AssertUnwindSafe<coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#3}>, ()>
             at /checkout/library/std/src/panicking.rs:589:40
  12: std::panic::catch_unwind::<core::panic::unwind_safe::AssertUnwindSafe<coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#3}>, ()>
             at /checkout/library/std/src/panicking.rs:552:19
  13: coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds
             at ./tests/mem.rs:837:13
  14: coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#0}
             at ./tests/mem.rs:824:59
  15: <coretests::mem::drop_guard_always_drops_value_if_closure_drop_unwinds::{closure#0} as core::ops::function::FnOnce<()>>::call_once
             at /checkout/library/core/src/ops/function.rs:253:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


failures:
    mem::drop_guard_always_drops_value_if_closure_drop_unwinds

test result: FAILED. 2107 passed; 1 failed; 4 ignored; 0 measured; 0 filtered out; finished in 2.75s

error: test failed, to rerun pass `-p coretests --test coretests`
env -u RUSTC_WRAPPER CARGO_ENCODED_RUSTDOCFLAGS="-Csymbol-mangling-version=v0\u{1f}-Zrandomize-layout\u{1f}-Zunstable-options\u{1f}--check-cfg=cfg(bootstrap)\u{1f}--check-cfg=cfg(llvm_enzyme)\u{1f}-Dwarnings\u{1f}-Wrustdoc::invalid_codeblock_attributes\u{1f}--crate-version\u{1f}1.90.0-nightly\t(883fcfb63\t2025-07-21)" CARGO_ENCODED_RUSTFLAGS="-Csymbol-mangling-version=v0\u{1f}-Zrandomize-layout\u{1f}-Zunstable-options\u{1f}--check-cfg=cfg(bootstrap)\u{1f}--check-cfg=cfg(llvm_enzyme)\u{1f}-Zmacro-backtrace\u{1f}-Csplit-debuginfo=off\u{1f}-Clink-arg=-L/usr/lib/llvm-19/lib\u{1f}-Cllvm-args=-import-instr-limit=10\u{1f}-Clink-args=-Wl,-z,origin\u{1f}-Clink-args=-Wl,-rpath,$ORIGIN/../lib\u{1f}-Alinker-messages\u{1f}--cap-lints=allow\u{1f}--cfg\u{1f}randomized_layouts" RUSTC="/checkout/obj/build/aarch64-unknown-linux-gnu/stage1-tools/cg_clif/dist/rustc-clif" RUSTDOC="/checkout/obj/build/aarch64-unknown-linux-gnu/stage1-tools/cg_clif/dist/rustdoc-clif" "/checkout/obj/build/aarch64-unknown-linux-gnu/stage0/bin/cargo" "test" "--manifest-path" "/checkout/obj/build/aarch64-unknown-linux-gnu/stage1-tools/cg_clif/build/sysroot_tests/Cargo.toml" "--target-dir" "/checkout/obj/build/aarch64-unknown-linux-gnu/stage1-tools/cg_clif/build/sysroot_tests_target" "--locked" "--target" "aarch64-unknown-linux-gnu" "-p" "coretests" "-p" "alloctests" "--tests" "--" "-q" exited with status ExitStatus(unix_wait_status(25856))
Command has failed. Rerun with -v to see more details.
Build completed unsuccessfully in 0:13:57
  local time: Mon Jul 21 22:18:45 UTC 2025
  network time: Mon, 21 Jul 2025 22:18:45 GMT
##[error]Process completed with exit code 1.
Post job cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-ACP Status: PR has an ACP and is waiting for the ACP to complete. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants