static-generics 0.1.3

Zero-cost generic statics for Rust.
Documentation

static-generics

docs.rs License: MIT OR Apache-2.0

Zero-cost generic statics for Rust.

Based on the ideas of cynecx/generic-statics, and implemented to be zero-cost*: one–two instructions to access a generic (lea, adrp+add, auipc+addi, larl, …) instead of a call to the accessor function.

*Zero cost applies only to supported platforms, unsupported platforms go through fallback of HashMap + Mutex and require std feature to be enabled.

use static_generics::{define_namespace, namespace::NamespaceExt};
use core::sync::atomic::{AtomicU64, Ordering};

define_namespace!(Counters);

let hits = Counters::get::<AtomicU64>();
hits.fetch_add(1, Ordering::Relaxed);

Why

Rust has no generic statics unlike C++, and default solution to that is using typemap/HashMap<TypeId, ...> which require locking, allocation, hashing on each access which is expensive and slow. static-generics implements zero-cost solution which allows to implement generic statics in ~1-2 instructions on supported platforms (and falls back to hashmap on unsupported).

Requirements and limits

  • Only bytemuck::Zeroable types can be stored in the static generic (use OnceSlot to lazily initialize any other types in static generic).
  • No Drop support, all static generics are essentially leaked. If you need to clean-up memory after them, it has to be done manually.
  • Within a single crate the same (Namespace, T) is always the same address. Across crates or dynamic libraries sharing is not guaranteed: depending on optimization and linking, another crate may alias your slot or keep its own copy. Dynamic library loading in particular is not guaranteed to return the same address for generic_static::<i32>() done in libA.so and libB.so.

Implementation

We use inline assembly + monomorphization of rustc to emit "unique" function body per each T. In the assembly code block we define storage with weak + COMDAT linkage in a per-symbol .bss section with the name of unique function

  • gen_static_ prefix, and after that we compute the PC-relative address using lea on x86, adrp on arm, and so on.

Since the storage symbol is linkonce, the linker keeps a single copy per binary, so inlined generic_static calls stay at 1 or 2 instructions with no runtime overhead.

Platform support

  • x86_64
  • aarch64
  • x86_32
  • arm (Thumb as well)
  • riscv32/riscv64
  • loongarc32/loongarch64
  • powerpc/powerpc64
  • s390x
  • wasm32: ONLY with nightly feature enabled, stable Rust does not support asm!() on WASM.

On other platforms (or with Miri/Cranelift detected) the crate will fallback to slow implementation via HashMap and Mutex.