October 17, 2024·6 min read·engineeringrustlifetimespointerscargo

Rust 1.82 separates lifetime capture from outlives bounds

Precise capture syntax states which parameters an opaque return may use. Native raw-pointer syntax addresses another case where an indirect expression can impose the wrong requirements.

An opaque return type may need to use an input lifetime without promising to outlive that lifetime. Rust programmers have used additional bounds and helper traits to express that distinction, sometimes imposing stronger requirements than the function actually needs. Rust 1.82, released on October 17, adds precise capture syntax so a return-position impl Trait can name the generic parameters available to its hidden type.

The same release adds native syntax for constructing raw pointers without first creating references, and Cargo gains info for inspecting packages. The language changes are especially interesting because they remove indirect spellings that can introduce unintended obligations.

I like Rust's ability to state low-level constraints, but the syntax needs to distinguish the constraints being stated. Capturing a lifetime and requiring an outlives relationship are different operations. A direct expression for the former makes the function's contract easier to assess.

Hidden types can depend on generic parameters

A return-position impl Trait exposes trait bounds while hiding a concrete return type. That hidden type can depend on generic parameters from the function. The release announcement calls this capturing, and explains that it affects borrow checking.

In Rust 2021 and earlier editions, ordinary functions and inherent methods do not automatically capture an input lifetime unless it appears syntactically in the opaque return type. A function that returns its borrowed input behind impl Sized therefore needs a way to state that lifetime dependency.

Rust 1.82 permits a use<...> bound. The announcement's simplest example adds use<'_> to capture the elided input lifetime. The returned concrete type can then use that lifetime even though the visible trait requirement is only Sized.

This is a statement about the parameters the hidden type may use. It does not reveal the hidden type, and it does not convert an arbitrary borrowed value into an owned one. Borrow checking still accounts for the lifetime relationship represented by that hidden type.

The precise-capturing RFC explains the design in terms of applying generic arguments to an opaque type. It also explores syntax alternatives. The release announcement's impl Trait + use<...> spelling is the stabilized interface, which differs from some historical proposal examples.

An outlives bound says more than capture

Before this addition, programmers often added + '_ or another lifetime bound to make an otherwise hidden dependency visible. The announcement describes that as the outlives trick. It works in some simple cases, but it can overconstrain a real function.

Its example returns an iterator that captures a context with one lifetime and yields references with another. Both lifetimes are relevant to the hidden iterator. Requiring the opaque return to outlive the context lifetime can force a relationship with the yielded references that the function does not actually require.

Adding another outlives bound between the two inputs would assert a relationship that is not generally true. The problem is therefore not solved by making the lifetime declarations longer. The signature needs to express dependency on both parameters without ordering their lifetimes unnecessarily.

With precise capture, the function can list both lifetimes in use<...>. That permits the hidden type to depend on them while leaving their actual relationships to the remaining bounds and function body. The capture list answers which parameters are available to the hidden type.

The older Captures trick used a dummy trait with a blanket implementation to express this dependency. It could encode the needed relationship, but introduced another abstraction solely to communicate a compiler-level fact. The direct syntax removes that helper from the explanation a reader must reconstruct.

The initial stabilization has limits

Rust 1.82 does not permit precise capture syntax everywhere. The announcement excludes traits and trait implementations, where in-scope lifetime parameters are already captured by default. It also requires listing every in-scope generic type and const parameter.

That means a capture list in this release should not be described as unrestricted selection among all generic parameters in every context. Its immediate benefit is precise control over lifetimes in the supported opaque returns. The design has broader ambitions, but the stabilized subset is the contract available today.

The announcement also explains the intended role in Rust 2024. That edition will change the default to capture all in-scope lifetime parameters, and precise syntax will allow opting out of that default. This is an announced edition direction, while the explicit syntax is usable now under Rust 1.82's stated restrictions.

The distinction is useful for migration planning. A compiler release can provide a syntax before a later edition changes defaults. Code can state its intended capture behavior explicitly without waiting for the default rules to change.

A raw pointer must not start with an invalid reference

The release's native raw-pointer operators solve another problem of expressing one operation through another. An ordinary & or &mut creates a reference. References carry validity and alignment requirements at creation, even when code immediately casts them to raw pointers afterward.

For an unaligned field in a packed struct, that temporary reference is already invalid. Casting it cannot undo the invalid construction. A program intending only to obtain a raw pointer needs an operation that never creates the reference.

Rust previously provided addr_of! and addr_of_mut! for that purpose. Rust 1.82 adds the native spellings &raw const and &raw mut. The announcement emphasizes that their operands are place expressions. The operation identifies the storage location without reading the value through a reference.

For a packed value named p, the relevant form is:

&raw const p.not_aligned_field

Creating that pointer does not authorize an aligned load from it. The release example uses read_unaligned for the later access. Pointer construction and pointer dereference have separate requirements, and the new syntax keeps the former from accidentally performing an invalid reference construction first.

This also explains why native syntax is more than cosmetic. The expression now states the intended operation directly, with the mutability distinction visible beside raw. A reader does not need to recognize a macro name before understanding that the expression avoids a reference.

Unsafe external declarations assign responsibility

Rust 1.82 also accepts unsafe extern blocks. The release explains the safety responsibility involved in declaring a foreign function's signature. If the signature is wrong, calling the item can cause undefined behavior even when the call appears to meet the declared type's requirements.

Marking the block unsafe puts that responsibility on its author. Within the block, an item can be declared safe when callers do not need to uphold extra safety conditions. Unmarked items remain conservatively unsafe. This separates correctness of the declaration from obligations imposed on each use.

The release similarly introduces unsafe attribute syntax for attributes including no_mangle, link_section, and export_name. These can undermine safety without appearing inside an ordinary unsafe block. Making their status visible aligns the source spelling with the responsibility the author is already taking.

I find that direction consistent with precise capture and raw pointers. Each change distinguishes operations that looked similar under an older spelling. Declaring a signature, satisfying a call's preconditions, and selecting an exported symbol name involve different arguments about correctness.

Cargo info describes the dependency version in context

The new Cargo command supplies a smaller but useful improvement. cargo info displays registry-package metadata, including features, licensing, and the declared Rust version. By default it describes the version in the local Cargo.lock when one exists, and can indicate that a newer version is available.

That default matters because documentation for the latest registry version may not describe the dependency a project actually resolves. An explicit package-version argument lets the caller inspect another release. The announcement also shows a suggestion for using an inverted dependency tree to find why the package is present.

The output separates the selected version from the latest available version. This lets a caller inspect the package already in the dependency graph while still learning that an update exists, without confusing information about the two releases.

The command developed as the external cargo-information tool before its integration. Moving it into Cargo makes that inspection available without requiring a separately installed extension. It does not change the dependency selection merely by reporting package information.

For the language additions, the practical gain is a clearer separation of constraints. A capture list permits a hidden type to use parameters without inventing outlives relationships. A raw-pointer expression avoids creating a reference with stronger requirements. External declaration syntax identifies who must justify the interface. Those distinctions let the source communicate the actual operation and the corresponding proof of correctness.