#await #point #cross #can-t #check #unanchored

nightly anchored

Anchored things can't cross the .await point

1 unstable release

0.1.0 Sep 20, 2021

#6 in #can-t

Apache-2.0

8KB

Anchored

Make things anchored and forbid them from crossing .await point.


lib.rs:

Make things anchored and forbid them from crossing .await point.

Quick Example

The two basic things in this crate are:

  • [unanchored] attribute macro for marking functions and methods you want to apply the .await point crossing check.
  • [Anchored] wrapper struct to wrap over things you want to anchored.

They usually work with each other like this: First wrap [Anchored] on the object you want it to keep away from .await, like MutexGuard, a &mut reference or anything else. Then add [unanchored] attribute to the async function / method to enable this compile time check.

That's all. Now the compiler will check it for you.

For example, the following code can't compile because bar is trying to cross the .await point.

#[unanchored]
async fn foo(){
    let bar = Anchored::new(Bar {});
    async_fn().await;
    drop(bar);
}

And after limiting bar's scope, everything is fine.

#[unanchored]
async fn foo(){
    {
        let bar = Anchored::new(Bar {});
    }
    async_fn().await;
}

Motivation

Some type is not intended to be used under async context, like the blocking Mutex from std, or interior mutable wrapper RefCell or UnsafeCell. Keeping them across the .await point may cause unexpected problems like data race, dead lock etc. When using these types in an async block, we need to check it carefully to ensure they are bounded in the sync context. In other words, not crossing the .await point.

This crate provides a way to enforce this check at compile time. Providing more safety than manually check and can avoid some checks at runtime like RefCell does.

How

This crate is pretty simple. It brings an auto trait Unanchored, and opt-out it for [Anchored]. Like the Unpin trait and Pin wrapper couple from std.

We use the mechanism that when converting an async block into generator, all the variables in scope before one .await will be captured into generator's state. By checking the converted future's type we can tell whether an [Anchored] is captured (crossed the .await point).

Limitation

The type inference phase only cares about the "scope" and won't take "ownership" into consideration. Which means drop the variable does works fine and have no problem in runtime, but it will fail the check:

#[unanchored]
async fn foo(){
    let bar = Anchored::new(Bar {});
    drop(bar);
    async_fn().await;
}

It should be enclosed into a smaller scope like above example explicit.

Related Work

clippy provides two lint options await_holding_lock and await_holding_refcell_ref that can check some specific lock and reference types.

Dependencies

~1.5MB
~35K SLoC