Skip to main content

win_numa_sys/
node.rs

1// Copyright (c) 2026 Mike Grier
2//! [`NumaNode`], and the two questions Windows will answer about nodes.
3
4use std::io;
5use std::os::windows::io::RawHandle;
6
7use windows_sys::Win32::Foundation::HANDLE;
8use windows_sys::Win32::System::IO::DeviceIoControl;
9use windows_sys::Win32::System::Ioctl::FSCTL_QUERY_VOLUME_NUMA_INFO;
10use windows_sys::Win32::System::Threading::GetNumaHighestNodeNumber;
11
12/// A NUMA node number, as Windows numbers them.
13///
14/// A newtype rather than a bare `u32` because the two `u32`s in this area mean
15/// different things and are trivially swapped at a call site: a node *number*
16/// and a *count* of nodes are both small integers, and
17/// [`highest_numa_node`] returns the former while reading like the latter.
18///
19/// **Node numbers are not an index.** Windows does not promise they run
20/// `0..n`, so a caller deriving a node from a position in some list is making
21/// an assumption the platform never offered -- see `windows-placement-probe`,
22/// where a positional index reaching `VirtualAllocExNuma` was a real defect and
23/// is called out at the site that fixed it.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub struct NumaNode(u32);
26
27impl NumaNode {
28    /// Wrap a raw node number.
29    ///
30    /// Not validated against the machine: `VirtualAllocExNuma` is where an
31    /// impossible node is rejected, and validating here would mean a second,
32    /// weaker opinion about what exists. See [`NumaBuffer::new`] for what the
33    /// platform actually does with an out-of-range node, which is not what its
34    /// documentation says.
35    ///
36    /// [`NumaBuffer::new`]: crate::NumaBuffer::new
37    #[must_use]
38    pub const fn new(node: u32) -> Self {
39        Self(node)
40    }
41
42    /// The raw node number, for handing to a Win32 call.
43    #[must_use]
44    pub const fn get(self) -> u32 {
45        self.0
46    }
47}
48
49impl std::fmt::Display for NumaNode {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "node {}", self.0)
52    }
53}
54
55/// The highest node number this machine reports, if it answers.
56///
57/// `GetNumaHighestNodeNumber`. The value is a *node number*, not a count: a
58/// machine with one node answers `NumaNode(0)`, not one.
59///
60/// # Why a caller wants this
61///
62/// Mostly to qualify a report rather than to drive a choice. On a machine that
63/// answers `NumaNode(0)` there is exactly one node, so every placement decision
64/// is the same decision, and a line saying "placed on node 0" is true and
65/// misleading. Asking this is how a caller can say which of those it is.
66///
67/// # Errors
68///
69/// `None` when the call fails, which is reported as "unknown" rather than as a
70/// node count of zero -- a machine that will not say how many nodes it has is a
71/// different thing from a machine with none.
72#[must_use]
73pub fn highest_numa_node() -> Option<NumaNode> {
74    let mut highest: u32 = 0;
75    // SAFETY: the out parameter is a live local for the call's duration.
76    let ok = unsafe { GetNumaHighestNodeNumber(&raw mut highest) };
77    (ok != 0).then_some(NumaNode::new(highest))
78}
79
80/// The NUMA node `handle`'s **volume** reports, if it reports one.
81///
82/// `FSCTL_QUERY_VOLUME_NUMA_INFO`, issued against the handle directly -- the
83/// documented control code accepts a file or directory handle, so this needs no
84/// device-tree walk and no second open.
85///
86/// # This answers a question about a volume, not about a file
87///
88/// The documented meaning is the node the *volume* resides on. A volume may
89/// span several devices -- an ordinary spanned volume or a Storage Spaces set
90/// does -- and then a single reported node is a fiction rather than an answer,
91/// because the file's extents may live anywhere across the set. **It therefore
92/// cannot tell a caller which node a particular file's I/O is closest to**, even
93/// when it succeeds.
94///
95/// That is a limit of the question, not of this wrapper, and it is why this
96/// function reports rather than acts: a caller who knows their storage layout
97/// can decide what the answer is worth, and one who does not should not have a
98/// placement chosen for them on the strength of it.
99///
100/// # Errors
101///
102/// Any error from `DeviceIoControl`, and
103/// [`io::ErrorKind::InvalidData`] if the control code returns a payload that is
104/// not the documented single `u32`.
105pub fn volume_numa_node(handle: RawHandle) -> io::Result<NumaNode> {
106    let mut node: u32 = u32::MAX;
107    let mut returned: u32 = 0;
108    // SAFETY: `handle` is the caller's, and outlives this call by the contract
109    // on this function. The FSCTL takes no input buffer, and its output is
110    // documented as `FSCTL_QUERY_VOLUME_NUMA_INFO_OUTPUT { ULONG NumaNode }` --
111    // one `u32`, which is what `node` provides and what the size argument says.
112    let ok = unsafe {
113        DeviceIoControl(
114            handle as HANDLE,
115            FSCTL_QUERY_VOLUME_NUMA_INFO,
116            std::ptr::null(),
117            0,
118            (&raw mut node).cast::<std::ffi::c_void>(),
119            u32::try_from(size_of::<u32>()).expect("four fits in a u32"),
120            &raw mut returned,
121            std::ptr::null_mut(),
122        )
123    };
124    if ok == 0 {
125        return Err(io::Error::last_os_error());
126    }
127    if returned as usize != size_of::<u32>() {
128        return Err(io::Error::new(
129            io::ErrorKind::InvalidData,
130            format!(
131                "FSCTL_QUERY_VOLUME_NUMA_INFO returned {returned} bytes, not {}",
132                size_of::<u32>()
133            ),
134        ));
135    }
136    Ok(NumaNode::new(node))
137}
138
139#[cfg(test)]
140mod tests;