use shared_memory::{Shmem, ShmemConf};
use std::{
mem::size_of,
ops::{Deref, DerefMut},
sync::atomic::{AtomicU32, AtomicU64, Ordering},
};
use thiserror::Error;
const MAGIC: u64 = 0x54414E47_4F434D50;
const VERSION: u32 = 4;
const DONE_BIT: u64 = 1 << 63;
const STOP_FLAG: u32 = 1;
#[repr(C)]
struct CommpageLayout {
magic: u64,
version: u32,
flags: AtomicU32,
cursor_c: CacheLineAligned<Cursor>,
cursor_b: CacheLineAligned<Cursor>,
}
#[repr(C)]
struct Cursor(AtomicU64);
#[repr(C, align(64))]
struct CacheLineAligned<T>(T);
impl<T> Deref for CacheLineAligned<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for CacheLineAligned<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Cursor {
fn set_value(&mut self, value: u64) {
self.0.store(value & !DONE_BIT, Ordering::Release);
}
fn load(&self) -> (bool, u64) {
let val = self.0.load(Ordering::Acquire);
(val & DONE_BIT != 0, val & !DONE_BIT)
}
fn set_done(&mut self) {
self.0.fetch_or(DONE_BIT, Ordering::AcqRel);
}
fn reset(&mut self) {
self.0.store(0, Ordering::Release);
}
}
impl CommpageLayout {
fn init(&mut self) {
self.magic = MAGIC;
self.version = VERSION;
*self.flags.get_mut() = 0;
self.cursor_c.reset();
self.cursor_b.reset();
}
fn validate(&self) -> Result<(), CommpageError> {
if self.magic != MAGIC {
return Err(CommpageError::BadMagic);
}
if self.version != VERSION {
return Err(CommpageError::BadVersion {
got: self.version,
expected: VERSION,
});
}
Ok(())
}
fn cursor(&self, role: Role) -> &Cursor {
match role {
Role::Candidate => &self.cursor_c,
Role::Baseline => &self.cursor_b,
}
}
fn cursor_mut(&mut self, role: Role) -> &mut Cursor {
match role {
Role::Candidate => &mut self.cursor_c,
Role::Baseline => &mut self.cursor_b,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
Candidate,
Baseline,
}
impl Role {
pub fn peer(self) -> Self {
match self {
Self::Candidate => Self::Baseline,
Self::Baseline => Self::Candidate,
}
}
}
pub struct Commpage {
shmem: Shmem,
}
impl Commpage {
pub fn create() -> Result<Self, CommpageError> {
let shmem = ShmemConf::new()
.size(size_of::<CommpageLayout>())
.create()
.map_err(CommpageError::Shmem)?;
let layout = unsafe { &mut *(shmem.as_ptr() as *mut CommpageLayout) };
layout.init();
Ok(Commpage { shmem })
}
pub fn open(name: &str) -> Result<Self, CommpageError> {
let shmem = ShmemConf::new()
.os_id(name)
.open()
.map_err(CommpageError::Shmem)?;
let layout = unsafe { &*(shmem.as_ptr() as *const CommpageLayout) };
layout.validate()?;
Ok(Commpage { shmem })
}
pub fn os_id(&self) -> &str {
self.shmem.get_os_id()
}
fn layout(&self) -> &CommpageLayout {
unsafe { &*(self.shmem.as_ptr() as *const CommpageLayout) }
}
fn layout_mut(&mut self) -> &mut CommpageLayout {
unsafe { &mut *(self.shmem.as_ptr() as *mut CommpageLayout) }
}
pub fn set_cursor_value(&mut self, role: Role, sample_no: u64) {
self.layout_mut().cursor_mut(role).set_value(sample_no);
}
pub fn wait_for_cursor_value(&self, role: Role, target: u64) -> bool {
let cursor = self.layout().cursor(role);
loop {
let (stop, val) = cursor.load();
if stop {
return false;
}
if val >= target {
return true;
} else {
std::hint::spin_loop();
}
}
}
pub fn mark_done(&mut self, role: Role) {
self.layout_mut().cursor_mut(role).set_done();
}
pub fn is_done(&self, role: Role) -> bool {
let (done, _) = self.layout().cursor(role).load();
done
}
pub fn is_some_done(&self) -> bool {
self.is_done(Role::Candidate) || self.is_done(Role::Baseline)
}
pub fn load_cursor_value(&self, role: Role) -> usize {
let (_, value) = self.layout().cursor(role).load();
value as usize
}
pub fn set_stop(&mut self) {
self.layout().flags.fetch_or(STOP_FLAG, Ordering::Release);
}
pub fn is_stopped(&self) -> bool {
self.layout().flags.load(Ordering::Acquire) & STOP_FLAG != 0
}
pub fn reset(&mut self) {
self.layout_mut().flags.store(0, Ordering::Release);
self.layout_mut().cursor_c.reset();
self.layout_mut().cursor_b.reset();
}
}
#[derive(Debug, Error)]
pub enum CommpageError {
#[error("shared memory error: {0}")]
Shmem(shared_memory::ShmemError),
#[error("invalid commpage magic number")]
BadMagic,
#[error("unsupported commpage version: {got} (expected {expected})")]
BadVersion { got: u32, expected: u32 },
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn create_and_open() {
let cp = Commpage::create().unwrap();
let id = cp.os_id().to_string();
let cp2 = Commpage::open(&id).unwrap();
assert!(!cp2.is_stopped());
}
#[test]
fn stop_flag() {
let mut cp = Commpage::create().unwrap();
assert!(!cp.is_stopped());
cp.set_stop();
assert!(cp.is_stopped());
}
#[test]
fn done_bit() {
let mut cp = Commpage::create().unwrap();
assert!(!cp.is_done(Role::Candidate));
cp.set_cursor_value(Role::Candidate, 1);
cp.mark_done(Role::Candidate);
assert!(cp.is_done(Role::Candidate));
assert_eq!(cp.load_cursor_value(Role::Candidate), 1);
}
#[test]
fn peer_synchronization_two_threads() {
let mut cp = Commpage::create().unwrap();
let id = cp.os_id().to_string();
let num_samples = 64u64;
let handle = thread::spawn(move || {
let mut cp = Commpage::open(&id).unwrap();
for i in 0..num_samples {
cp.set_cursor_value(Role::Candidate, i);
cp.wait_for_cursor_value(Role::Baseline, i);
}
cp.mark_done(Role::Candidate);
});
for i in 0..num_samples {
cp.set_cursor_value(Role::Baseline, i);
cp.wait_for_cursor_value(Role::Candidate, i);
}
cp.mark_done(Role::Baseline);
handle.join().unwrap();
assert_eq!(
cp.load_cursor_value(Role::Baseline),
(num_samples - 1) as usize
);
assert_eq!(
cp.load_cursor_value(Role::Candidate),
(num_samples - 1) as usize
);
}
}