Skip to main content

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;