patterngoMajor
Golang-like defer in Rust
Viewed 0 times
rustgolangdeferlike
Problem
In Go, you can use the
How can this functionality be achieved in Rust? I know about RAII, but in my specific case the state is in an external system. I'm writing a test that writes a key to a key-value store, and I need to make sure it's deleted at the end of the test regardless of whether or not the assertions in the test cause a panic.
I found this Gist but I don't know if this is a recommended approach. The unsafe destructor is worrisome.
There is also this issue on the Rust GitHub repository, but it's three years old and clearly not very relevant anymore.
defer keyword to execute a function when the current function returns, similar to the traditional finally keyword in other languages. This is useful for cleaning up state regardless of what happens throughout the function body. Here's an example from the Go blog:func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}How can this functionality be achieved in Rust? I know about RAII, but in my specific case the state is in an external system. I'm writing a test that writes a key to a key-value store, and I need to make sure it's deleted at the end of the test regardless of whether or not the assertions in the test cause a panic.
I found this Gist but I don't know if this is a recommended approach. The unsafe destructor is worrisome.
There is also this issue on the Rust GitHub repository, but it's three years old and clearly not very relevant anymore.
Solution
(e: don't miss bluss's answer and their scopedguard crate, below.)
The correct way to achieve this by having code that runs in a destructor, like the
In any case, that macro is now (much!) improved due to recent changes, in particular, pnkfelix's sound generic drop work, which removes the necessity for
Output:
Although, it would be syntactically nicer as:
(The
The use of the generic
Also, for maximum flexibility about what the deferred code can do with captured variables, one could use
That will also require constructing the
All in all (playground):
(Same output as the original.)
The correct way to achieve this by having code that runs in a destructor, like the
defer! macro you link to does. For anything more than ad-hoc testing I would recommend writing a handle type with a proper destructor, e.g. one interacts with std::sync::Mutex via its MutexGuard type (returned by lock): there's no need to call unlock on the mutex itself. (The explicit handle-with-destructor approach is more flexible too: it has mutable access to the data, whereas the deferred approach may not be able to, due to Rust's strong aliasing controls.)In any case, that macro is now (much!) improved due to recent changes, in particular, pnkfelix's sound generic drop work, which removes the necessity for
#[unsafe_destructor]. The direct update would be:struct ScopeCall {
c: F
}
impl Drop for ScopeCall {
fn drop(&mut self) {
(self.c)();
}
}
macro_rules! defer {
($e:expr) => (
let _scope_call = ScopeCall { c: || -> () { $e; } };
)
}
fn main() {
let x = 42u8;
defer!(println!("defer 1"));
defer!({
println!("defer 2");
println!("inside defer {}", x)
});
println!("normal execution {}", x);
}Output:
normal execution 42
defer 2
inside defer 42
defer 1Although, it would be syntactically nicer as:
macro_rules! expr { ($e: expr) => { $e } } // tt hack
macro_rules! defer {
($($data: tt)*) => (
let _scope_call = ScopeCall {
c: || -> () { expr!({ $($data)* }) }
};
)
}(The
tt hack is necessary due to #5846.)The use of the generic
tt ("token tree") allows one to invoke it without the inner { ... } when there are multiple statements (i.e. it behaves more like a "normal" control flow structure):defer! {
println!("defer 2");
println!("inside defer {}", x)
}Also, for maximum flexibility about what the deferred code can do with captured variables, one could use
FnOnce instead of FnMut:struct ScopeCall {
c: Option
}
impl Drop for ScopeCall {
fn drop(&mut self) {
self.c.take().unwrap()()
}
}That will also require constructing the
ScopeCall with a Some around the value for c. The Option dance is required because calling a FnOnce moves ownership, which isn't possible from behind self: &mut ScopeCall without it. (Doing this is OK, since the destructor only executes once.)All in all (playground):
struct ScopeCall {
c: Option
}
impl Drop for ScopeCall {
fn drop(&mut self) {
self.c.take().unwrap()()
}
}
macro_rules! expr { ($e: expr) => { $e } } // tt hack
macro_rules! defer {
($($data: tt)*) => (
let _scope_call = ScopeCall {
c: Some(|| -> () { expr!({ $($data)* }) })
};
)
}
fn main() {
let x = 42u8;
defer!(println!("defer 1"));
defer! {
println!("defer 2");
println!("inside defer {}", x)
}
println!("normal execution {}", x);
}(Same output as the original.)
Code Snippets
struct ScopeCall<F: FnMut()> {
c: F
}
impl<F: FnMut()> Drop for ScopeCall<F> {
fn drop(&mut self) {
(self.c)();
}
}
macro_rules! defer {
($e:expr) => (
let _scope_call = ScopeCall { c: || -> () { $e; } };
)
}
fn main() {
let x = 42u8;
defer!(println!("defer 1"));
defer!({
println!("defer 2");
println!("inside defer {}", x)
});
println!("normal execution {}", x);
}normal execution 42
defer 2
inside defer 42
defer 1macro_rules! expr { ($e: expr) => { $e } } // tt hack
macro_rules! defer {
($($data: tt)*) => (
let _scope_call = ScopeCall {
c: || -> () { expr!({ $($data)* }) }
};
)
}defer! {
println!("defer 2");
println!("inside defer {}", x)
}struct ScopeCall<F: FnOnce()> {
c: Option<F>
}
impl<F: FnOnce()> Drop for ScopeCall<F> {
fn drop(&mut self) {
self.c.take().unwrap()()
}
}Context
Stack Overflow Q#29963449, score: 61
Revisions (0)
No revisions yet.