March 21, 2024·6 min read·engineeringrustffilayoutasync

Rust 1.77 makes C strings and field offsets explicit

C-string literals and offset_of! remove delicate boilerplate around foreign interfaces and memory layout. Indirect async recursion addresses a separate representation constraint in generated futures.

Some low-level operations are awkward because the programmer must express a fact through operations that have additional safety requirements. Computing a field offset without a value can lead to delicate pointer code. Constructing a constant C string can require validation that a compiler could perform itself. Rust 1.77, released on March 21, gives these operations direct forms through offset_of! and C-string literals.

The release also permits recursive async functions when indirection keeps their futures finite in size. These additions address different problems, but each clarifies a representation requirement that was previously difficult to express. None removes the need to understand the foreign interface or layout contract surrounding the operation.

I find Rust compelling for systems work because it makes those contracts visible in the language. The value of this release is that a programmer can express some necessary facts without constructing an unnecessarily fragile route to them. It reduces boilerplate at exactly the places where incorrect boilerplate is expensive.

A C string has a different termination contract

The new literal c"abc" produces a reference of type &'static CStr to bytes terminated by a nul byte. The compiler checks the literal, including rejecting interior nul bytes. This suits foreign interfaces expecting a nul-terminated string because the termination condition is established before the program runs.

The release-tag CStr source documents the relevant error cases for byte-based construction. A C string needs its nul in the expected terminal position. An interior nul means a consumer using C termination rules can see a shorter string than the supplied byte sequence suggests.

The literal therefore communicates more than a convenient spelling of bytes. It establishes the invariant associated with the borrowed C-string type. The resulting static reference also avoids asking the caller to create an owned string merely to pass a constant name to another API.

That does not make a foreign call safe by itself. The called function still determines what it does with the pointer, whether it retains it, and whether other arguments must satisfy additional conditions. The literal handles the constant string's construction. It does not replace the function's full contract.

For an existing API taking a borrowed C string, the argument can now be written directly:

c"backend"

This is particularly useful when the data is a fixed interface name or option key. Dynamic input still needs a construction path that checks its bytes. Compile-time validation can establish properties of literal content because that content is already available to the compiler.

Offsets are facts about a specific type layout

std::mem::offset_of! expands to the offset in bytes of a field from the beginning of its containing type. The release announcement emphasizes that it works without an existing instance. Before this addition, implementing that operation on stable Rust could require unsafe code whose correctness was easy to get wrong.

The 1.77 macro documentation states that visibility still applies. A caller cannot use the macro to bypass a private field. The type and selected field must be visible at the call site.

The stable form in this release supports a single field name. The source documentation also discusses richer forms behind feature gates, including nested fields and enum access. Those experimental forms should not be confused with the interface being stabilized. The immediate stable operation is enough for many cases where a known struct field's displacement is needed.

The result is a usize expression. A caller can obtain an offset from the type and field names without first manufacturing a value. That is important when constructing a valid instance would itself require resources or invariants unrelated to the layout question.

The query also remains attached to the field name in source. It does not duplicate a numeric displacement that a maintainer must update after changing a declaration. The compiler resolves the selected field and computes its offset using that build's layout.

The macro also avoids confusing address calculation with reading a field. A program seeking only an offset should not have to create references through invalid or uninitialized storage. Providing the operation directly lets the compiler answer the layout question without asking application code to simulate it with pointer operations.

Discovering layout does not freeze it

The documentation explicitly warns that layouts are generally platform-specific and can change between compilations. A field's offset remains fixed within a program execution, but another compilation of the same source may choose a different layout. offset_of! reports the compiler's answer for the type in that build.

This distinction matters for persistent data or foreign interfaces. A program can compute an offset correctly and still lack a promise that a different program interprets the same bytes identically. A layout requirement needs an appropriate representation contract in addition to a way to query it.

The documentation points to explicit representation attributes when stability is required. Its repr(C) example shows how alignment can insert padding between fields. The offset is therefore not generally the sum of preceding field sizes. The layout rules and each field's alignment determine where storage begins.

It also warns against assuming similar generic types have identical layouts. Replacing a type argument with another type of the same size does not necessarily preserve every offset. A wrapper with a transparent representation likewise does not establish identical layout for every surrounding generic aggregate.

The macro answers a precise question about the selected type. An application should use that precision when composing a low-level interface. Inferring a broader relationship between two types would require an additional guarantee not supplied by the offset query itself.

I consider that separation useful. It gives low-level code a safe query while keeping the layout policy explicit. A convenience macro should not silently turn Rust's implementation freedom into a cross-build data-format promise.

Async recursion needs finite representation

Rust 1.77 also removes a compiler limitation that prevented async functions from calling themselves even when the call used suitable indirection. The release permits recursive calls provided indirection prevents an infinitely sized function state.

The reason concerns representation of the future. A recursive async call introduces another future of the same function into the state needed while awaiting it. Storing that state directly would recursively require space for itself. An indirection gives the containing state a finite-size reference to the recursive future.

The release announcement demonstrates the pattern with Box::pin around each recursive call before awaiting it. The important part is the indirection. Stabilizing support does not mean a directly self-containing future suddenly has finite size, and it does not mean recursive work acquires an automatic scheduling strategy.

The example is a demonstration of accepted construction rather than a recommendation for a particular algorithm. Recursive Fibonacci is useful for showing two recursive awaits, but the language change concerns any recursive async operation whose state is made finite in this way. The compiler can now accept the representation the programmer has explicitly provided.

This is different from the two foreign-interface additions. C-string literals validate a byte invariant, and offset_of! queries a layout fact. Indirect async recursion makes a previously rejected finite representation usable. Grouping them as general performance improvements would obscure what each actually changes.

Release profiles also change debug information handling

Cargo's profile behavior in this release has another representation-related consequence. Profiles that do not enable debug information now enable stripping of debug information by default. The announcement explains why disabling local debug information was insufficient to produce that result previously.

The precompiled standard library contains debug information. A statically linked result could therefore include standard-library debug information even when local compilation did not request it. Stripping removes that retained information from the output rather than changing only how the application's own units are compiled.

Users who need debug information can explicitly enable it with the profile's debug setting. The distinction is between producing debug information during compilation and removing it from the final artifact. Those operations occur at different stages, which explains why the older setting could leave information the caller thought it had excluded.

The release gives systems programmers several more direct operations. Constant C strings carry a checked termination invariant. Field offsets can be queried without an instance while leaving representation requirements explicit. Recursive async calls can use indirection to obtain finite state. Each removes a specific obstacle without weakening the surrounding type, layout, or foreign-call contract.