Skip to main content

win_numa_sys/
buffer.rs

1// Copyright (c) 2026 Mike Grier
2// Moved from windows-ioring-sys/src/numa_buffer.rs at 6101ca65, which had in
3// turn moved it from examples/ring_copy/buffer.rs at 834c7afa.
4//! A `VirtualAllocExNuma`-backed buffer, so a buffer can be placed on a chosen
5//! NUMA node rather than wherever the default allocator's own heuristics land
6//! it.
7//!
8//! # Why this is its own crate
9//!
10//! It was in `windows-ioring-sys`, which recommends placing a registered pool
11//! near the device and so had a reason to provide the allocator rather than
12//! leave every caller to write it. But the buffer has nothing to do with a
13//! ring: its only connection was one doc line saying it *can* be registered
14//! into one. Meanwhile `windows-placement-probe` -- which does not depend on
15//! the ring crate -- had written the same `VirtualAllocExNuma` call for itself,
16//! making two independent allocators in a workspace that had already hoisted
17//! this code once to stop exactly that.
18//!
19//! # What this does not decide
20//!
21//! **Which node.** A caller who knows their storage or thread layout passes
22//! `Some(node)`; one who does not passes `None` and gets the system's own
23//! choice, which is what the default allocator would have given anyway.
24//! [`volume_numa_node`](crate::volume_numa_node) is available for finding a
25//! candidate, and says plainly what its answer is and is not worth.
26//!
27//! A node argument is a *preference*, which the underlying parameter says in
28//! its own name (`nndPreferred`). So a successful allocation is not by itself
29//! evidence that the pages landed on the node that was asked for, and code
30//! that needs to know must measure rather than assume -- see the crate root.
31
32use std::io;
33use std::ptr;
34
35use windows_sys::Win32::System::Memory::{
36    MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE, VirtualAllocExNuma, VirtualFree,
37};
38use windows_sys::Win32::System::Threading::GetCurrentProcess;
39
40use crate::NumaNode;
41
42/// `VirtualAllocExNuma`'s documented sentinel for "no NUMA preference" --
43/// windows-sys does not name this constant, so it is named here rather than
44/// written as a bare literal at the call site.
45const NUMA_NO_PREFERRED_NODE: u32 = u32::MAX;
46
47/// An owned buffer allocated with `VirtualAllocExNuma`, freed with
48/// `VirtualFree` on drop.
49///
50/// The allocation is page-granular: `VirtualAllocExNuma` rounds a request up
51/// to the system page size, so a caller asking for a small buffer gets at
52/// least a page. [`NumaBuffer::len`] reports the length that was *requested*,
53/// which is the length a kernel call should be told about.
54///
55/// # Buffer traits
56///
57/// This type implements `io-buffers`' `IoBuf` and `IoBufMut`, so a ring takes it
58/// as a buffer. This crate defines no trait of its own: the traits belong to
59/// `io-buffers`, which is portable and so cannot depend on this allocator, and
60/// the orphan rule lets only the crate that owns the trait or the type
61/// implement it. A crate that owns a buffer trait of its own implements it for
62/// this type the same way, over [`NumaBuffer::as_ptr`],
63/// [`NumaBuffer::as_mut_ptr`] and [`NumaBuffer::len`].
64pub struct NumaBuffer {
65    ptr: *mut u8,
66    len: usize,
67}
68
69// SAFETY: the allocation is exclusively owned by this value; sending it
70// across threads only moves that ownership, never aliases it.
71unsafe impl Send for NumaBuffer {}
72
73impl std::fmt::Debug for NumaBuffer {
74    /// Shows the address and requested length, never the contents: a
75    /// registered buffer routinely holds someone's data, and a `Debug` that
76    /// printed it would put that data anywhere a caller logs.
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.debug_struct("NumaBuffer")
79            .field("ptr", &self.ptr)
80            .field("len", &self.len)
81            .finish()
82    }
83}
84
85impl NumaBuffer {
86    /// Allocate `len` bytes, preferring `node` if given.
87    ///
88    /// Passing `None` requests no NUMA preference, which is the same choice
89    /// the default allocator makes implicitly.
90    ///
91    /// # What a successful return does and does not mean
92    ///
93    /// A valid node is a **preference**, not an instruction -- `nndPreferred`
94    /// says so in its name -- so success means the request was accepted and
95    /// memory was obtained, never that the pages came from the node asked for.
96    ///
97    /// An *invalid* node is a different matter and is genuinely refused: this
98    /// crate's tests assert that `u32::MAX - 1` fails. Note that `u32::MAX`
99    /// itself is **not** a test of that, because it is the API's own
100    /// no-preference sentinel and is accepted by design -- a measurement that
101    /// reads it as an out-of-range node being tolerated has measured the
102    /// sentinel instead.
103    ///
104    /// # Errors
105    ///
106    /// The error from `VirtualAllocExNuma`, which includes a `len` of zero and
107    /// a node number the machine does not have.
108    pub fn new(len: usize, node: Option<NumaNode>) -> io::Result<Self> {
109        // SAFETY: no pointer arguments; the returned value is a pseudo-handle
110        // that needs no closing.
111        let process = unsafe { GetCurrentProcess() };
112        // SAFETY: `process` is a valid pseudo-handle for the duration of this
113        // call; a null `lpAddress` lets the system choose the address.
114        let ptr = unsafe {
115            VirtualAllocExNuma(
116                process,
117                ptr::null(),
118                len,
119                MEM_COMMIT | MEM_RESERVE,
120                PAGE_READWRITE,
121                node.map_or(NUMA_NO_PREFERRED_NODE, NumaNode::get),
122            )
123        };
124        if ptr.is_null() {
125            return Err(io::Error::last_os_error());
126        }
127        Ok(Self {
128            ptr: ptr.cast(),
129            len,
130        })
131    }
132
133    /// The allocation's base address, fixed for this value's life.
134    #[must_use]
135    pub fn as_ptr(&self) -> *const u8 {
136        self.ptr
137    }
138
139    /// The allocation's base address for writing.
140    ///
141    /// Takes `&mut self` because this value uniquely owns the allocation, so
142    /// exclusive access to the value is exclusive access to the bytes.
143    #[must_use]
144    pub fn as_mut_ptr(&mut self) -> *mut u8 {
145        self.ptr
146    }
147
148    /// The length that was requested.
149    ///
150    /// Not the length that was reserved: `VirtualAllocExNuma` rounds up to a
151    /// page, so the mapping is at least this large and usually larger. This is
152    /// the number a kernel call should be told, because it is the number the
153    /// caller asked to use.
154    #[must_use]
155    pub fn len(&self) -> usize {
156        self.len
157    }
158
159    /// Whether the requested length was zero.
160    ///
161    /// Present because clippy asks for it beside [`Self::len`], and because a
162    /// zero-length request is rejected by `VirtualAllocExNuma` rather than
163    /// producing an empty buffer -- so on any value that exists, this is false.
164    #[must_use]
165    pub fn is_empty(&self) -> bool {
166        self.len == 0
167    }
168}
169
170impl Drop for NumaBuffer {
171    fn drop(&mut self) {
172        // SAFETY: `self.ptr` was returned by `VirtualAllocExNuma` above and
173        // is freed exactly once, here.
174        unsafe {
175            VirtualFree(self.ptr.cast(), 0, MEM_RELEASE);
176        }
177    }
178}
179
180#[cfg(test)]
181mod tests;