use core::{marker::PhantomData, mem::ManuallyDrop, pin::Pin, ptr::NonNull};
use crate::{
CpuBindingEpoch, CpuLocalError, CpuPin, CurrentThreadHeader, ThreadSwitchError, current_thread,
};
#[must_use = "dropping an uncommitted switch rolls back the next CPU binding"]
pub struct PreparedThreadSwitch<'switch> {
next: NonNull<CurrentThreadHeader>,
next_epoch: CpuBindingEpoch,
current_thread: usize,
area: crate::CpuAreaRef,
_scope: PhantomData<&'switch mut &'switch ()>,
_not_send_or_sync: PhantomData<*mut ()>,
}
impl PreparedThreadSwitch<'_> {
#[doc(hidden)]
pub const fn next_header(&self) -> NonNull<CurrentThreadHeader> {
self.next
}
#[doc(hidden)]
#[inline(always)]
pub unsafe fn commit(self) {
let prepared = ManuallyDrop::new(self);
unsafe { crate::register::commit_current_thread(prepared.area, prepared.current_thread) };
}
}
impl Drop for PreparedThreadSwitch<'_> {
fn drop(&mut self) {
let next = unsafe { Pin::new_unchecked(self.next.as_ref()) };
if unsafe { next.unbind_cpu(self.next_epoch) }.is_err() {
panic!("prepared thread-switch rollback lost the next CPU binding");
}
}
}
#[must_use = "the incoming task must withdraw the previous CPU binding"]
#[derive(Debug)]
pub struct PreviousThreadBinding {
previous: NonNull<CurrentThreadHeader>,
epoch: CpuBindingEpoch,
}
impl PreviousThreadBinding {
pub unsafe fn finish(
self,
previous: Pin<&CurrentThreadHeader>,
) -> Result<(), ThreadSwitchError> {
if previous.as_non_null() != self.previous {
return Err(ThreadSwitchError::PreviousThreadMismatch);
}
unsafe { previous.unbind_cpu(self.epoch) }
}
}
pub unsafe fn prepare_thread_switch<'switch>(
pin: &'switch CpuPin<'_>,
previous: Pin<&CurrentThreadHeader>,
next: Pin<&CurrentThreadHeader>,
) -> Result<(PreparedThreadSwitch<'switch>, PreviousThreadBinding), ThreadSwitchError> {
let published = current_thread(pin).map_err(|error| match error {
CpuLocalError::CurrentThreadMismatch => ThreadSwitchError::CurrentThreadMismatch,
other => ThreadSwitchError::CpuLocal(other),
})?;
if published != previous.as_non_null() {
return Err(ThreadSwitchError::CurrentThreadMismatch);
}
let previous_binding = previous
.cpu_binding()
.filter(|binding| binding.area == pin.area())
.ok_or(ThreadSwitchError::CurrentThreadMismatch)?;
let next_epoch = unsafe { next.bind_cpu(pin.area()) }?;
Ok((
PreparedThreadSwitch {
next: next.as_non_null(),
next_epoch,
current_thread: next.as_non_null().as_ptr() as usize,
area: pin.area(),
_scope: PhantomData,
_not_send_or_sync: PhantomData,
},
PreviousThreadBinding {
previous: previous.as_non_null(),
epoch: previous_binding.epoch,
},
))
}
#[cfg(all(test, feature = "host-test"))]
mod tests {
use core::mem::MaybeUninit;
use super::*;
use crate::{
CpuAreaPrefix, CpuAreaRef, CpuIndex, CurrentContext, install_bootstrap_thread,
install_cpu_area, with_cpu_pin,
};
fn on_fresh_modeled_cpu(operation: impl FnOnce(CpuAreaRef) + Send + 'static) {
std::thread::spawn(move || {
let storage = Box::leak(Box::new(MaybeUninit::<CpuAreaPrefix>::uninit()));
let base = storage.as_mut_ptr() as usize;
storage.write(CpuAreaPrefix::initialize(CpuIndex::try_from(0).unwrap(), base).unwrap());
let area = unsafe { CpuAreaRef::from_initialized_base(base) }.unwrap();
unsafe { install_cpu_area(area) }.unwrap();
operation(area);
})
.join()
.unwrap();
}
fn task_header(identity: usize) -> Pin<Box<CurrentThreadHeader>> {
Box::pin(CurrentThreadHeader::new(
CurrentContext::from_raw(identity).unwrap(),
))
}
#[test]
fn abandoned_prepare_rolls_back_next_binding() {
on_fresh_modeled_cpu(|area| {
let previous = task_header(1);
let next = task_header(2);
unsafe {
with_cpu_pin(|pin| {
install_bootstrap_thread(pin, previous.as_ref()).unwrap();
let (prepared, _previous_binding) =
prepare_thread_switch(pin, previous.as_ref(), next.as_ref()).unwrap();
assert_eq!(current_thread(pin), Ok(previous.as_ref().as_non_null()));
assert_eq!(next.cpu_area(), Some(area));
drop(prepared);
assert_eq!(current_thread(pin), Ok(previous.as_ref().as_non_null()));
assert_eq!(next.cpu_area(), None);
})
}
.unwrap();
});
}
#[test]
fn prepare_reports_the_domain_mismatch_before_binding_next() {
on_fresh_modeled_cpu(|_| {
let published = task_header(1);
let wrong_previous = task_header(2);
let next = task_header(3);
unsafe {
with_cpu_pin(|pin| {
install_bootstrap_thread(pin, published.as_ref()).unwrap();
let result = prepare_thread_switch(pin, wrong_previous.as_ref(), next.as_ref());
assert!(matches!(
result,
Err(ThreadSwitchError::CurrentThreadMismatch)
));
assert_eq!(next.cpu_area(), None);
})
}
.unwrap();
});
}
#[test]
fn publication_precedes_incoming_unbind() {
on_fresh_modeled_cpu(|area| {
let previous = task_header(1);
let next = task_header(2);
unsafe {
with_cpu_pin(|pin| {
install_bootstrap_thread(pin, previous.as_ref()).unwrap();
let (prepared, previous_binding) =
prepare_thread_switch(pin, previous.as_ref(), next.as_ref()).unwrap();
assert_eq!(current_thread(pin), Ok(previous.as_ref().as_non_null()));
assert_eq!(previous.cpu_area(), Some(area));
assert_eq!(next.cpu_area(), Some(area));
prepared.commit();
assert_eq!(current_thread(pin), Ok(next.as_ref().as_non_null()));
assert_eq!(previous.cpu_area(), Some(area));
previous_binding.finish(previous.as_ref()).unwrap();
assert_eq!(previous.cpu_area(), None);
assert_eq!(next.cpu_area(), Some(area));
})
}
.unwrap();
});
}
#[test]
fn stale_epoch_cannot_unbind_a_new_binding() {
on_fresh_modeled_cpu(|area| {
let header = task_header(1);
let stale = unsafe { header.as_ref().bind_cpu(area) }.unwrap();
unsafe { header.as_ref().unbind_cpu(stale) }.unwrap();
let current = unsafe { header.as_ref().bind_cpu(area) }.unwrap();
assert_eq!(
unsafe { header.as_ref().unbind_cpu(stale) },
Err(ThreadSwitchError::StalePreviousBinding)
);
assert_eq!(header.cpu_area(), Some(area));
unsafe { header.as_ref().unbind_cpu(current) }.unwrap();
});
}
}