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;