July 13, 2023·5 min read·engineeringrustffiunwindingabi

Rust 1.71 makes unwinding across foreign calls an explicit ABI choice

The C-unwind ABI defines a supported path for ordinary exceptions to cross language boundaries. Rust 1.71 stabilizes that path without yet changing the behavior of existing C ABI boundaries.

Sharing a calling convention does not automatically make exception propagation safe. A Rust function and a foreign function can agree on argument layout while the compiler still assumes that no unwind crosses their boundary. Rust 1.71, released July 13, stabilizes C-unwind to make that additional part of the contract explicit.

The release distinguishes the new ABI from ordinary C. For this initial stabilization, unwinding across existing non-unwind ABI boundaries remains undefined behavior. The announcement describes changing that behavior in a future release as a remaining step. Treating today's stabilization as if ordinary C boundaries already abort would give users the wrong contract for Rust 1.71.

I find this kind of explicit boundary one of Rust's useful qualities for systems integration. Foreign code brings obligations that the compiler cannot infer from a function name. An ABI declaration gives the caller and implementation a place to state whether unwinding is part of the supported interaction.

Native mechanism compatibility is not sufficient

RFC 2945 describes existing projects that need cross-language unwinding, including WebAssembly runtimes and wrappers around C libraries. Some had relied on compatibility between Rust's panic propagation and native exception machinery. The RFC explains why that practical compatibility did not establish defined Rust behavior.

The compiler assumes that an ordinary extern "C" function cannot unwind. LLVM can optimize on the basis of that assumption. Even if the underlying stack unwinder could traverse the frames, the generated program may no longer preserve behavior needed for that traversal. A mechanism that appears to work in one build is not the same as a supported language contract.

C-unwind states the additional permission. The release says each unwind-suffixed ABI is mostly equivalent to its corresponding ordinary ABI, with defined behavior when an ordinary unwinding operation crosses it. This applies to Rust panics and C++-style exceptions within the documented conditions.

The distinction belongs in the type of a foreign function as well as its implementation. A caller needs to know whether invoking a function can unwind. The RFC therefore favors an ABI string over an attribute scheme that cannot express the same distinction for function pointers in the relevant design.

An exception can traverse another language's frames

With panic=unwind, the new ABI allows an exception originating in one language to pass through frames in another and return to its originating language for handling. The release announcement explicitly qualifies the supported case by requiring that the exception be caught in the language where it originated.

For example, a C++ caller can invoke Rust, which invokes another C++ function. An exception from the inner C++ function can traverse the intervening Rust frames and reach C++ handling outside them when the relevant boundaries use the unwind ABI. The reverse arrangement permits a Rust panic to cross compatible foreign frames and return to Rust.

This is stack traversal, not automatic translation between error systems. The ABI does not make a C++ exception into a Rust error value. It establishes a supported path through frames whose cleanup and unwinding behavior need to remain valid. The place where the exception is ultimately handled still matters.

The RFC leaves the interaction between a foreign exception and Rust's catch_unwind outside the defined scope. That is another reason not to describe the feature as universal interoperability between exception handlers. Passing through frames and being caught by a handler are different operations with different contracts.

Abort mode preserves a defined failure path

The design also considers code built with panic=abort. A Rust panic in that configuration aborts instead of initiating unwinding. For an ordinary foreign exception entering Rust through a C-unwind boundary, the RFC specifies aborting the process.

One stated design goal is that switching from unwind mode to abort mode must not introduce undefined behavior into code that uses the supported ordinary-unwind path. The behavior can change to termination, but the ABI still provides a defined outcome. That is an important distinction for libraries consumed by applications with different panic policies.

I prefer a declaration that preserves this distinction because build settings are part of a real integration. A library cannot assume that every downstream executable uses the panic strategy its author happened to use locally. The contract needs to say what happens under both strategies.

The separate ABI also retains optimization opportunities for calls that do not permit unwinding. The RFC discusses avoiding unnecessary unwinding support and preserving flexibility in how Rust implements panics. Making the permission explicit lets the compiler distinguish the boundaries that require the extra behavior.

Forced unwinding remains a separate problem

The RFC distinguishes ordinary exceptions from forced unwinding used by mechanisms such as longjmp on Windows and pthread_exit in glibc. Those mechanisms are not all equivalent across platforms. A supported ordinary exception crossing does not establish that every stack-deallocation mechanism is safe.

Pending destructors are central to that distinction. A frame with values requiring cleanup cannot simply disappear without considering their obligations. The RFC introduces plain old frames for frames with no pending destructors or relevant catch operations, but still does not define a generally safe forced-unwind path merely because a frame meets that condition.

This keeps the stabilization's scope understandable. C-unwind solves a specific ordinary-unwinding boundary problem. It does not remove the need to understand foreign control-flow mechanisms with different cleanup semantics.

Foreign-library integration also gains a Windows linking option

Rust 1.71 separately adds raw-dylib linking on Windows. The release announcement says it permits using functions from a dynamic library without requiring that library to be present at build time. This can reduce the need for crates to ship stub libraries, particularly in cross-compilation workflows.

The release also supports binding to DLL exports by ordinal through link_ordinal. These are linking changes, distinct from unwinding permission. An application may need both a suitable way to resolve a foreign function and an accurate declaration of how control can leave that function.

The practical value of C-unwind is that the second obligation now has a stable expression. Interfaces that intentionally allow ordinary exceptions to cross can declare it, while existing C boundaries retain their current Rust 1.71 behavior. The implementation and declaration can then agree on a property that argument layout alone never established.