Skip to main content

otter/
alloc.rs

1//! An opt-in `#[global_allocator]` backed by the BEAM allocator.
2//!
3//! [`EnifAlloc`] routes every Rust heap allocation in the NIF library through
4//! `enif_alloc`/`enif_free`. This matters for hot code upgrade: `enif_free` is
5//! the one free path that is valid across two independently compiled builds of
6//! the library (a stable C ABI), whereas Rust's default allocator is
7//! build-private. Routing allocations through the VM allocator is therefore a
8//! building block for carrying state across the upgrade boundary safely (see
9//! `docs/UPGRADE.md`).
10//!
11//! ## Why direct-linked, not `dlsym`
12//!
13//! The rest of otter resolves `enif_*` symbols at run time with `dlsym` (via
14//! the `enif_ffi` crate). A global allocator cannot: it may be called for the very
15//! first Rust allocation, before any initialization code runs, so it must not
16//! depend on a resolution step that itself allocates. Instead this module
17//! **direct-links** the two functions it needs as `extern "C"`. The BEAM
18//! exports them, and the dynamic linker binds them when the NIF `.so` is
19//! `dlopen`'d (`RTLD_NOW`) — before `nif_init`, before any Rust code. There is
20//! no catch-22.
21//!
22//! ## Inert until you opt in
23//!
24//! [`EnifAlloc`] is always available, but it is referenced — and therefore
25//! pulls in the direct-linked `enif_alloc`/`enif_free` symbols — only when you
26//! install it as the global allocator with [`enif_global_allocator!`]. Until
27//! then dead-code elimination drops it, so otter still links into ordinary,
28//! non-BEAM binaries (its own `cargo test`, doc builds, etc.).
29//!
30//! Once you *do* install it, those two symbols are undefined in the object file
31//! and resolved only when the BEAM loads the `.so` — so a crate that invokes
32//! the macro links **only** as a NIF cdylib hosted by the BEAM, never as an
33//! ordinary executable.
34//!
35//! [`enif_global_allocator!`]: crate::enif_global_allocator
36
37use std::alloc::{GlobalAlloc, Layout};
38use std::ffi::c_void;
39
40// Direct-linked, not resolved via `dlsym` — see module docs.
41unsafe extern "C" {
42    fn enif_alloc(size: usize) -> *mut c_void;
43    fn enif_free(ptr: *mut c_void);
44}
45
46/// One machine word, used to stash the allocation's base pointer just below
47/// the aligned pointer handed to the caller.
48const HEADER: usize = std::mem::size_of::<*mut u8>();
49
50/// A [`GlobalAlloc`] backed by the BEAM allocator (`enif_alloc`/`enif_free`).
51///
52/// Install it in the final NIF cdylib with [`enif_global_allocator!`], or by
53/// hand: `#[global_allocator] static A: EnifAlloc = EnifAlloc;`.
54///
55/// [`enif_global_allocator!`]: crate::enif_global_allocator
56pub struct EnifAlloc;
57
58// `enif_alloc`, like `malloc`, takes no alignment argument and guarantees only
59// max-fundamental alignment (8 bytes on Linux x86-64). To honor an arbitrary
60// `Layout::align`, we over-allocate and stash the original base pointer in the
61// machine word just below the aligned pointer we hand out, then recover it in
62// `dealloc` — the pointer passed to `enif_free` must be exactly the one
63// `enif_alloc` returned.
64unsafe impl GlobalAlloc for EnifAlloc {
65    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
66        let align = layout.align();
67        // One word for the base header, plus slack to bump up to an aligned
68        // boundary. Reject a size that would overflow `usize` rather than wrap.
69        let total = match layout
70            .size()
71            .checked_add(align)
72            .and_then(|n| n.checked_add(HEADER))
73        {
74            Some(n) => n,
75            None => return std::ptr::null_mut(),
76        };
77        // SAFETY: `enif_alloc` is a BEAM-exported C function, bound by the
78        // dynamic linker at load time.
79        let base = unsafe { enif_alloc(total) } as *mut u8;
80        if base.is_null() {
81            return std::ptr::null_mut();
82        }
83        // First address at least HEADER past `base` and aligned to `align`
84        // (align is always a power of two, per `Layout`).
85        let aligned = (base as usize + HEADER + align - 1) & !(align - 1);
86        let user = aligned as *mut u8;
87        // Stash `base` in the word immediately below `user`.
88        // SAFETY: `user - HEADER >= base` by construction, so this write lands
89        // inside the block we just allocated.
90        unsafe { user.cast::<*mut u8>().sub(1).write(base) };
91        user
92    }
93
94    unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
95        // SAFETY: `ptr` came from `alloc`, which stored the base one word below it.
96        let base = unsafe { ptr.cast::<*mut u8>().sub(1).read() };
97        // SAFETY: `base` is the exact pointer `enif_alloc` returned.
98        unsafe { enif_free(base as *mut c_void) };
99    }
100
101    // `realloc` is intentionally left as the default (alloc + copy + dealloc) so
102    // every reallocation stays on the enif path and through the header scheme;
103    // `enif_realloc` cannot be used here because it does not preserve the
104    // base-relative offset the header scheme depends on.
105}
106
107/// Install [`EnifAlloc`] as the `#[global_allocator]` of the current crate.
108///
109/// Invoke this once in your NIF cdylib (where a `#[global_allocator]` is legal):
110///
111/// ```ignore
112/// otter::enif_global_allocator!();
113/// ```
114///
115/// Invoking this makes the crate link only as a BEAM-hosted cdylib (see the
116/// [`alloc`](crate::alloc) module docs).
117#[macro_export]
118macro_rules! enif_global_allocator {
119    () => {
120        #[global_allocator]
121        static __OTTER_ENIF_GLOBAL_ALLOC: $crate::alloc::EnifAlloc = $crate::alloc::EnifAlloc;
122    };
123}