use windows_sys::Win32::System::Kernel::PROCESSOR_NUMBER;
use windows_sys::Win32::System::SystemInformation::GROUP_AFFINITY;
use windows_sys::Win32::System::Threading::{
GetCurrentProcess, GetCurrentThread, GetProcessAffinityMask, GetThreadGroupAffinity,
SetThreadAffinityMask, SetThreadGroupAffinity,
};
use corescout_core::cpuset::CpuSet;
use corescout_core::error::{Error, Result};
use corescout_core::LogicalId;
fn last_error(call: &'static str) -> Error {
Error::syscall(
call,
std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
)
}
fn split(cpu: LogicalId) -> (u16, u64) {
((cpu / 64) as u16, 1u64 << (cpu % 64))
}
pub fn process_affinity() -> Result<CpuSet> {
let mut process_mask: usize = 0;
let mut system_mask: usize = 0;
let ok =
unsafe { GetProcessAffinityMask(GetCurrentProcess(), &mut process_mask, &mut system_mask) };
if ok == 0 {
return Err(last_error("GetProcessAffinityMask"));
}
let group = current_group().unwrap_or(0);
let mut cpus = CpuSet::new();
for bit in 0..64u32 {
if process_mask & (1usize << bit) != 0 {
cpus.insert(group as u32 * 64 + bit);
}
}
Ok(cpus)
}
fn current_group() -> Option<u16> {
let mut affinity = GROUP_AFFINITY {
Mask: 0,
Group: 0,
Reserved: [0; 3],
};
let ok = unsafe { GetThreadGroupAffinity(GetCurrentThread(), &mut affinity) };
(ok != 0).then_some(affinity.Group)
}
pub fn pin_current_thread(cpu: LogicalId) -> Result<()> {
let (group, mask) = split(cpu);
let affinity = GROUP_AFFINITY {
Mask: mask as usize,
Group: group,
Reserved: [0; 3],
};
let mut previous = GROUP_AFFINITY {
Mask: 0,
Group: 0,
Reserved: [0; 3],
};
let ok = unsafe { SetThreadGroupAffinity(GetCurrentThread(), &affinity, &mut previous) };
if ok == 0 {
return Err(last_error("SetThreadGroupAffinity"));
}
Ok(())
}
pub fn set_current_thread_affinity(cpus: &CpuSet) -> Result<()> {
if cpus.is_empty() {
return Err(Error::invalid(
"cannot confine a thread to an empty set of CPUs",
));
}
let groups: Vec<u16> = {
let mut seen: Vec<u16> = cpus.iter().map(|cpu| (cpu / 64) as u16).collect();
seen.sort_unstable();
seen.dedup();
seen
};
if groups.len() > 1 {
return Err(Error::unsupported(format!(
"an affinity spanning {} processor groups cannot be set as one mask on Windows",
groups.len()
)));
}
let group = groups[0];
let mut mask = 0usize;
for cpu in cpus.iter() {
mask |= 1usize << (cpu % 64);
}
let affinity = GROUP_AFFINITY {
Mask: mask,
Group: group,
Reserved: [0; 3],
};
let mut previous = GROUP_AFFINITY {
Mask: 0,
Group: 0,
Reserved: [0; 3],
};
let ok = unsafe { SetThreadGroupAffinity(GetCurrentThread(), &affinity, &mut previous) };
if ok == 0 {
return Err(last_error("SetThreadGroupAffinity"));
}
Ok(())
}
pub fn current_thread_affinity() -> Result<CpuSet> {
let mut affinity = GROUP_AFFINITY {
Mask: 0,
Group: 0,
Reserved: [0; 3],
};
let ok = unsafe { GetThreadGroupAffinity(GetCurrentThread(), &mut affinity) };
if ok != 0 && affinity.Mask != 0 {
let mut cpus = CpuSet::new();
for bit in 0..64u32 {
if affinity.Mask & (1usize << bit) != 0 {
cpus.insert(affinity.Group as u32 * 64 + bit);
}
}
return Ok(cpus);
}
let permitted = process_affinity()?;
let mut wide = 0usize;
for cpu in permitted.iter() {
wide |= 1usize << (cpu % 64);
}
let previous = unsafe { SetThreadAffinityMask(GetCurrentThread(), wide) };
if previous == 0 {
return Err(last_error("SetThreadAffinityMask"));
}
let restored = unsafe { SetThreadAffinityMask(GetCurrentThread(), previous) };
if restored == 0 {
return Err(last_error("SetThreadAffinityMask (restoring)"));
}
let group = current_group().unwrap_or(0);
let mut cpus = CpuSet::new();
for bit in 0..64u32 {
if previous & (1usize << bit) != 0 {
cpus.insert(group as u32 * 64 + bit);
}
}
Ok(cpus)
}
pub fn current_cpu() -> Option<LogicalId> {
let mut number = PROCESSOR_NUMBER {
Group: 0,
Number: 0,
Reserved: 0,
};
unsafe {
windows_sys::Win32::System::Threading::GetCurrentProcessorNumberEx(&mut number);
}
Some(number.Group as u32 * 64 + number.Number as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_process_is_permitted_some_cpus() {
let permitted = process_affinity().expect("Windows knows what we may use");
assert!(!permitted.is_empty());
}
#[test]
fn pinning_this_thread_actually_moves_it() {
let permitted = process_affinity().expect("permitted");
let original = current_thread_affinity().expect("readable");
for cpu in permitted.iter().take(4) {
pin_current_thread(cpu).expect("pinnable");
let now = current_thread_affinity().expect("readable");
assert_eq!(
now.to_vec(),
vec![cpu],
"after pinning to {cpu} the thread should be confined to it"
);
if let Some(running_on) = current_cpu() {
assert_eq!(
running_on, cpu,
"pinned to {cpu} but running on {running_on}"
);
}
}
set_current_thread_affinity(&original).expect("restorable");
}
#[test]
fn an_empty_affinity_is_refused() {
assert!(set_current_thread_affinity(&CpuSet::new()).is_err());
}
#[test]
fn a_narrowed_affinity_can_be_widened_again() {
let permitted = process_affinity().expect("permitted");
let original = current_thread_affinity().expect("readable");
let first = permitted.iter().next().expect("at least one CPU");
pin_current_thread(first).expect("pinnable");
assert_eq!(current_thread_affinity().unwrap().len(), 1);
set_current_thread_affinity(&permitted).expect("wideable");
assert!(!current_thread_affinity().unwrap().is_empty());
set_current_thread_affinity(&original).expect("restorable");
}
#[test]
fn splitting_a_logical_id_addresses_the_right_group() {
assert_eq!(split(0), (0, 1));
assert_eq!(split(63), (0, 1 << 63));
assert_eq!(split(64), (1, 1));
assert_eq!(split(65), (1, 2));
}
}