Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

YieldSafe auto trait #2890

Closed
wants to merge 4 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions text/0000-yield-safe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
- Feature Name: `yield_safe`
- Start Date: 2020-03-27
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary

[summary]: #summary

This feature enables giving types a default auto trait `YieldSafe` which permits them to be used in an async context.
Consequently, it's possible to mark a type as `!YieldSafe` which disallows yielding control in an async block, while an
object of such type is alive at that point.

# Motivation

[motivation]: #motivation

Some operations are not safe to be performed within an async context. A trivial example might be locking a standard
mutex in an async block - without proper care we risk deadlocking the process. Rust currently doesn't check such
situations at compile time, which might lead to hard to debug runtime errors. While it's true such dangerous operations
are logic errors, not related to language itself, it would be very helpful if there was a mechanism to mark types as
potentially hazardous to use in async code, which would result in a compile error. This mechanism is similar to
detecting async code inside locks in C#.

# Guide-level explanation

[guide-level-explanation]: #guide-level-explanation

This feature adds a new `YieldSafe` default auto trait which propagates according to the same rules as other auto
traits. Yield-safe types can be alive at a point of yielding control, e.g. `.await`-ing a Future. At the same time, no
`!YieldSafe` object can cross a control yield boundary.

struct Widget;

async fn foo() {
}

async fn bar() {
// valid code, since Widget is YieldSafe by default
let w = Widget;
foo().await
}

struct Gadget;

impl !YieldSafe for Gadget {}

async fn invalid() {
// compile-time error, since Gadget is not YieldSafe
let g = Gadget;
foo().await
}

// need to explicitly mark type as YieldSafe
async fn potentially_invalid(value: Box<dyn Debug + YieldSafe>) {
foo().await
}

Additionally, an `AssertYieldSafe` wrapper is introduced to mark enclosed variables as `YieldSafe`, thus avoiding
compile errors when they cross yield boundaries:

use std::sync::Mutex;

async fn foo() {
}

async fn bar() {
let m = Mutex::new(0);

// the following would result in a compile error, since MutexGuard is not YieldSafe
// let lock = m.lock();
// foo().await

// the following compiles without errors, but the user is responsible for ensuring logical correctness
let lock = AssertYieldSafe(m.lock());
foo().await
}

# Reference-level explanation

[reference-level-explanation]: #reference-level-explanation

New `YieldSafe` default auto trait needs to be implemented for all primitive types and follow the same propagation rules
as other auto traits. Each control yield point would need to check if only `YieldSafe` objects are alive at that point.
Generic, `impl` and `dyn` types need also take yield safety into account, to avoid circumventing the system.

One example usage would be a standard `MutexGuard` which is error-prone to use across `.await` points. We would need to
identify other standard types which are similarly dangerous.

`AssertYieldSafe` is a counterpart of `AssertUnwindSafe` with the differences of implementing `YieldSafe` instead of
`UnwindSafe`/`RefUnwindSafe` and not implementing `Future`.

# Drawbacks

[drawbacks]: #drawbacks

A new auto trait might involve backwards incompatible changes, especially where generic types currently cross yield
points. The compiler could limit the problem by checking if the effective lifetime of objects of such types actually
cross yield boundary, e.g.

async fn foo<T>(value: T) {
// potentially valid code, since value doesn't need to cross .await if it doesn't implement Drop (recursively)
bar().await
}

We also could provide a grace period by emitting a warning instead of an error.

# Rationale and alternatives

[rationale-and-alternatives]: #rationale-and-alternatives

- Early solution involved a custom attribute which marked functions as potentially dangerous to call in an async
context, but that solution proved to be inadequate.
- The effect of this solution is close to the one seen in C#.

# Prior art

[prior-art]: #prior-art

C# compiler detects awaiting on async methods inside lock-guarded code and emits an error in such case. Given a lock is
not a special instruction in Rust, it's better to provide a generic mechanism which solves the problem.

# Unresolved questions

[unresolved-questions]: #unresolved-questions

- How to handle more granular control, e.g. yield-unsafe functions?
- How to handle backwards compatibility better?

# Future possibilities

[future-possibilities]: #future-possibilities

This RFC paves a way for extended async code validation. Having syntactically valid code in async blocks is not enough -
we also need to provide a mechanism for logical validation to avoid runtime problems.