Rust 1.36 stabilizes the Future contract before async syntax
Future supplies a stable polling and wakeup contract while async syntax remains forthcoming. The same release separates allocation from operating-system support and represents uninitialized storage without pretending it contains a valid value.
A future that is waiting for input must tell its executor when another poll may be useful. Otherwise asynchronous code either blocks a thread or spends that thread repeatedly checking for work. Rust 1.36 stabilizes the standard library's Future trait together with the task types needed for this polling and wakeup contract.
The July 4 release announcement describes this as preparation for async and await. Language syntax is still forthcoming, while the stable interface lets libraries prepare their asynchronous implementations.
The release also stabilizes alloc and MaybeUninit<T>. These changes have different purposes, but each makes a systems-level requirement more precise. Allocation need not imply the full operating-system facilities of std, and storage for a future value need not pretend that the value is already initialized.
I find these explicit distinctions central to Rust's usefulness. A type can communicate what an implementation requires and what the caller may assume. That makes the new interfaces worth understanding before treating them as conveniences around familiar operations.
Polling attempts progress without blocking
The versioned Future definition has an associated Output type and one required method. Its signature combines mutable access to the computation with a task context.
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
This is a trait-method signature, not a complete standalone function. The Poll result distinguishes a completed value from a computation that is not ready. Poll::Ready carries the output, while Poll::Pending means another attempt will be needed.
The method should return promptly and avoid blocking. If a socket is not yet readable, for example, the future can retain a clone of the context's Waker and return Pending. When the socket can make progress, another part of the implementation calls the waker to notify the task.
That notification is a request to poll again, not a guarantee that the next poll will complete. The trait documentation explicitly allows the next attempt to remain pending. Code driving futures therefore needs to retain the distinction between becoming worth polling and producing the final output.
Futures are inert until polled. Creating a value implementing this trait does not itself schedule background execution. The executor or other caller must drive it, responding to wakeups and polling computations it still wants to complete. The trait supplies the interaction contract without selecting one executor for every application.
Wakeups belong to the current polling context
The documentation says that only the waker from the most recent polling context should receive the scheduled wakeup. This requirement matters when the context associated with a future changes across polls. Retaining an outdated waker could notify a task that is no longer the right one to drive progress.
Polling in a tight loop is specifically discouraged. The point of storing a waker is to let the task wait for useful notification. Repeatedly calling poll without that signal would consume execution capacity while the underlying event remains unavailable.
The receiver is Pin<&mut Self>, connecting the trait to the pinning types stabilized earlier this year. That form allows implementations to rely on the relevant address guarantees while they remain pinned. It does not require every future to need address sensitivity, but the common method signature can accommodate ones that do.
After a future returns Ready, the caller should stop polling it. The trait does not promise a useful repeated result after completion. Its documentation still requires ordinary Rust safety for such calls, because poll is a safe method. Unspecified post-completion behavior cannot authorize undefined behavior through a safe invocation.
I appreciate that last distinction. A caller can violate an API's intended workflow without giving a safe implementation permission to corrupt memory. The contract can leave completion behavior unspecified while retaining the language's underlying safety requirements.
Allocation becomes available without all of std
Before this release, core supplied facilities that did not require allocation or operating-system capabilities. std supplied allocating types along with broader platform facilities. The release announcement explains that this grouping made libraries needing only allocation depend on more than their basic requirements.
The stable alloc crate exposes allocating types such as Vec<T>. The standard library re-exports these facilities, so ordinary users of std retain their familiar paths. A library can instead use alloc to represent a dependency on allocation without requiring the full standard library environment.
The release gives this setup for a library.
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
pub fn empty_bytes() -> Vec<u8> {
Vec::new()
}
The important release-specific limit is that stable support applies to no_std libraries. The announcement says no_std binaries using alloc still need nightly Rust. It would be inaccurate to take the library example as proof that a complete allocator-using bare-metal binary now needs no unstable support.
Normal binaries using std can depend on these libraries. That lets library authors separate their requirements before all final binary arrangements are stable. A reusable component can state its narrower needs while an ordinary application supplies the broader environment.
Uninitialized storage needs a different type
MaybeUninit<T> addresses the distinction between reserving storage and having a valid value. The release explains that mem::uninitialized pretends a T exists without establishing the invariants the compiler assumes for that type. Even a boolean illustrates the problem, because uninitialized contents do not establish one of its valid values.
Wrapping storage in MaybeUninit<T> tells the compiler not to assume that it already contains a valid initialized T. Code can gradually initialize the storage and later call assume_init() once the validity requirement is satisfied. That final step remains an obligation for the implementation, rather than a check the wrapper performs automatically.
This is especially relevant to low-level allocation and array initialization. Reserving enough bytes is insufficient evidence for constructing references to arbitrary Rust values. The type must allow the temporary absence of an initialized value without letting ordinary uses treat those bytes as though initialization had happened.
The announcement presents MaybeUninit as the safer alternative, with deprecation of mem::uninitialized planned for a later release. As of today, the concrete change is the stable replacement type. The later deprecation is a stated plan and should not be described as already completed.
The versioned memory documentation identifies another obligation, dropping MaybeUninit<T> never calls the destructor for T. An implementation that initializes a value and then abandons the wrapper must account for that value's cleanup itself. The wrapper cannot infer whether initialization happened, which is the state it deliberately permits the program to manage.
Zeroing storage is also not a general substitute for initialization. The documentation distinguishes a zeroed integer from a zeroed reference, because a reference cannot be null. An enum can have a similarly restricted set of discriminants. The validity of all-zero bytes depends on the particular target type, even though writing zeros into the wrapper's storage is allowed.
This makes assume_init() a claim about more than whether some bytes have been written. The stored representation must satisfy the requirements of a valid T. A library using gradual initialization must also preserve that requirement on its failure paths and handle any already initialized resources correctly. The type represents the intermediate state without automatically resolving those obligations for the implementer. Calling the final operation early would discard the distinction that made the wrapper useful.
Other runtime assumptions become more specific
Rust 1.36 brings non-lexical lifetimes to the 2015 edition. The announcement explains that the borrow checker still runs in migration mode for code accepted by the old checker but rejected by the new one. Such cases receive warnings at this stage, including code exposing old soundness problems.
The standard library also adopts the hashbrown-based HashMap implementation while retaining SipHash 1-3 as its default hash algorithm. The container implementation and default hashing policy are distinct choices. A new table implementation does not mean every setting of the external hashbrown crate becomes the standard library's behavior.
Cargo's new --offline mode similarly has a concrete boundary. It resolves using locally cached dependencies and fails when required data would need network access. cargo fetch can populate that cache in advance. Offline operation cannot manufacture a dependency that was never downloaded.
These changes support different environments without obscuring their requirements. A future needs an executor to poll it, an allocating library needs an allocator-capable final environment, and MaybeUninit needs the implementation to establish validity before producing T. Rust 1.36 supplies stable interfaces that state those requirements directly.