windows_topology_sys/processor_set.rs
1// Copyright (c) 2026 Mike Grier
2//! A set of logical processors spanning one or more Windows processor groups.
3
4use std::collections::BTreeMap;
5
6/// How many processors one group's mask can name: one bit per machine word
7/// bit, not a hardcoded 64 -- a 32-bit target's `usize` mask is only 32 bits
8/// wide, and hardcoding 64 there would let `insert`/`contains` silently
9/// address bits past the mask's actual width.
10pub(crate) const MAX_PROCESSORS_PER_GROUP: u32 = usize::BITS;
11
12/// A set of logical processors, correctly spanning more than one processor
13/// group.
14///
15/// A Windows thread's affinity is a `GROUP_AFFINITY` -- one group and a bitmask
16/// within it -- because a group is a hard boundary: no single mask can name
17/// processors in two different groups. A type that silently flattened to one
18/// 64-bit mask would misrepresent any topology wider than one group, which is
19/// exactly the class of bug this type exists to prevent. `ProcessorSet` keeps
20/// each group's mask separate instead.
21#[derive(Clone, Debug, Default, PartialEq, Eq)]
22pub struct ProcessorSet {
23 groups: BTreeMap<u16, usize>,
24}
25
26impl ProcessorSet {
27 /// A set naming no processors.
28 #[must_use]
29 pub fn empty() -> Self {
30 Self::default()
31 }
32
33 /// A set naming every bit set in `mask` within `group`.
34 #[must_use]
35 pub fn from_group_mask(group: u16, mask: usize) -> Self {
36 let mut set = Self::empty();
37 if mask != 0 {
38 set.groups.insert(group, mask);
39 }
40 set
41 }
42
43 /// Whether a processor number fits this platform's group width.
44 ///
45 /// A `ProcessorSet` holds one bit per processor in a `usize`, so the
46 /// ceiling is `usize::BITS` -- 64 on x86-64, 32 on `i686-pc-windows-msvc`.
47 /// `ProcessorId::number` is a `u8` and its field is public, so a caller can
48 /// build an id this set cannot hold; [`Self::insert`] asserts on one, and
49 /// this is how a caller asks *before* rather than being aborted.
50 #[must_use]
51 pub fn can_represent(number: u8) -> bool {
52 u32::from(number) < MAX_PROCESSORS_PER_GROUP
53 }
54
55 /// Add `number` within `group` to the set.
56 ///
57 /// # Panics
58 ///
59 /// Panics if `number` is `MAX_PROCESSORS_PER_GROUP` or greater: a
60 /// processor group cannot hold more processors than a mask has bits.
61 /// [`Self::can_represent`] is how a caller holding a publicly-constructed
62 /// [`crate::ProcessorId`] asks first -- the panic is for a caller building
63 /// a set it controls, not a way to reject a query's argument.
64 pub fn insert(&mut self, group: u16, number: u8) {
65 assert!(
66 u32::from(number) < MAX_PROCESSORS_PER_GROUP,
67 "a processor group has at most {MAX_PROCESSORS_PER_GROUP} processors, got {number}"
68 );
69 *self.groups.entry(group).or_insert(0) |= 1_usize << number;
70 }
71
72 /// Whether the set names no processors.
73 #[must_use]
74 pub fn is_empty(&self) -> bool {
75 self.groups.is_empty()
76 }
77
78 /// How many processors the set names, across every group.
79 #[must_use]
80 pub fn len(&self) -> usize {
81 self.groups
82 .values()
83 .map(|mask| mask.count_ones() as usize)
84 .sum()
85 }
86
87 /// Whether `group`/`number` is a member of this set.
88 #[must_use]
89 pub fn contains(&self, group: u16, number: u8) -> bool {
90 if u32::from(number) >= MAX_PROCESSORS_PER_GROUP {
91 return false;
92 }
93 self.groups
94 .get(&group)
95 .is_some_and(|mask| mask & (1_usize << number) != 0)
96 }
97
98 /// The raw mask for a single group, or `0` if the group has no members here.
99 #[must_use]
100 pub fn group_mask(&self, group: u16) -> usize {
101 self.groups.get(&group).copied().unwrap_or(0)
102 }
103
104 /// Iterate over `(group, mask)` pairs, one per group with any member here,
105 /// in ascending group order.
106 pub fn group_masks(&self) -> impl Iterator<Item = (u16, usize)> + '_ {
107 self.groups.iter().map(|(&group, &mask)| (group, mask))
108 }
109
110 /// Iterate over every `(group, number)` member, in ascending order.
111 pub fn iter(&self) -> impl Iterator<Item = (u16, u8)> + '_ {
112 self.groups.iter().flat_map(|(&group, &mask)| {
113 (0..MAX_PROCESSORS_PER_GROUP as u8)
114 .filter(move |&n| mask & (1_usize << n) != 0)
115 .map(move |n| (group, n))
116 })
117 }
118
119 /// The set of processors present in both `self` and `other`.
120 #[must_use]
121 pub fn intersection(&self, other: &Self) -> Self {
122 let mut result = Self::empty();
123 for (&group, &mask) in &self.groups {
124 let combined = mask & other.groups.get(&group).copied().unwrap_or(0);
125 if combined != 0 {
126 result.groups.insert(group, combined);
127 }
128 }
129 result
130 }
131
132 /// The set of processors present in either `self` or `other`.
133 #[must_use]
134 pub fn union(&self, other: &Self) -> Self {
135 let mut result = self.clone();
136 for (&group, &mask) in &other.groups {
137 *result.groups.entry(group).or_insert(0) |= mask;
138 }
139 result
140 }
141
142 /// Whether `self` and `other` share no processors.
143 #[must_use]
144 pub fn is_disjoint(&self, other: &Self) -> bool {
145 self.groups.iter().all(|(group, &mask)| {
146 other
147 .groups
148 .get(group)
149 .is_none_or(|&other_mask| mask & other_mask == 0)
150 })
151 }
152
153 /// Whether every processor in `self` is also in `other`.
154 ///
155 /// This is the comparison the granularity order is built on (M2+.2):
156 /// inclusion is *checkable* against the memberships the platform actually
157 /// reported, where a firmware level number is only asserted. The empty
158 /// set is a subset of everything, which follows from the definition and
159 /// is not a special case.
160 #[must_use]
161 pub fn is_subset(&self, other: &Self) -> bool {
162 self.groups.iter().all(|(group, &mask)| {
163 other
164 .groups
165 .get(group)
166 .is_some_and(|&other_mask| mask & !other_mask == 0)
167 })
168 }
169}
170
171impl FromIterator<(u16, u8)> for ProcessorSet {
172 fn from_iter<T: IntoIterator<Item = (u16, u8)>>(iter: T) -> Self {
173 let mut set = Self::empty();
174 for (group, number) in iter {
175 set.insert(group, number);
176 }
177 set
178 }
179}
180
181/// The wire shape of one member of a [`ProcessorSet`]: `{"group":_,"number":_}`.
182///
183/// Deliberately duplicated from `domain::ProcessorId` rather than reused: this
184/// module sits below `domain.rs` (D-2 in `DESIGN-NOTES.md` -- durable
185/// enumeration below, interpretation above), and importing a type from the
186/// higher layer purely to save two fields would invert that direction for a
187/// serialization convenience.
188#[cfg(feature = "serde")]
189#[derive(serde::Serialize, serde::Deserialize)]
190struct WireProcessor {
191 group: u16,
192 number: u8,
193}
194
195#[cfg(feature = "serde")]
196impl serde::Serialize for ProcessorSet {
197 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
198 use serde::ser::SerializeSeq;
199 let mut seq = serializer.serialize_seq(Some(self.len()))?;
200 for (group, number) in self.iter() {
201 seq.serialize_element(&WireProcessor { group, number })?;
202 }
203 seq.end()
204 }
205}
206
207#[cfg(feature = "serde")]
208impl<'de> serde::Deserialize<'de> for ProcessorSet {
209 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
210 let wire = Vec::<WireProcessor>::deserialize(deserializer)?;
211 let mut set = Self::empty();
212 for entry in wire {
213 if u32::from(entry.number) >= MAX_PROCESSORS_PER_GROUP {
214 // A real Windows GROUP_AFFINITY.Mask is one machine word, so a
215 // group genuinely cannot hold this processor: reject rather
216 // than silently truncate (D-10 in `DESIGN-NOTES.md`).
217 return Err(serde::de::Error::custom(format!(
218 "processor number {} is out of range: a processor group has at most \
219 {MAX_PROCESSORS_PER_GROUP} processors on this platform",
220 entry.number
221 )));
222 }
223 set.insert(entry.group, entry.number);
224 }
225 Ok(set)
226 }
227}
228
229#[cfg(test)]
230mod tests;