I'd like to open a conversation on scoped resource management.
For the sake of this discussion, I define "scoped resource management" as enforcing that a resource is always released at a predictable position in code. This is useful in many cases, including cleaning up lower-level resource (e.g. closing file handles), cleaning up sensitive data, transaction-like behavior, and more generally ensuring that a protocol is respected.
Sync Rust
In sync code, there are at least two simple ways to manipulate scoped resources. While neither is 100% safe (e.g. double-panics or power failure), both are common idioms.
RAII
struct Resource { ... }
impl Resource {
pub fn new(...) -> Self {
// Acquire resource
}
}
impl Drop for Resource {
// Cleanup resource, even in case of (single) panic.
}
With-style manipulation
impl Resource {
pub fn with<F, T>(cb: F) -> T
where F: FnOnce(&Resource) -> T // + UnwindSafe, if you want to be sure
{
// Acquire
// Call `cb` (possibly catch panics)
// Release
}
Async Rust
In today's async Rust, as far as I can tell, there is no way to achieve the same result.
RAII
Well, Drop
cannot execute async code, and it's not clear to me whether an async-friendly Drop
can be designed, so for the time being, RAII is most likely out of the question.
With-style manipulation
Attempting to convert the sync version of with
into something async
quickly grows complicated (full code)...
impl Resource {
pub fn with<F, Fut, T>(cb: F) -> T
where F: for<'a> KindaAsyncClosure<'a, Resource, T>
Fut: Future<Output=T>
{
// Acquire
// Call `cb` (hopefully catching panics)
// Release
}
trait KindaAsyncClosure<'a, T, U> {
type Future: Future<Output = U> + Send + 'a /* + UnwindSafe */;
fn call(self, r: &'a T) -> Self::Future;
}
impl<'a, F, Fut, T, U> KindaAsyncClosure<'a, T, U> for F
where
F: FnOnce(&'a T) -> Fut,
Fut: Future<Output = U> + Send + 'a /* + UnwindSafe */,
T: 'a
{
type Future = Fut;
fn call(self, r: &'a T) -> Self::Future {
self(r)
}
}
(thanks to jplatte for the help designing this)
...and cannot be used with closures as far as I can tell.
let result = Resource::with( |resource| async { format!("I have a resource {:?}", resource) }).await;
results in
implementation of `KindaAsyncClosure` is not general enough
`[closure@src/main.rs:36:34: 36:98]` must implement `KindaAsyncClosure<'0, Resource, std::string::String>`, for any lifetime `'0`...
...but `[closure@src/main.rs:36:34: 36:98]` actually implements `KindaAsyncClosure<'1, Resource, std::string::String>`, for some specific lifetime `'1`
The same error message shows up if we convert the closure-with-async
into an async closure, so as far as I understand, async closures will not solve the problem.
So what?
As far as I can tell, this is a hole in Rust that blocks some real applications and cannot be solved by a library.
I do not have a solution to offer, but I'd be glad to see ideas on the topic.