unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
rust-lang/rust
senior7/10
api-designcompiler-internalsmemory-management

atomic volatile: add intrinsics

Project: rust-lang/rust | Language: Rust | Concepts: api-design, compiler-internals, memory-management | Difficulty: senior
Link: https://github.com/rust-lang/rust/pull/160058

The problem

Rust's atomic load and store intrinsics (atomic_load, atomic_store) did not support a volatile flag, which is sometimes required for correct code generation in low-level systems programming. Some hardware and use-cases require volatile atomic operations to prevent the compiler from reordering or optimizing away memory accesses.

Why it's non-trivial

Simply adding a volatile flag to the API is not enough. The change must:

  • Propagate the new flag through all layers: language intrinsics, standard library wrappers, codegen backends (LLVM, GCC, Cranelift), and the interpreter (Miri/CTFE).
  • Maintain backward compatibility and correct type-checking.
  • Handle codegen backends that do not yet implement the volatile semantics, ideally without breaking them.
  • Update all call sites to use the extended API, including those in macros and generic code.

The approach

1. Extend the intrinsic signatures

The core atomic intrinsics are updated to take an additional const VOLATILE: bool generic parameter:

pub const unsafe fn atomic_load<T: Copy, const ORD: AtomicOrdering, const VOLATILE: bool>(
    src: *const T,
) -> T;

pub const unsafe fn atomic_store<T: Copy, const ORD: AtomicOrdering, const VOLATILE: bool>(
    dst: *mut T,
    val: T,
);

This change makes the volatile property explicit and available at compile time.

2. Propagate the volatile flag through the standard library

All standard library atomic types are updated to use the new intrinsics with VOLATILE set to false in their safe wrappers:

unsafe {
    atomic_load::<_, /* VOLATILE */ false>(self.as_ptr(), order)
}

This ensures existing atomic operations remain non-volatile by default.

3. Update the trait for codegen backends

The BuilderMethods trait, which abstracts over codegen backends, is updated to accept a volatile: bool parameter for atomic load/store methods:

fn atomic_load(
    &mut self,
    ty: Self::Type,
    ptr: Self::Value,
    order: AtomicOrdering,
    volatile: bool,
    size: Size,
) -> Self::Value;

fn atomic_store(
    &mut self,
    val: Self::Value,
    ptr: Self::Value,
    order: AtomicOrdering,
    volatile: bool,
    size: Size,
);

Backends now have the information needed to generate volatile atomic instructions.

4. Implement volatile support in LLVM backend

The LLVM codegen sets the volatile property on load/store instructions when requested:

let load = llvm::LLVMBuildLoad2(self.llbuilder, ty, ptr, UNNAMED);
// Set atomic ordering
llvm::LLVMSetOrdering(load, AtomicOrdering::from_generic(order));
if volatile {
    llvm::LLVMSetVolatile(load, llvm::TRUE);
}

This ensures that volatile atomic semantics are respected during code generation.

5. Update the interpreter and other backends

The interpreter (Miri/CTFE) and other backends (GCC, Cranelift) are updated to accept the new flag, even if they do not yet implement special handling:

let _volatile = generic_args.const_at(2).to_value(); // makes no difference for us

This maintains compatibility and leaves a clear marker for future implementation.

6. Update type checking for new intrinsic signature

The type checker is updated to expect the new const parameter count for the atomic intrinsics:

sym::atomic_load => (1, 2, vec![Ty::new_imm_ptr(tcx, param(0))], param(0)),
sym::atomic_store => (1, 2, vec![Ty::new_mut_ptr(tcx, param(0)), param(0)], tcx.types.unit),

This ensures that calls to the intrinsics have the correct number of arguments.

From the review thread

No notable review discussion on this PR.

Takeaways

  • API changes to low-level intrinsics must be propagated through all layers of the stack, including wrappers, codegen, and interpreters.
  • When extending APIs, use explicit and type-safe parameters (such as const generics) to avoid ambiguity and encourage correct usage.
  • Backwards compatibility and clear migration paths are critical when changing widely-used primitives.
  • Even if some platforms or backends do not support a new feature, the interface should be updated everywhere to maintain consistency and enable future support.