Another Rust-related quickie today!
Much like C++, Rust encourages the Resource Acquisition is Initialization pattern: whenever an object goes out of scope, its destructor (drop
in Rust) is called and its owned resources are freed. Possibly as a consequence of this, the Rust programming language (like C++) does not include the equivalent of a finally
construct as found in languages like Java and C#. In most situations, Rust’s standard RAII is perfectly adequate. A downside I’ve encountered is that some resources or resource-like things do not implement the Drop
trait. To work around this, we have to introduce additional struct
s which do have implementations for Drop
to manage these resources. I’ll work through an example below.
Here’s a simple resource:
Here’s an example illustrating how one might (naively) attempt to manage the resource:
In the presence of errors, this code will leak the resource since an early return will prevent the clean-up code from running. The resource leak is obvious in this example (and obvious to the compiler—hence the #[allow(unreachable_code)]
annotation), but this is not always the case. Use of the ?
operator will obscure early returns in ways that are not obvious to the developer or the compiler.
If we owned the Resource
struct
, then we could, of course, directly provide an implementation of Drop
for it and most of the problems would go away. Imagine if you will, however, that this resource is defined by another package. In this situation Rust will prevent us from defining an implementation of somebody else’s trait (i.e. Drop
in this case) for it (this is Rust’s orphan rule which is equivalent to Haskell’s orphan instance rule). We can address this by introducing a wrapper (named ResourceHolder
in this example) with an implementation for Drop
which will release the resource even in the presence of errors:
This is perfectly acceptable. Note that you have to assign the resource holder to a name (_holder
in this case) in order for its lifetime to extend to the end of the enclosing lexical scope—note that the “throwaway” name _
is not sufficient since the compiler will generate code to release this kind of value immediately and release the resource prematurely.
I’m not a big fan of introducing this kind of unused variable, largely because the intent is not clear. To the casual observer, the name might seem insignificant and someone might mistakenly change the name to _
in the future which would drastically change the behaviour of the code (by releasing the resource prematurely). We can eliminate this problem by giving it a more meaningful (non-underscore) name and explicitly calling the std::mem::drop
function:
Now it’s clear what the variable name, holder
, is for and it is clear that holder
must live until the call to drop
(and no further, since drop
consumes its argument). The drop
version still requires this additional holder type which also bothers me.
An approach I like, which addresses all of these concerns, is to introduce the bracket
function (heavily inspired by Haskell):
And here’s how it’s used:
This GitHub project is a full working example project demonstrating these approaches.
Content © 2024 Richard Cook. All rights reserved.