Implicit code control and defer statements
DISCLAIMER: This text idea once created during a brainstorm aims to be possible answer for many questions about async drop, many new possible features mentioned are for feature symmetry or aim at ability to write all implicit code explicitly in some way or fasion. Some code examples may be related to questions asked in the Motivation section.
Motivation
While striving to design async drop, there's one fundamental problem which is unresolved. Since Rust uses poll-based futures, future's drop (i.e. future cancellation) should be one of valid ways to deal with async tasks. To achieve flexible usage of futures (via select! for example) cancellation safety becomes a necessity in some circumstances, which in turn requires developer to carefully look at .await
expressions and their placement. However, some designs of async drop may automatically generate invisible and sudden (in a new dependency version) await points which would make cancellation safety very difficult.
Also remember that Drop
is the only way to make sure your code runs at all exits from the scope, thus it is common to see people writing structs with Drop
implementation for only one scope in the entire code. Multiple ways of running these async destructors are discussed: concurrently, sequentially, in a new spawned task; picking one favorite could make others too verbose and tedious.
Consider another problem. Destructors are not allowed to have an error. But are they really infallible? As you may know File
practically flushes inside its drop and flushing is actually very much fallible. So is ignoring error the right way there? Maybe, sometimes it's fine, but maybe one time you would have liked to print a warning about that?
Proposal
The core problem may lie in the implicit code. If so adding control over implicit drops may solve many problems. Please remember that new syntax is not finalized yet and async_drop is just there to make a point.
Defer blocks
defer {...}
block is defined to copy its contained code to places just like placement of calls to a drop glue. There is a trivial example:
defer {
println!("hello");
}
println!("world");
Output:
world
hello
It is based upon a similar idea from Defer blocks and async drop. However, these entire scopes are duplicated and inserted near calls to drop glue, thus potentially introducing enormous bloat, (thus maybe this should be the default defer
behavior?) So you would usually want to introduce an indirection like:
#[indirect]
defer {
println!("hello");
}
println!("world");
which desugars into
defer {
(|| {
println!("hello");
})()
}
println!("world");
There could be necessity for defer blocks to not be copied into unwind or coroutine_drop code:
defer #[cfg_where(not(any(unwind, coroutine_drop)))] {
println!("We are on the happy path!");
}
If conditional move occures we forbid regular defer blocks unless we use weak defer blocks, which are run if variable did not move somewhere else:
let a = "ඞ".to_owned()
#[weak(a)] // or simply #[weak]?
defer {
println!("Who is that? {a}");
}
if check() {
foo(a);
}
so that message would be printed only if check()
returned false, alternatively
let a = "ඞ".to_owned()
#[on_scope_exit(a)]
defer {
println!("Who is that? {a}");
}
if check() {
foo(a);
}
should print during move too.
Implicit and explicit attributes
Let's start with async_drop. #[implicit(async_drop)]
would allow to insert implicit code like async_drop(fut).await
, introducing invisible await points. Right now this is probably not a desired default behavior, so it's #[explicit(async_drop)]
for now, and can be changed between editions.
#[implicit(async_drop)]
async fn foo(x: Bar) {
let y = x.recv().await;
println!("This is y: {y:?}");
}
may be equivalent to:
async fn foo(x: Bar) {
defer { async_drop(x).await };
let y = x.recv().await;
println!("This is y: {y:?}");
}
Could there be others? #[implicit(await)]
? #[explicit(drop, into_iter, into_future)]
? #[explicit(code)]
to make "everything" explicit?
#[implicit(await, async_drop)]
async fn foo(x: Bar) {
let y = x.recv();
println!("This is y: {y:?}");
}
NOTE: one other important aspect would be to preserve macro hygiene.
Essential finalizer methods
We can mark methods which consume self
as #[essential_finalizer]
to indicate their importance and lint where it's not used. For example:
// src/foo.rs
#[warn(missing_essential_finalizer)]
// #[cfg_attr_where(warn(missing_essential_finalizer), not(unwind))] // to not warn if finalizer is not called during unwind
struct Foo();
impl Foo {
#[essential_finalizer]
pub fn finalize(self) -> Result<()> {
// flush data and everything
Ok(())
}
#[essential_finalizer]
pub fn finalize_with_param(self, param: usize) -> Result<()> {
// ...
}
}
// src/lib.rs
fn process(x: Foo) {
// /-----^
// Warning: Hey buddy, you forgot calling one of essential finalizer methods:
// Foo::finalize, Foo::finalize_with_param.
println!("length of x: {}", x.len())
}
so you edit process:
fn process(x: Foo) {
defer {
// No longer ignoring errors in destructors!
if let Err(e) = x.finalize_into_string() {
log::error!("this is bad: {e:?}");
}
};
println!("length of x: {}", x.len())
}
or to suppress it for one argument:
fn process(#[allow(missing_essential_finalizer)] x: Foo) {
// or `defer { let _ = x; }`?
println!("length of x: {}", x.len())
}
Implicit and explicit lints
To ensure gradual adoption of async_drop and other possible features I see some new lints being really helpful:
missing_*
tells about missing some explicit or implicit code, likemissing_async_drop
tells about missing async drops of values or parents of values withimpl AsyncDrop
#[warn(missing_async_drop)] // maybe this should be default?
#[explicit(async_drop)] // this is default but anyway
async fn foo(x: Bar) {
// Warn: you should probably defer async_drop(x).await
// because it's `Bar: AsyncDrop` or whatever
let y = x.recv().await;
println!("This is y: {y:?}");
}
implicit_*
tells about some unwanted implicit code, likeimplicit_async_drop
tells about implicitasync_drop(value).await
of values withimpl AsyncDrop
or their parents
#[warn(implicit_async_drop)]
#[implicit(async_drop)]
async fn foo(x: Bar) {
// Warn: x generates defered async_drop call and awaits it
// because it's `Bar: AsyncDrop` or whatever
let y = x.recv().await;
println!("This is y: {y:?}");
}
Let-defer declarations and defer assignments
Let-defer declarations allow to declare variables in the defered context, and then defer assignments are used to assign new values to these variables:
let defer mut a = 0;
defer {
println!("{a}!");
}
defer a = { 42 };
prints:
42
.eivom taht ni ekil si emit fo wolf ,ees nac uoy sA
We can add ability to declare returned value as variable:
fn foo() -> Result<String> {
#[return_place] // Automatic #[cfg_where(return)]
let defer output;
defer #[cfg_where(return)] {
if let Err(e) = output {
let err_len = e.to_string().len();
warn!("Get ready this is bad, error message is {err_len} bytes!"),
}
}
// ...
Ok(())
}
Defer expressions
To explicitly enable async_drop inside expressions we could use:
#[explicit(async_drop)]
async fn foo(x: Bar) {
baz(x.recv().await(defer |fut| { async_drop(fut).await }));
// baz(x.recv().defer |fut| { async_drop(fut).await }.await); // Or this?
// baz(x.recv().defer(async_drop).await); // Or this?
}
or some other way to include defers into expressions
Custom implicit code
With functionality described above, even proc_macros can define attribute to add custom drops or other implicit code. However there could be advantages to make it built-in, like ensuring expansion order.
#[use_custom_drop(MyDrop)]
fn foo() {
// Dark magic territory
}
Benefits to the internal structure of rustc?
With defer blocks drops can now be generated during HIR stage, instead of during building of MIR, possibly allowing flexibility. It is definitely useful concept for implicit async_drop alone.