June 4, 2020·5 min read·engineeringrustasyncembeddedcargo

Rust 1.44 removes a standard-library dependency from async lowering

Async code can now compile without std because polling context no longer depends on thread-local storage. Cargo also gains built-in dependency and feature graph inspection.

An asynchronous state machine does not inherently require an operating-system thread-local variable. Rust's implementation nevertheless used thread-local storage to pass the task context through generated async code. That tied the language facility to std. Rust 1.44 changes how the compiler passes that context and makes async and await usable in no_std code.

The release announcement names this change and built-in cargo tree as its highlights. Their mechanisms address different boundaries. The async change removes an implementation dependency that constrained where a language facility could be used. The Cargo change exposes dependency relationships through a tool shipped with the package manager.

I find Rust most compelling where its semantics give an explicit account of low-level behavior. Removing an unnecessary dependence on the full standard library fits that direction. It makes the available abstraction depend more closely on what the implementation actually needs.

A polling context must reach the suspended computation

The async implementation change replaces a thread-local task-context path with generator resume arguments. Generated async code needs a polling context when it polls an awaited future. Previously, the implementation made that context available through a thread-local slot in std.

The patch moves the supporting future machinery into core and passes the context when resuming the underlying generator. The future wrapper's poll method receives the context, converts it into the internal representation, and resumes the generator with that argument. Await lowering can then obtain the context from generated state instead of consulting thread-local storage.

When the awaited future returns Pending, the generated computation yields. On resumption it receives the new task context. That detail matters because the context belongs to the current poll. Storing the first context indefinitely would not express the required interaction with later polls.

This is compiler and library cooperation. Changing a public import in application code would not remove a hidden dependency in the generated implementation of await. The compiler must generate a different path, and the supporting types must live in the library layer that no_std programs can use.

Internal pointers preserve an external safe interface

The patch uses an internal resume type containing a non-null pointer to the context. Its comments explain that the generator interface cannot directly express the required borrowed-context relationship. They also note that bare raw pointers would make every generated future fail Send and Sync, which would be an unwanted change to future behavior.

The wrapper therefore supplies the internal representation and the required marker implementations. Its contents are private in the final patch. Application authors continue to use Future and normal async syntax. The pointer conversion belongs to the compiler's support machinery, where the implementation can enforce the relationship between polling and resumption.

The future wrapper remains non-Unpin, allowing the generated state machine to contain self-referential borrows under its pinning requirements. Polling projects the pinned wrapper onto the generator field before resuming it. Removing thread-local storage does not remove these state-machine constraints.

A compile-pass test in the patch demonstrates the supported source shape. This example is a library compiled with the 2018 edition.

#![no_std]

use core::future::Future;

pub async fn wait_for(value: impl Future<Output = u8>) -> u8 {
    value.await
}

The example establishes that the language transformation can compile without std. It does not provide a scheduler or an I/O implementation. An environment still needs a way to poll the resulting future and arrange further polls when progress becomes possible. The release removes the compiler-generated thread-local requirement, not the execution responsibilities surrounding a future.

I regard that scope as the important practical distinction. A constrained environment can supply its own execution policy while using the language's state-machine syntax. It no longer needs to provide the full standard-library environment solely because await lowering chose thread-local context storage.

Cargo exposes the dependency graph directly

The integrated cargo tree command answers a separate set of dependency questions. The Cargo 0.45 command documentation describes forward dependencies, reverse dependencies, duplicate versions, and feature edges. These views are useful because a resolved dependency is often present through another package rather than a direct manifest entry.

cargo tree
cargo tree -d
cargo tree -e features -i syn

The first command shows dependencies. Its duplicate view selects packages appearing in multiple versions and implies an inverted view. The feature example shows which dependents cause features of syn to be enabled. That distinguishes asking what depends on a package from asking why a particular capability entered the graph.

Repeated subtrees receive a (*) marker after their dependencies have already appeared. This is display deduplication. It does not mean Cargo has removed a dependency from the resolved graph. The --no-dedupe option repeats those subtrees when following references across the output becomes difficult.

The default dependency kinds include normal, build, and development dependencies. A target filter defaults to the host platform, with all available to include every target. These settings affect interpretation. A graph viewed for the host need not describe every dependency selected for another deployment target.

Workspace scope also matters for reverse dependencies. The command defaults to the tree associated with the workspace member in the current directory. Its workspace option extends the reverse view across all members. A package may therefore have additional consumers outside the current subtree even when the local inverted output is short.

Feature explanations need their dependency path

The inverted feature view follows the chain that enables each feature. A package can receive a feature through another package's defaults. Looking only at the direct manifest entry would miss that cause. Cargo's example traces features back through intermediate packages to the package selected on the command line.

Duplicate versions are another diagnostic rather than an automatic error. The documentation notes possible build-time and binary-size benefits from avoiding repeated versions. The command identifies which dependents introduce them, leaving maintainers to assess whether their requirements can converge.

Rust 1.44 consequently makes two forms of hidden context explicit. Async polling receives its context through generated arguments, which permits support in core. Dependency inspection can display the package or feature path that explains a resolved choice. Each change exposes the information needed at the boundary where it is consumed.