Summary
Add a Forget marker trait indicating whether it is safe to skip the destructor before the value of a type exits the scope and basic utilities to work with !Forget types. Introduce a seamless migration route for the standard library and ecosystem.
Motivation
Back in 2015, the decision was made to make mem::forget safe, making every type effectively implement Forget. All the APIs in std were able remain safe after this change, except one. This RFC is not targeted at resource leaks in general, but is instead focused on allowing a number of APIs to become safe, by providing new unsafe guarantees using Forget.
In short, the lack of !Forget types undermines lifetimes, sacrificing all 3 of performance, ergonomics and efficiency. Most Rust code, as well as external APIs, naturally converge towards !Forget types, but in the absence of Forget trait support, those APIs use a mixture of Arc, 'static, allocations, etc.
Forget is designed to allow proxy RAII guards. Futures needed for io_uring-like APIs are essentially a proxy RAII guard, it will be explained later.
Many Rust programmers may find the biggest problem with Forget to be migration. But this RFC describes how migration can be done easily. See #ecosystem-migration section for details.
What is a proxy RAII guard?
thread::scoped was special because it used the RAII guard 1 as a proxy to represent other values, but this proxy was not used to access those values. Instead, we are trusted that the borrow checker will ensure that the guard cannot outlive those values, and therefore that joining the thread in the guard's destructor is enough to ensure that the spawned thread is no longer running. 2
https://rust-unofficial.github.io/patterns/patterns/behavioural/RAII.html
;
let mut buffer = ;
// `guard` is now borrowing from `buffer`
let guard = scoped;
buffer = 4; // Error: `buffer[_]` is assigned to here but it was already borrowed
As we can see, buffer is borrowed for a lifetime 'a, until guard is live. But buffer is used inside another thread, not directly inside JoinHandle<'a>. Thus, JoinHandle is a proxy RAII guard, and its drop handler is used for then necessary cleanup.
https://github.com/rust-lang/rfcs/pull/1084#issuecomment-96875651
Why is the proxy RAII guard gone?
We can't use proxy RAII guard to ensure cleanup anymore. In 2015, the leakpocalypse happened, and the language faced the question: do we make it safe to skip destructors or not? PPYP allows data structures to provide RAII guards while being resilient to skipping the destructor. The only use case in std that cannot be expressed without destructor always running was JoinGuard, which later got replaced too.
In sync Rust, necessary cleanup can be achieved by taking a closure/callback instead of returning a guard object:
As you can see, after calling something_with_clean_up, the control flow is passed to the library. The rest of the user's code cannot continue executing before something_with_clean_up performs a cleanup - so it can, for example, derigester pointer from external code or restore broken invariants.
Thus, there was no point in redesigning the language and delaying Rust 1.0, practically all APIs and patterns could be safely expressed without destructors always running, so making std::mem::forget safe and removing "Proxy RAII Guard" was a good decision at the time.
What is different
Edition 2018 introduced async Rust. But as turned out, nuances in its design conflicted with an earlier decision. All async calls are essentially constructors for state machines which borrow some resources from outside or directly own them. It is user's responsibility to poll those state machines to completion.
!Forget use cases could've been expressed by other means in sync Rust (like taking a callback instead of returning a guard or PPYP), but with async, anything turns directly into impl Future + use<'a> which is equivalent to the RAII guard. This means, that sync pattern of taking a closure cannot be used - everything is transformed into RAII guard by the compiler.
Various OS or C/C++ APIs cannot be made async without performance or ergonomics costs, because futures become a proxy RAII guard for those APIs. PPYP can work for Drain<'a>, but not for io_uring. As long as the future is 'static or directly owns all data it is accessing, Pin guarantees are sufficient. Otherwise, there is no way to make a sound API. Currently, they are forced into using 'static bounds, which is one of the pain points users are reporting about async Rust, together with Send issues.
Let's try to translate the previous example, a widely used pattern, to async Rust.
async
async
In this code snippet we added async modifiers to our functions, as well as await. You may think that cleanup will be done, but it is not guaranteed. All async calls are turned into structs - like the RAII guards we talked about earlier:
async
async
The library is only taking control flow in between await points. Here, future is pinned and Pin's drop guarantee is met (boxed future remains allocated for 'static), but cleanup cannot run - Drop handler of fut is skipped. Thus, APIs that require any cleanup for safety can be expressed in sync Rust, but not in async Rust, making async less attractive, as the operating system APIs and C/C++ libraries cannot be used efficiently, ergonomically, and safely.
Another important observation that we can make is that Pin's drop guarantee only applies to the memory of the Future itself. But if Future borrows a buffer, that buffer can be deallocated or re-used before the drop of the Future is called. See #connection-to-pin.
Examples of unsafe async APIs that can be allowed in sync Rust
Async spawn
Example from the ecosystem: spawn_unchecked
With the Forget trait we can make that API safe:
>);
// Or `struct TaskHandler<'a>(u64, PhantomNonForget<&'a ()>)`;
// Note that this is basically equivalent to async `scope`, as async `scope`
// would be transformed into the `Future` struct, just like `TaskHandler`.
Async DMA
DMA stands for Direct Memory Access, which is used to transfer data between two memory locations in parallel to the operation of the core processor. For the purposes of this example, it can be thought of as memcpy in parallel to any other code.
Let's say that the poll of Serial::read_exact triggers a DMA transfer. It would be safe if we were to block on this future (basically passing control flow to the future itself), but we may instead trigger undefined behavior with forget:
start;
// `DMA` keeps writing to `buf`, which is on the stack. `x` and `y` live on the stack too,
// so they will be corrupted.
corrupted;
GPU
async-cuda, an ergonomic library for interacting with the GPU asynchronously. GPU is just another I/O device (from the point of view of the program), the async model fits surprisingly well. But, this library enforces !Forget via documentation requirements.
Internally, the
Futuretype in this crate schedules a CUDA call on a separate runtime thread. To make the API as ergonomic as possible, the lifetime bounds of the closure (that is sent to the runtime) are tied to the future object. To enforce this bound, the future will block and wait if it is dropped. This mechanism relies on the future being driven to completion, and not forgotten. This is not necessarily guaranteed. Unsafety may arise if either the runtime gives up on or forgets the future, or the caller manually polls the future, and then forgets it.
io_uring
io_uring is another API that needs !Forget to function properly. There are attempts at making safe wrappers like ringbahn, which introduces an internal buffer, or tokio_uring, that requires passing ownership of the target buffer.
rio took an approach like async-cuda, implicitly making its futures !Forget via documentation.
rioaims to leverage Rust's compile-time checks to be misuse-resistant compared to io_uring interfaces in other languages, but users should beware that use-after-free bugs are still possible withoutunsafewhen usingrio.Completionborrows the buffers involved in a request and its destructor blocks to delay the freeing of those buffers until the corresponding request has been completed, but it is considered safe in Rust for an object's lifetime and borrows to end without its destructor running, and this can happen in various ways, including throughstd::mem::forget. Be careful not to let completions leak in this way, and if Rust's soundness guarantees are important to you, you may want to avoid this crate.
WASI 0.3
WASI 0.3 has an uring-like design, and the lack of guaranteed destructors means that for the Rust bindings we have to choose between different options, none of which are great. Thanks @yoshuawuyts for bringing that up!
take_mut
The async version of take_mut cannot be created as it relies on cleanup code to abort the program.
Bumpalo and allocators in general
bumpalo is a fast bump allocation arena for Rust. Their Box cannot be soundly pinned, because you can forget the pinned box and then reset the allocator, invalidating the memory (otherwise you are prevented from calling reset by the shared reference to the allocator).
Performance
As we saw earlier, async code is forced into 'static bounds on any non-trivial task such as spawning or sending messages between tasks. That way, references cannot be used, and users must fall back into Arc or owned types. Arc will ping-pong cache line with the counter between the cores, while owned types enforce unnecessary allocations and clones. Example would be a rumqttc publish which takes topic as Into<String>. Why? Because it sends this topic to another task. If !Forget types were available, a better API choice would be to make the Future returned by publish be !Forget and wait until another task formats the topic into the output buffer and reports either success or failure of the publish.
C/C++ bindings + async do not work well together
It is common for C/C++ APIs to require some cleanup. It is not an issue for sync rust, as wrappers can just take a closure/callback and ensure that cleanup. But all async calls are transformed into impl Future + use<'a>, not passing control flow to the wrapper. io_uring, WASI 0.3 and async-cuda fall into that category too. By not having !Forget types Rust clearly lags behind current moments towards efficient asynchronous APIs. For embedded/kernel development this issue is even worse, as you often cannot afford an allocation due to the lack of resources or complex locking, making borrows your only option and making Pin's drop guarantee not useful for you.
Guide-level explanation
The core goal of Forget trait, as proposed in that RFC, is to bring back the "Proxy Guard" idiom for non-static types, with async being the primary motivation.
If any resources are borrowed by some type T: !Forget, they will remain borrowed until T is dropped.
let mut resource = ;
let borrower: = new;
// Violation of the unsafe contract - `resource` is no longer borrowed,
// so repurposing protected memory is safe.
unsafe ;
let first_byte = resource; // Potential UB
Relation between Forget and Pin
Both Forget and Pin concepts serve a similar purpose - guaranteeing that some memory is not moved or repurposed. How Forget does it? If any resource is borrowed, you cannot take &mut reference to it, as it would be aliased by !Forget type that is borrowing from it. Before !Forget type goes out of scope, removing the borrow, its drop handler must be executed, just like Pin's drop guarantee. So !Unpin protects directly owned memory, while !Forget protects borrowed memory. It is important to note that Forget is not defined around memory, but around values - see #reference-level-explanation.
With Forget, some authors may have the option of borrowing data rather than owning it, making their futures Unpin, but !Forget.
It is possible that in the future we may teach Pin in terms of Forget, because new rustaceans will already be familiar with the borrow checker, which is enough to grasp Forget and how they pin other values using borrows.
Undefined Behavior without !Forget
Consider that example
In this case, handle borrows from buf, but the code that accessing buf is not directly tied to handle, it runs independently of it. Because of this, even if we pin handle, we still can remove the borrow (by ending the lifetime of handle) on buf while JoinHandle's memory remains available (forget(Box::pin(handle))). Thus, Pin guarantees are not enough, we need !Forget.
Functions having signatures with weakening can remove a type from the scope without running its destructor. The following function is an example of a weakening function - after it is called, the borrow checker assumes that the lifetime of T has ended, as well as all borrows held by T.
How API of channels needs to change in order to work with !Forget types.
Currently, channels are created via let (tx, rx) = channel(). This is not compatible with !Forget types. This section explains how developers should use them instead and why.
There exists a way to exploit the old thread::scoped API without any memory leaks3. We can move JoinHandle inside the thread it is meant to protect, thereby creating a cyclic relationship:
This would become a memory leak if instead of spawning the thread we will just return the closure as the handle, but without it's type mentioned to prevent cycle errors. See later.
use ;
>);
This code is clearly unsound because we are aliasing a mutable reference, which permits potential data races and use-after-free issues. Furthermore, many types of channels - including rendezvous channels - can be vulnerable to this issue if their signatures allow an equivalent implementation using reference counting.
What is the actual problem?
The reason of the channels unsoundness
The core issue is not inherent to scoped or JoinHandle per se - it lies in the API design and its interaction with !Forget types. From the type system's perspective, scoped is consuming F and returning another type with some lifetime. This erasure plays a critical role to avoid a cyclic type that will not compile. It creates a pathway for unsoundness when combined with signatures resembling reference-counted types like Arc.
;
JoinHandle and scoped have exactly the same signature as before, but use basic language primitives under the hood. The signature of scoped can be summarized as F + 'a -> 'a - it erased the concrete type F and returned just JoinHandle<'a>. Box<dyn Trait> is a core language feature, we can't remove it for !Forget types, as it would render them unusable.
We will call APis that split ownership of the allocation between tx and rx and allow writes Arc-like. Box<dyn Trait> can cause Arc-like APIs to leak, because we can erase the type of rx and place it in the shared allocation using tx, keeping it alive indefinitely because rx is inside.
Any Arc-like API is capable of causing leaks, without any unsafe code on the scoped side. This demonstrates that approaches such as making JoinHandle: !Send are not feasible. How can we fix it?
Solution for message passing of !Forget types.
Looking at our first example with Arc, let's replace our Arc with a reference:
This change results in compiler errors that prevent the unsound behavior:
error: `mutex` does not live long enough
-/main.rs:23:21
|
22 | let mutex = new;
;
40 | }
| -
| |
| `mutex` dropped here while still borrowed
| borrow might be used here, when `mutex` is dropped and runs the destructor for type ``
error: cannot move out of `mutex` because it is borrowed
-/main.rs:37:10
|
22 | let mutex = new;
;
37 | drop;
| move out of `mutex` occurs here
| borrow later used here
Here, a single step to replace Arc<T> with &'a T allowed code to become sound - JoinHandle: !Forget allowes to pass references into tasks and removes the need for the reference counting in that case - the allocation is not owned by tx and rx, they are borrowing from it, making them not Arc-like. The same approach applies to channels. This approach is not compatible with JoinHandle: Forget and cannot be used today with functions like tokio::spawn due to 'static bound, but ecosystem has some examples:
This code fails to compile, which prevents the unsound behavior. The failure occurs because the borrow checker detects a self-reference: handle borrows queue, but moving handle into queue causes queue to indirectly borrow itself. Since the compiler inserts a call to drop on queue, this self-referential borrow is caught at compile time. But Arc is designed to remove the lifetime, removing borrow-checker's ability to prevent loops, self-references and leaks.
Thus, to support message passing with !Forget types, API authors must rely more heavily on lifetimes. Since Forget types inherently involve lifetime management, using explicit lifetimes (for example, by replacing Arc<T> with &'a T, having PhantomData together with a pointer etc) prevents the formation of cycles that can lead to unsoundness. While this approach is not compatible with APIs that require a 'static bound (such as tokio::spawn), it works in environments like thread::scope or async scopes, where the future itself can be !Forget. Notably, rendezvous channels can be soundly expressed using this API alongside PhantomData.
Traditional combinators and patterns
Async combinators with join, race, or merge semantics will continue to work as they do. If some future passed into them is !Forget, their future becomes !Forget too. Arc cannot be used with !Forget types, but the need for Arc, which is quite a pain point, will decrease, as users will be able to spawn with references directly.
Reference-level explanation
This new auto trait is added to the core::marker and std::marker modules:
pub unsafe auto
Let T be T: !Forget and value be a value borrowed by value of type T. Unsafe code is given the following guarantees:
- If
valueis borrowed byTas&mut,valuecannot be moved/invalidated/borrowed untilTis dropped. - If
valueis borrowed byTas&,valuecannot be moved/invalidated/exclusively borrowed untilTis dropped.
In practice, we disallow skipping the destructor of !Forget types before they exit the scope. Violation is not an immediate undefined behavior, but other code can rely on the destructor running, which can lead to undefined behavior down the road. Unsafe code authors can freely violate this rule, if responsibility is taken.
Several observations can be made about this guarantee. For T: 'static we don't have to run the destructor to fulfill it, as T: 'static can only have 'static borrows, which are assumed to be valid indefinite borrows (like with Pin::static_ref). Another one is, memory borrowed by T: !Forget type cannot be reused or invalidated, as safe code needs to move/take a reference to value, similar to the drop guarantee. value cannot be dropped, as it requires moving.
;
;
let ref_buf = ;
let mut_buf = ;
// We have a guarantee, that no `&mut` can be taken to `ref_buf` until `Foo`'s `drop`.
let foo_ref = Foo;
// We have a guarantee, that no `&/&mut` can be taken to `ref_buf` until `Foo`'s `drop`.
let foo_mut = Foo;
drop; // error[E0505]: cannot move out of `ref_buf` because it is borrowed
drop; // error[E0505]: cannot move out of `mut_buf` because it is borrowed
let ref_first_byte = ref_buf.0; // Allowed
let mut_first_byte = mut_buf.0; // error[E0503]: cannot use `mut_buf.0[_]` because it was mutably borrowed
drop;
drop;
let mut_first_byte = mut_buf.0; // Allowed
drop; // Allowed
drop; // Allowed
// `Baz` cannot be moved or exclusively borrowed until `Foo` is dropped.
Type becomes !Forget if it directly contains !Forget member.
We should either allow !Forget types in statics or make all 'static types Forget because it fulfills the unsafe guarantee and we can't enforce any code running before the program exits or aborts.
let mut resource = ;
let _unforget = execute;
abort; // `resource` is (forcefully) borrowed for `'static`
resource = 42; // unreachable
Standard Library
All APIs in the standard library should be migrated at once. With available migration strategies, there is no benefit in gradual migration, it will greatly reduce the productivity of rustc developers by adding boilerplate and noise into the codebase. An audit must be performed to ensure which APIs must remain Forget. See #ecosystem-migration for more details.
All existing non-generic types in std will continue to be Forget.
Copy
All types that implement Copy must implement Forget too.
Unions
Unions are always Forget. All members of union must be Forget, but it is already covered by other rules and does not need to be enforced.
API changes
Rc/Arc- all APIs for construction, except the newRc::new_uncheckedmethod, only exist forT: Forgettypes. If we decide to not haveimpl<T: 'static> Forget for T {}, in the future we may allow safe constructors forT: ?Forget + 'static(resources are borrowed for'static, it fulfills the guarantee we are giving to the unsafe code) and something along the lines ofT: ?Forget + Freeze(to forbid cycles), author of the RFC is not familiar enough with interior mutability questions.ManuallyDrop<T>always implementsForget, regardless of theT.ManuallyDrop::newis available for types withT: Forget. New unsafe methodManuallyDrop::new_unchecked, available forT: ?Forget, is introduced. We may add a safe constructor withT: ?Forget + 'static, as we allow forgetting in statics.Box::<T>::into_ptris available only forT: Forget. As forT: !Forgetusers shouldManuallyDrop::new_uncheckedand take the pointer via&raw mut. It will still be allowed to pass this pointer toBox::from_ptr.Box::<T>::leakis available only forT: Forget.forget_unchecked, a new unsafe function, is added to forgetT: ?Forgettypes. It is a wrapper aroundManuallyDrop::new_unchecked, just asforgetis a wrapper aroundManuallyDrop::new.PhantomNonForgetis a!ForgetZST for types to become!Forget. If we decide to haveimpl<T: 'static> Forget for T {}, we should add a generic parameter/lifetime toPhantomNonForget.- Similar to other APIs,
T: Forget->Drait<T>: ForgetandT: !Forget->Drait<T>: !Forget. - APIs like
std::sync::mpsc::Sender::sendare available only forT: Forget. - Possibly new channels should be introduced, that are compatible with
T: !Forgettypes too. Vec::set_lenis available only forT: Forgettypes, to not create a footgun forunsafecode in the wild. Maybe a new method should be added to supportT: ?Forget.ptr::writeis available for all types.- Etc
Ecosystem Migration
Migration using local_default_bounds RFC
The local_default_bounds RFC facilitates a smoother migration without requiring an edition change. In essence, it allows users to override default bounds on generics and associated types, such as change from Forget to ?Forget. The process is comparable to the adoption of const fn, which is already accepted and loved feature, that keeps expanding and does not cause ecosystem splitting.
We will provide an opt-in mechanism for crates to modify default bounds in function signatures.
// Crate that has migrated
// Crate that has not migrated
In the context of the local_default_bounds RFC, along with introducing the Forget trait, Bounds for Self and associated types should default to ?Forget rather than Forget. This change is not observable for code that does not explicitly opt into using Forget, as default_generic_bounds will continue to default to Forget. A more detailed explanation will follow later.
As discussed in #semver-and-ecosystem, libraries adopting ?Forget in their signatures will, at most, require a minor semver change. Consequently, migrating to ?Forget would be equivalent to the now stable const fn feature. Libraries have already been adopting const fn without causing ecosystem fragmentation, as pull requests continue to be merged, progressively making more functions const.
Not interested in migration crates
Some crates may refuse to migrate due to being unmaintained, the only difference is that for downstream crates their signatures would be filled with T: Forget. This is only natural, as those crates were written with that assumption as if they manually put T: Forget on their signatures. Some automatic methods to determine that function can accept Forget types are not feasible. We are already not doing it for const fn, it would be a semver hazard and only safe code can touch T, as analysing unsafe code is against the design of the language.
If the crate is maintained, however, migration should not be difficult.
#![forbid(unsafe)] crates
- Set the appropriate bounds:
// can be with `cfg_attr`
-
Resolve any compilation errors by explicitly adding
+ Forgetwhere needed. -
Optionally: Recurse into your dependencies, applying the same changes as needed. Most probably you will use
!Forgettypes with well-maintained crates providing combinators or containers.
For crates with unsafe code (like libcore)
- Set the appropriate bounds:
-
Audit your codebase to work properly with
!Forgettypes. -
Resolve any compilation errors by explicitly adding
+ Forgetwhere needed. -
Optionally: Recurse into your dependencies, applying the same changes as needed.
Semver and compatibility, ecosystem splitting
This approach is targeted at minimizing problems between different crates in the ecosystem. For any library, opting into using Forget and accepting those types will be a minor semver change.
Earlier it was stated that Bounds for Self and associated types should default to ?Forget instead of Forget. This is due to an important case. If a user of the library updated earlier than the library, then without that change it will observe that associated types of traits are Forget, so it would be a breaking change for the library to lift that constraint in the future. But now, the user will observe ?Forget, thus it cannot rely on them being Forget. But for users that did not migrate, as well as the library itself, it will not be observable due to default_generic_bounds still being Forget.
// This indicates that user explicity opted in
// After opting in, user needs to add `T::baz(..): Forget` to silence the error - quite easy.
async
// A library that did not migrate yet. `Trait::bar(..)` is not locked into `Forget`, but
// this `other_crate` and other crates can only observe `Trait::bar(..): Forget` cases.
Macros
If macro-library generates code, some problems during the migration are possible:
Changing default
It is not required, but in next editions we may swap the default for default_generic_bounds. Crates that want to continue using old default in next editions will set #![default_generic_bounds(Forget)].
Migration over the edition with default auto traits
If local_default_bounds is not accepted, we can make a satisfactory migration by having editions <= 2024 have Forget as the default, and editions after 2024 have ?Forget as the default.
While it will not split the ecosystem, it will require everyone to make a migration just as in the local_default_bounds solution. There are concern about locking crates into Forget bounds on associated types in traits (like async fn). Migration can probably be automated for crates without unsafe code.
Drawbacks
Migration
If local_default_bounds is accepted, migration would be practically seamless, as described in #migration. Even if it's not accepted, less seamless but still acceptable solution would be a change over edition.
Message Passing
A traditional approach to the message-passing cannot be applied to !Forget types - slightly different APIs should be developed, preserving a
lifetime connection between tx and rx handles.
Rationale and alternatives
All types were assumed to be !Forget in Rust's early days, and then it was changed in hurry. They flow naturally out of Rust's type system, do not clash with any preexisting concepts that do not directly involve forgetting and are used pleasantly and intuitively, modulo migration. With the async built around Future trait it became apparent that language directly lacks this feature. Being simple and non-disturbing, it's hard to find something that would fit that purpose better.
We can do nothing, but use cases just keep piling up.
The author of https://zetanumbers.github.io/book/myosotis.html is working on another approach to that problem, but it is not public yet.
It is also possible to preserve current Arc-like channels by disabling the type ereasure for !Forget types - disallow the signatures like F + 'a -> 'a. This way, JoinHandle would be generic over the closure, not a lifetime: JoinHandle<F>. That way, we will trigger the infinitely recursive type error instead of a borrow-checker's error. But this will be a major downside to !Forget types, making them barely usable.
Prior Art
- https://github.com/rust-lang/rfcs/pull/1084#issuecomment-96875651
- https://github.com/rust-lang/rfcs/pull/1094
- https://internals.rust-lang.org/t/forgetting-futures-with-borrowed-data/10824
- https://github.com/aturon/rfcs/blob/scoped-take-2/text/0000-scoped-take-2.md
- https://without.boats/blog/the-scoped-task-trilemma
- https://without.boats/blog/asynchronous-clean-up/
- https://zetanumbers.github.io/book/myosotis.html: an independent exploration of the same problem space with similar, but subtly different, conclusions.
- https://hackmd.io/@wg-async/S1Q6Leam0: a design meeting regarding the previous post
- https://blog.yoshuawuyts.com/linear-types-one-pager/
Leakpocalypse
- https://github.com/rust-lang/rfcs/pull/1066
- https://github.com/rust-lang/rust/issues/24292
- https://cglab.ca/~abeinges/blah/everyone-poops/
- https://github.com/rust-lang/rfcs/pull/1085
- https://doc.rust-lang.org/std/thread/fn.scope.html
- https://smallcultfollowing.com/babysteps/blog/2015/04/29/on-reference-counting-and-leaks/
Usage of the pattern
- https://doc.rust-lang.org/std/thread/struct.Builder.html#method.spawn_unchecked
- https://docs.rs/async-task/latest/async_task/fn.spawn_unchecked.html
- https://blog.japaric.io/safe-dma/
- https://docs.rs/async_nursery/latest/async_nursery/
- https://without.boats/blog/the-scoped-task-trilemma/
- https://docs.rs/async-scoped/latest/async_scoped/struct.Scope.html#method.scope_and_collect
Miscellaneous
- https://github.com/rust-lang/rfcs/issues/1111
- https://tmandry.gitlab.io/blog/posts/2023-03-01-scoped-tasks/
MustMove types
- https://faultlore.com/blah/linear-rust/
- https://smallcultfollowing.com/babysteps/blog/2023/03/16/must-move-types/
- https://blog.yoshuawuyts.com/linearity-and-control/
Unresolved questions
- [ ] Maybe the name
Forgetis misleading, as its core is around theunsafeguarantee of borrowed resources and the destructor? - [ ] Maybe force
impl Forget for T where T: 'static {}and add a generic to thePhantomNonForget? Use cases and unsafe guarantee are fine with it, and we already allow!Forgetinstatic. - [ ] Maybe add
StaticForget<T: ?Forget + 'static>: Forget. - [ ] How does it interact with
&own? - [ ] Maybe make
Vec::set_lenavailable forT: ?Forget, but with a new unsafe precondition. Crates with unsafe code that are manually migrating to support!Forgetwould need to be aware of that change and verify/modify their unsafe code to work correctly with!Forgettypes, or manually restrain them toForget. - [ ] Which approach to migration should be followed?
- [ ] How should it interact with
async Drop?
Future possibilities
This RFC will allow async Rust to come closer to the sync ergonomics, but some code will not be able to reach this end goal and insert "abort bombs" into mandatory destructors. This is strictly better than today's status quo: unsafe in application code - you can work with it, but this defies the whole point of Rust. A more robust approach would be the Linear/MustMove/!Drop types. This RFC makes a step towards more liveness guarantees, making them closer. As for the biggest problem - unwinding - with async, we have more choice over our behavior during unwinds. Even if we do not succeed with effects forbidding unwinding, the future containing linear type may catch any unwind during the poll and return Poll::Pending, potentially recovering - async Drop looks promising too.
Maybe if !Forget type borrows itself, it would be equivalent to the pinning?