July 3, 2025·4 min read·engineeringrustassemblyabisafety

Rust naked functions give assembly a typed declaration

The July explanation of naked functions connects their Rust signatures with complete handwritten bodies. The design avoids backend-specific instruction insertion while retaining symbol handling and documentation in Rust.

Defining a function in global assembly duplicates information that a Rust declaration normally owns. The assembly needs object-format directives and a symbol name, while an external declaration separately tells Rust how to call it. If those descriptions diverge, a plausible signature can conceal an incorrect calling convention. Naked functions put the declaration and complete assembly body together.

Folkert de Vries's July 3 explanation describes the feature stabilized in Rust 1.88. A function uses #[unsafe(naked)], and its body contains one naked_asm! invocation. The compiler does not insert ordinary argument handling or a prologue and epilogue around the body.

I find this useful as a systems-language boundary. Rust can describe the interface, while assembly can implement an entry sequence the ordinary compiler should not alter. That separation is most useful when the declaration makes the calling obligations easier to inspect.

The complete body belongs to the author

RFC 2972 motivated a constrained definition of naked functions. Merely saying that a function lacks a prologue leaves difficult questions about local variables, stack access, and how arguments enter the assembly. A defined calling convention and a body restricted to assembly give those questions a more precise answer.

The stabilized syntax differs from the RFC's older spelling. The dedicated naked_asm! macro replaces the earlier approach of adding restrictions to an ordinary asm! call. That distinction matters when copying examples, because a design document records the reasoning but does not necessarily show the final stable interface.

For the x86-64 System V convention, a small wrapping-add function can implement its whole operation directly:

#[cfg(target_arch = "x86_64")]
// SAFETY: The body implements the declared System V argument and return convention.
#[unsafe(naked)]
pub extern "sysv64" fn add_wrapping(a: u64, b: u64) -> u64 {
    core::arch::naked_asm!(
        "lea rax, [rdi + rsi]",
        "ret"
    );
}

The architecture guard matters because these instructions and registers are not portable Rust. Its declaration selects sysv64 explicitly. The assembly must implement that convention, including where arguments arrive and how the result returns. Rust does not read the parameter names and automatically bind them as ordinary assembly operands.

Unsafe implementation does not require an unsafe caller

The unsafe attribute identifies the author's obligation to keep signature, convention, and instructions consistent. A correctly implemented function can still have a safe calling interface, as the July article's wrapping-add example demonstrates. The implementation technique and the caller's preconditions are different properties.

Marking the function unsafe would add a caller obligation, but it would not repair an incorrect convention by itself. A function with extra requirements needs those requirements documented. A function claiming an ordinary convention must still establish how the body relates to its declaration.

This distinction helps avoid an indiscriminate view of unsafe code. The relevant question is which guarantee the surrounding compiler cannot establish and who must supply it. Here, the author supplies the connection between the machine instructions and the Rust-level interface.

Lowering avoids an unspecified backend behavior

The July explanation describes an important implementation change. An earlier version relied on LLVM's handling of naked functions, which could add unexpected instructions. Rust also has other code-generation backends, so matching unspecified LLVM behavior would be a poor contract for the feature.

The stabilized implementation lowers the function to global assembly instead. Backends already support emitting global assembly, and the function body consists of the instructions the author supplied. This directly addresses the requirement that ordinary function generation must not interfere with the handwritten entry and exit sequence.

Object-format directives still require handling. A naked function lets Rust emit the appropriate directives and apply its symbol naming. By comparison, a globally named assembly function can require platform-specific symbol spelling and a matching declaration elsewhere. Keeping the function within Rust's naming system also reduces the need for a globally visible unmangled name solely to connect those pieces.

The July article also discusses future assembly work, including custom calling conventions. Those examples use unstable feature gates and should not be confused with the stabilized naked-function mechanism. A stable body still needs a calling convention the declaration can describe. A custom machine-level entry contract raises the separate question of whether Rust can generate an ordinary call to it.

Generics and documentation stay with the interface

The article identifies const generics as another advantage over a free-standing assembly definition. A Rust function declaration can participate in generic instantiation while retaining the handwritten body. That is useful where a constant needs to affect low-level code without generating separate external assembly definitions manually.

Documentation and attributes also have one declaration to describe. This does not prove the implementation correct, but it reduces duplicated places that can become inconsistent. A safety explanation belongs beside the signature whose convention it justifies.

I prefer that arrangement because interface drift is a concrete maintenance problem. A future reader should be able to find the declared calling convention and its implementation together. Naked functions retain that relationship while assigning complete instruction generation to the author. The compiler supplies declaration handling, and the assembly remains responsible for the machine-level behavior promised by that declaration.