RFA-513 · Case file with fixtures · Case 485 of 694 · Compiler evidence
Rust Drop Behaviour Belongs to an Owned Wrapper, Not a Reference
Dropping a reference ends a borrow but does not own the referent. Put cleanup responsibility in the owned resource or a local guard whose lifetime represents the cleanup scope.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- A reference owns only a borrow handle and ending it must not define destruction behaviour for the referent owned by another value.
- First discriminating check
- Put final cleanup on a local owner or scoped cleanup on a local RAII guard, keeping fallible work in an explicit close or finish method.
I tried to implement Drop for &mut Resource. Rust emitted E0120 because references are not local structs, enums, or unions on which my crate can define destruction behaviour.
The failing fixture also exposes an ownership mismatch: a reference borrows a resource; it does not own the resource being destroyed.
Drop follows ownership of a value
Rust runs Drop::drop when an owned value reaches the end of its drop scope. The value's type defines the cleanup invariant.
A reference value ending means the borrow ends. The referent remains owned elsewhere and may continue living. Attaching referent cleanup to any temporary reference would make borrowing capable of unexpectedly destroying or resetting someone else's state.
The official E0120 page directs this design toward a wrapper.
The repaired fixture introduces a guard
The repaired fixture defines local Guard<'a> containing &'a mut Resource and implements Drop for the guard.
Now the guard is an owned nominal value with one clear drop scope. Its borrow ensures exclusive access to the resource while the guard exists. Cleanup can run at guard destruction without claiming ownership of the resource itself.
This is the RAII guard pattern used for scoped state and lock-like APIs.
Put final destruction on the resource owner
If cleanup must happen exactly when the resource itself dies, Drop belongs on the local resource type. It may close a handle, release foreign memory, or restore an invariant.
If the resource type is foreign, I wrap it in a local newtype that owns it. The wrapper's destructor then follows the wrapper's ownership and can prevent raw access that would violate cleanup assumptions.
The distinction is final ownership versus temporary scoped access.
Drop cannot be called directly as ordinary cleanup
Rust does not allow an explicit value.drop() call through the trait as a normal method. std::mem::drop(value) consumes the value and lets its destructor run at the appropriate point.
For fallible cleanup, relying only on Drop is insufficient because destructors do not return Result. I expose an explicit close or finish method and keep Drop as a best-effort fallback when appropriate.
I test both explicit and implicit paths.
Guard destructors must remain panic-safe
Destructors can run during unwinding. A second panic may abort the process, and complex work can create surprising latency at scope boundaries.
I keep guard cleanup small, non-panicking, and clear about failure handling. Network operations or required persistence usually deserve explicit methods rather than hidden destructor work.
The Drop documentation also matters for drop order and interaction with ownership.
Forgetting and cycles are real limits
Safe mechanisms can intentionally leak values, and reference-count cycles can prevent destruction. Therefore Drop is not a universal guarantee that externally important work always happens.
I do not base safety on a destructor running in every conceivable program termination path. Memory safety invariants must remain valid even when cleanup is skipped; external durability uses explicit protocols.
Drop order should be part of the guard design
Fields are dropped according to Rust's defined ordering after the type's own drop method runs. A guard holding several resources should not depend on accidental declaration changes without tests and documentation. I may wrap fields separately or use Option::take when cleanup needs deliberate sequencing, while respecting the prohibition on moving ordinary fields directly out of a type that implements Drop.
For nested scopes, I keep guard bindings close to the operations they protect. An underscore-prefixed named binding lives to scope end, while _ can discard a value immediately. Small lifetime differences can decide when cleanup happens, so the repaired code should exercise the intended scope boundary.
Cleanup should be observable in tests
For a real guard I record a flag, counter, or fake resource event and assert it after the guard's scope ends. Compilation proves only that the destructor is legal; the test proves that it performs the promised transition once.
My E0120 checklist
- Does the target type own the resource or only borrow it?
- Is the target a local struct, enum, or union?
- Should cleanup happen at final ownership or at scope exit?
- Would a local guard represent temporary cleanup responsibility?
- Does a foreign owned resource need a newtype wrapper?
- Can cleanup fail and therefore need an explicit method?
- Is the destructor small and non-panicking?
- What happens if the value is leaked or the process terminates?
The core principle is that destruction responsibility follows an owned local value. A reference only grants access; a guard or owner gives cleanup one explicit lifetime and one accountable type.