clean docs and way chiller lifetimes and api

This commit is contained in:
2025-07-19 19:40:22 +02:00
parent 4af59d5ae0
commit 50b35de725
7 changed files with 593 additions and 474 deletions

112
src/double/mod.rs Normal file
View File

@@ -0,0 +1,112 @@
//! Doubly non-Arc linked list.
//!
//! Doubly as each node points to the next and previous node.
use std::ops::Deref;
use parking_lot::RwLock;
use crate::double::node::{BackNodeWriteLock, Node, NodeBackPtr};
pub mod node;
// # Rules to prevent deadlocks
//
// Left locking must be `try_` and if it fails at any point, the way rightwards must be cleared in
// case the task holding the left lock is moving rightwards.
// Rightwards locking can be blocking.
pub struct NodeHeadInner<'ll, T> {
start: Option<&'ll node::Node<'ll, T>>,
}
impl<T> Default for NodeHeadInner<'_, T> {
fn default() -> Self {
Self { start: None }
}
}
// TODO:
// impl<'ll, T> Drop for LinkedList<'ll, T> {
// fn drop(&mut self) {
// // SAFETY: this is the very last ref of &self so it can pretty much assume no external
// // refs into the inner data as they would be invalid to live after this
// while unsafe { self.pop().is_some() } {}
// }
// }
pub struct LinkedList<'ll, T>(RwLock<NodeHeadInner<'ll, T>>);
impl<T> Default for LinkedList<'_, T> {
#[must_use]
fn default() -> Self {
Self(RwLock::new(NodeHeadInner::default()))
}
}
impl<'ll, T> Deref for LinkedList<'ll, T> {
type Target = RwLock<NodeHeadInner<'ll, T>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'ll, T> LinkedList<'ll, T> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn prepend(&'ll self, data: T) {
let self_lock = self.write();
let next = self_lock.start;
let next_lock = next.map(|n| n.write());
let new_node = Node::new_leaked(data, NodeBackPtr::new_head(self), next);
// SAFETY: ptrs are surrounding and they've been locked all along
unsafe { new_node.integrate((BackNodeWriteLock::Head(self_lock), next_lock)) };
}
/// Returns [`None`] if there's no next node.
///
/// # Safety
///
/// There must be no outer references to the first node.
pub unsafe fn pop(&'ll self) -> Option<()> {
let self_lock = self.write();
let pop_node = self_lock.start?;
let pop_node_lock = pop_node.write();
let next_node_lock = pop_node_lock.next.map(|n| n.write());
// SAFETY: locked all along and consecutive nodes
unsafe {
Node::isolate(
&pop_node_lock,
(BackNodeWriteLock::Head(self_lock), next_node_lock),
);
}
drop(pop_node_lock);
// SAFETY: node has been isolated so no references out
unsafe { pop_node.wait_free() }
Some(())
}
pub fn clone_into_vec(&self) -> Vec<T>
where
T: Clone,
{
let mut total = Vec::new();
let mut next_node = self.read().start;
while let Some(node) = next_node {
let read = node.read();
total.push(read.data.clone());
next_node = read.next;
}
total
}
}
#[cfg(test)]
mod tests;

314
src/double/node.rs Normal file
View File

@@ -0,0 +1,314 @@
use std::{ops::Deref, ptr};
use parking_lot::{RwLock, RwLockReadGuard, RwLockUpgradableReadGuard, RwLockWriteGuard};
use super::NodeHeadInner;
#[repr(transparent)]
pub struct Node<'ll, T>(RwLock<NodeInner<'ll, T>>);
type NodeHead<'ll, T> = RwLock<NodeHeadInner<'ll, T>>;
pub enum NodeBackPtr<'ll, T> {
Head(&'ll NodeHead<'ll, T>),
Node(&'ll Node<'ll, T>),
}
// yes the whole purpose is docs, might add Isolated Guards around nodes here
pub mod topology_safety;
// TODO: RwLock the ptrs only instead of the node
// Box<(RwLock<(&prev, &next)>, T)>
// instead of
// Box<RwLock<(&prev, &next, T)>>
// allows user to opt out of RwLock, allowing changes to adyacent nodes while T is being externally
// used and enables T: ?Sized
pub struct NodeInner<'ll, T> {
pub(crate) prev: NodeBackPtr<'ll, T>,
pub(crate) next: Option<&'ll Node<'ll, T>>,
pub data: T,
}
impl<'ll, T> Deref for Node<'ll, T> {
type Target = RwLock<NodeInner<'ll, T>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> Deref for NodeInner<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T> Copy for NodeBackPtr<'_, T> {}
impl<T> Clone for NodeBackPtr<'_, T> {
// # TODO: check if this works as expected with the cacnonical clone impl as I'm not sure if
// Copy would make it recursive or not
#[allow(clippy::enum_glob_use, clippy::non_canonical_clone_impl)]
fn clone(&self) -> Self {
use NodeBackPtr::*;
match self {
Head(h) => Head(h),
Node(n) => Node(n),
}
}
}
type WriteAndBackDoublet<'ll, T> = (
BackNodeWriteLock<'ll, T>,
RwLockUpgradableReadGuard<'ll, NodeInner<'ll, T>>,
);
type WriteSurroundTriplet<'ll, T> = (
BackNodeWriteLock<'ll, T>,
RwLockWriteGuard<'ll, NodeInner<'ll, T>>,
Option<RwLockWriteGuard<'ll, NodeInner<'ll, T>>>,
);
type WriteOnlyAroundTriplet<'ll, T> = (
BackNodeWriteLock<'ll, T>,
Option<RwLockWriteGuard<'ll, NodeInner<'ll, T>>>,
);
impl<'ll, T> Node<'ll, T> {
// TODO: think about the isolaed state of the following 3 fn's
/// Creates a new node in the heap, will link to `prev` and `next` but will be isolated, it can
/// be thought of just ita data and the two pointers, having it isolated doesn't guarantee any
/// integration into the linked list.
///
/// As long as this node exists and is not properly integrated into a linked list, it's
/// considered that the `prev` and `next` refs are being held.
#[must_use]
pub fn new(data: T, prev: NodeBackPtr<'ll, T>, next: Option<&'ll Node<'ll, T>>) -> Self {
Self(RwLock::new(NodeInner { prev, next, data }))
}
/// Boxes [`self`]
#[must_use]
pub fn boxed(self) -> Box<Self> {
Box::new(self)
}
/// Leaks [`self`] as a [`Box<Self>`]
#[must_use]
pub fn leak(self: Box<Self>) -> &'static mut Self {
Box::leak(self)
}
pub fn new_leaked(
data: T,
prev: NodeBackPtr<'ll, T>,
next: Option<&'ll Node<'ll, T>>,
) -> &'ll mut Self {
Box::leak(Box::new(Self::new(data, prev, next)))
}
/// # Safety
///
/// The [`self`] pointer must come from a [`Box`] allocation like [`Self::boxed`] and
/// [`Self::leak`].
pub unsafe fn free(self: *mut Self) {
drop(unsafe { Box::from_raw(self) });
}
/// Frees the current node but waits until the inner [`RwLock`] has no waiters.
///
/// # Safety
///
/// There must be no references left to [`self`]
pub unsafe fn wait_free(&self) {
loop {
if self.is_locked() {
drop(self.write());
} else {
break;
}
}
let myself = ptr::from_ref(self).cast_mut();
unsafe { myself.free() }
}
pub fn lock_and_back(&'ll self) -> WriteAndBackDoublet<'ll, T> {
let mut self_read = self.upgradable_read();
// "soft" back lock
match self_read.prev.try_write() {
Some(prev_write) => (prev_write, self_read),
None => {
// already locked, no worries but we have to clear for the lock before use its
// possible way forward, we can also wait until `prev` is accesible either case
// (the task holding it could modify us or if it doesn't we need to lock that same
// node)
loop {
let old_prev = self_read.prev;
let old_prev_write =
RwLockUpgradableReadGuard::unlocked_fair(&mut self_read, move || {
old_prev.write()
});
// we reaquire ourselves after `unlocked_fair` so `self_read` couls have
// changed
if NodeBackPtr::ptr_eq(&self_read.prev, &old_prev) {
break (old_prev_write, self_read);
}
}
}
}
}
/// Attempts to get a write lock on the surrouding nodes
pub fn write_surround(&'ll self) -> WriteSurroundTriplet<'ll, T> {
// backward blocking must be try
let (prev_write, self_read) = self.lock_and_back();
// Now `prev` is write locked and we can block forwards
let self_write = RwLockUpgradableReadGuard::upgrade(self_read);
let next_write = self_write.next.map(|n| n.write());
(prev_write, self_write, next_write)
}
/// # Safety
///
/// The passed locks must also be consecutive for this to respect topology.
///
/// This node will remain isolated. See [`topology_safety`].
pub unsafe fn isolate(self_read: &NodeInner<'ll, T>, locks: WriteOnlyAroundTriplet<'ll, T>) {
let (mut back_write, next_write) = locks;
back_write.set_next(self_read.next);
if let Some(mut next_write) = next_write {
next_write.prev = self_read.prev;
}
}
/// # Safety
///
/// The passed locks must be surrounding for this to respect topology.
///
/// This taken node ([`self`]) must be an isolated node. See [`topology_safety`].
pub unsafe fn integrate(&'ll self, locks: WriteOnlyAroundTriplet<'ll, T>) {
let (mut back_write, next_write) = locks;
back_write.set_next(Some(self));
if let Some(mut next_write) = next_write {
next_write.prev = NodeBackPtr::new_node(self);
}
}
/// # Safety
///
/// [`self`] must be integrated into the linked list. See [`topology_safety`].
///
/// Assumes there's no other external references into this node when called as it will be
/// deallocated. This will also wait for all waiters into the node lock to finish before really
/// freeing it, this includes concurrent calls to this same node.
pub unsafe fn remove(&'ll self) {
let surround_locks = self.write_surround();
let (prev, myself, next) = surround_locks;
let around_locks = (prev, next);
// Should be integrated and the surrounding locks are consesutive and locked all along
unsafe { Self::isolate(&myself, around_locks) }
// lazy-wait for no readers remaining
drop(myself);
// SAFETY: The node is isolated so good to be freed.
unsafe { self.wait_free() }
}
}
/// Generic Write Lock of a [`NodeBackPtr`]
pub enum BackNodeWriteLock<'ll, T> {
Head(RwLockWriteGuard<'ll, NodeHeadInner<'ll, T>>),
Node(RwLockWriteGuard<'ll, NodeInner<'ll, T>>),
}
/// Generic Read Lock of a [`NodeBackPtr`]
pub enum BackNodeReadLock<'ll, T> {
Head(RwLockReadGuard<'ll, NodeHeadInner<'ll, T>>),
Node(RwLockReadGuard<'ll, NodeInner<'ll, T>>),
}
#[allow(clippy::enum_glob_use)]
impl<'ll, T> NodeBackPtr<'ll, T> {
#[must_use]
pub fn ptr_eq(&self, other: &Self) -> bool {
use NodeBackPtr::*;
match (self, other) {
(Head(h1), Head(h2)) => ptr::eq(h1, h2),
(Node(n1), Node(n2)) => ptr::eq(n1, n2),
_ => false,
}
}
#[must_use]
pub fn new_node(node: &'ll Node<'ll, T>) -> Self {
Self::Node(node)
}
#[must_use]
pub fn new_head(head: &'ll NodeHead<'ll, T>) -> Self {
Self::Head(head)
}
/// Analogous to [`RwLock::write`]
#[must_use]
pub fn write(&self) -> BackNodeWriteLock<'ll, T> {
use BackNodeWriteLock as WL;
use NodeBackPtr::*;
match self {
Head(h) => WL::Head(h.write()),
Node(n) => WL::Node(n.write()),
}
}
/// Analogous to [`RwLock::read`]
#[must_use]
pub fn read(&self) -> BackNodeReadLock<'ll, T> {
use BackNodeReadLock as RL;
use NodeBackPtr::*;
match self {
Head(h) => RL::Head(h.read()),
Node(n) => RL::Node(n.read()),
}
}
/// Analogous to [`RwLock::try_write`]
#[must_use]
pub fn try_write(&self) -> Option<BackNodeWriteLock<'ll, T>> {
use BackNodeWriteLock as WL;
use NodeBackPtr::*;
Some(match self {
Head(h) => WL::Head(h.try_write()?),
Node(n) => WL::Node(n.try_write()?),
})
}
/// Analogous to [`RwLock::try_read`]
#[must_use]
pub fn try_read(&self) -> Option<BackNodeReadLock<'ll, T>> {
use BackNodeReadLock as RL;
use NodeBackPtr::*;
Some(match self {
Head(h) => RL::Head(h.try_read()?),
Node(n) => RL::Node(n.try_read()?),
})
}
}
impl<'ll, T> BackNodeWriteLock<'ll, T> {
#[allow(clippy::enum_glob_use)]
fn set_next(&mut self, next: Option<&'ll Node<'ll, T>>) {
use BackNodeWriteLock::*;
match self {
Head(h) => h.start = next,
Node(n) => n.next = next,
}
}
}
impl<T> NodeInner<'_, T> {}

View File

@@ -0,0 +1,71 @@
//! The linked list is supposed to have bilinear continuity (can be iterated forwards and
//! backwards for each node and it will be linear).
//!
//! "Topology Safety" is the term I made for functions that if used incorrectly can break this
//! topology.
//!
//! # Side Effects
//!
//! Side effects to breaking the topology could be:
//! * Leaving unrelated nodes out of the linearity of the node.
//! * Creating loops in the linked list.
//!
//! I'm not so sure of these two but it depends on how hard you break the topology.
//!
//! It's completely fine although not recommended to unsafely manipulate the topology of the
//! linked list if you know what you are doing.
//!
//! However, [`LinkedList`]'s [`Drop`] implementation assumes the list can be iterated forward
//! and will attempt to drop each element it finds. If you broke the topology where this is
//! undoable you might want to use [`ManuallyDrop`] on it.
//!
//! # Safety
//!
//! For these functions to be topology safe, if they take locks to nodes, you must make sure
//! any related nodes and adyacent ones (as they hold pointers into that section) are locked
//! for all the operation's duration.
//!
//! If any node to be integrated had the adyacent nodes locked since its creation, it would be
//! safe to integrate in.
//!
//! # Examples
//!
//! Assume the following [`LinkedList`]:
//! ```txt
//! A -> B
//! ```
//!
//! If you then create `C` and `D` isolated after `A`. The [`LinkedList`] would remain the same
//! (as they are isolated) but `C` and `D` would have broken topology views of the list.
//!
//! If you then were to integrate `C` and create another `E` isolated after the newly
//! integrated `C`.
//! ```txt
//! A -> C -> B
//! // But D and E would thing
//! D: A -> D -> B
//! E: A -> C -> E -> B
//! ```
//!
//! If you now integrate `D`, `C` would be isolated indirectly without it knowing. But `E` is
//! also unaware of this and thinks `C` is integrated. So finally integrating `E` would lead
//! to:
//! ```txt
//! ╭─> A -> D -> B <╮
//! │ C <- E <──╯ │
//! ╰───┴────────────╯
//! ```
//!
//! No reading the chain from different points of view leads to:
//! ```txt
//! [A] -> D -> B
//! A -> C -> E -> [B]
//! A -> [C] -> E -> B
//! A -> [D] -> B
//! A -> C -> [E] -> B
//! ```
//!
//! This is more a graph than a linked list.
#[allow(unused_imports)]
use {super::NodeHeadInner, std::mem::ManuallyDrop};

81
src/double/tests.rs Normal file
View File

@@ -0,0 +1,81 @@
#![allow(clippy::items_after_statements)]
use std::{
fmt::Debug,
sync::{
Barrier,
atomic::{AtomicUsize, Ordering},
},
thread,
};
use super::*;
static DROP_C: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
struct StringWithDrop(String);
impl From<String> for StringWithDrop {
fn from(value: String) -> Self {
Self(value)
}
}
impl Drop for StringWithDrop {
fn drop(&mut self) {
DROP_C.fetch_add(1, Ordering::Relaxed);
}
}
impl Debug for StringWithDrop {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<String as Debug>::fmt(&self.0, f)
}
}
#[test]
fn it_works() {
const CREATE1: usize = 100;
const CREATE2: usize = 100;
const NOT_POP: usize = 20;
// TODO: Find out WHYTF do I need to scope this and compiler won't let me `drop`
{
let ll = LinkedList::<StringWithDrop>::new();
let barrier = Barrier::new(3);
thread::scope(|s| {
s.spawn(|| {
barrier.wait();
for n in 0..CREATE1 {
ll.prepend(format!("A {n}").into());
}
});
s.spawn(|| {
barrier.wait();
for n in 0..CREATE2 {
ll.prepend(format!("B {n}").into());
}
});
s.spawn(|| {
barrier.wait();
for _ in 0..(CREATE1 + CREATE2 - NOT_POP) {
unsafe {
while ll.pop().is_none() {
std::thread::yield_now();
}
}
}
});
});
assert_eq!(DROP_C.load(Ordering::Relaxed), CREATE1 + CREATE2 - NOT_POP);
}
// TODO: when drop is impl
// assert_eq!(DROP_C.load(Ordering::Relaxed), CREATE1 + CREATE2);
}

View File

@@ -1,399 +1,10 @@
#![feature(arbitrary_self_types, offset_of_enum, generic_const_items)]
#![doc = include_str!("../README.md")]
#![feature(arbitrary_self_types, arbitrary_self_types_pointers)]
#![warn(clippy::pedantic)]
#![allow(incomplete_features)]
use std::{
hint::unreachable_unchecked,
mem::{MaybeUninit, offset_of},
ops::Deref,
};
#[cfg(doc)]
use std::{pin::Pin, sync::Arc};
use parking_lot::{RwLock, RwLockWriteGuard};
pub mod double;
mod docs {
//! Rules for soundness of modifications.
//! To modify the pointer that goes to a node a write lock must be held to it: e.g:
//! - Bidirectional consistency is not guaranteed. If you "walk" a list you must only do so in
//! the same direction, that continuity will be guaranteed.
//! - If N node is to be removed, write lock it. Update adyacent pointers first and keep them
//! locked until the N node is freed, then release the adyacenty locks properly.
//! - The previous prevents deadlocks because by having a write lock of the previous node
//! before locking itself it guaratees that the previous lock can't get read access to
//! itself to get the ptr to the node of ourselves and update our prev ptr.
//! - For every operation only a single item in the list must be write blocked to prevent
//! deadlocks.
}
pub type NodeHead<T> = LinkedList<T>;
pub enum NodeDiscr<T: 'static> {
Head(RwLock<NodeHead<T>>),
Node(Node<T>),
}
impl<T> Default for NodeDiscr<T> {
fn default() -> Self {
Self::Head(RwLock::new(LinkedList::default()))
}
}
impl<T: 'static> NodeDiscr<T> {
#[must_use]
pub fn new(value: LinkedList<T>) -> Self {
Self::Head(RwLock::new(value))
}
/// # Safety
/// UB if [`self`] is not [`Self::Head`].
pub unsafe fn as_head_unchecked(&self) -> &RwLock<NodeHead<T>> {
let Self::Head(head) = self else {
unsafe { unreachable_unchecked() }
};
head
}
/// # Safety
/// UB if [`self`] is not [`Self::Node`].
pub unsafe fn as_node_unchecked(&self) -> &Node<T> {
let Self::Node(node) = self else {
unsafe { unreachable_unchecked() }
};
node
}
fn try_write(&'static self) -> Option<NodeDiscrWriteLocks<'static, T>> {
match self {
NodeDiscr::Head(h) => {
let lock = h.try_write()?;
Some(NodeDiscrWriteLocks::Head(lock))
}
NodeDiscr::Node(n) => {
let lock = n.0.try_write()?;
Some(NodeDiscrWriteLocks::Node(lock))
}
}
}
/// # Safety
///
/// Will leak if not handled properly.
#[must_use]
#[allow(clippy::mut_from_ref)]
fn alloc_new_with_ptrs(
data: T,
next: Option<&'static Node<T>>,
prev: &'static NodeDiscr<T>,
) -> (&'static Self, &'static Node<T>) {
let discr = Box::leak(Box::new(NodeDiscr::Node(Node(RwLock::new(NodeInner {
next: MaybeUninit::new(next),
prev: MaybeUninit::new(prev),
isolated: false,
data,
})))));
(discr, unsafe { discr.as_node_unchecked() })
}
}
#[allow(dead_code)] // We dont even read variants, just hold whatever lock
enum NodeDiscrWriteLocks<'a, T: 'static> {
Head(RwLockWriteGuard<'a, NodeHead<T>>),
Node(RwLockWriteGuard<'a, NodeInner<T>>),
}
impl<T: 'static> NodeDiscrWriteLocks<'_, T> {
fn set_next(&mut self, next: Option<&'static Node<T>>) {
match self {
Self::Head(h) => h.start = next,
Self::Node(n) => n.next = MaybeUninit::new(next),
}
}
}
#[repr(transparent)]
pub struct Node<T: 'static>(RwLock<NodeInner<T>>);
/// It's safe to assume `next` and `prev` are initialized. But any function which would break this
/// assumption should be considered unsafe.
struct NodeInner<T: 'static> {
next: MaybeUninit<Option<&'static Node<T>>>,
prev: MaybeUninit<&'static NodeDiscr<T>>,
/// intended for removal, when the `RwLock` is being "drained" from waiters, there might be
/// another remover waiting, if it finds this it simply aborts
isolated: bool,
data: T,
}
impl<T: 'static> Deref for NodeInner<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T> NodeInner<T> {
fn prev(&self) -> &'static NodeDiscr<T> {
unsafe { self.prev.assume_init() }
}
// /// Could also leak memory
// ///
// /// # Safety
// /// The `prev` self ptr is valid as long as the write lock is held, as soon as it's dropped it
// /// becomes invalid.
// fn try_update_prev(&self) -> Option<NodeDiscrWriteLocks<'static, T>> {
// match self.prev() {
// NodeDiscr::Head(h) => {
// let mut lock = h.try_write()?;
// lock.start = unsafe { self.next.assume_init() };
// Some(NodeDiscrWriteLocks::Head(lock))
// }
// NodeDiscr::Node(n) => {
// let mut lock = n.0.try_write()?;
// lock.next = self.next;
// Some(NodeDiscrWriteLocks::Node(lock))
// }
// }
// }
fn next(&self) -> Option<&'static Node<T>> {
unsafe { self.next.assume_init() }
}
// /// Could also leak memory.
// ///
// /// First option is if theres any next, second one if it locked or not.
// ///
// /// # Safety
// /// The `next` self ptr is valid as long as the write lock is held, as soon as it's dropped it
// /// becomes invalid.
// fn try_update_next(&self) -> Option<Option<RwLockWriteGuard<'static, NodeInner<T>>>> {
// self.next().map(|next| {
// if let Some(mut lock) = next.0.try_write() {
// lock.prev = self.prev;
// Some(lock)
// } else {
// None
// }
// })
// }
#[allow(clippy::type_complexity)]
fn try_write_sides(
&self,
) -> Option<(
NodeDiscrWriteLocks<'static, T>,
Option<RwLockWriteGuard<'static, NodeInner<T>>>,
)> {
let prev_lock = self.prev().try_write()?;
let next_lock = if let Some(next_lock) = self.next() {
Some(next_lock.0.try_write()?)
} else {
None
};
Some((prev_lock, next_lock))
}
}
impl<T> Node<T> {
/// # Safety
///
/// Node is uninitialized.
///
/// Will leak if not handled properly.
#[must_use]
#[allow(dead_code)]
unsafe fn alloc_new(data: T) -> &'static mut Self {
Box::leak(Box::new(Node(RwLock::new(NodeInner {
next: MaybeUninit::uninit(),
prev: MaybeUninit::uninit(),
isolated: true,
data,
}))))
}
/// Isolates the node from surrounding ones and returns a `ReadGuard` to the dangling node that
/// would leak unless freed or managed. This guard could still have readers or writers
/// awaiting. Adyacent write locks are also sent back to prevent their modification since the
/// isolation and make the pointers of self still valid.
///
/// If it returns None, the node was already isolated.
///
/// # Safety
///
/// Its unsafe to access `next` and `prev` ptr's after the edge locks are dropped.
#[allow(clippy::type_complexity)]
fn isolate(
&'_ self,
) -> Option<(
RwLockWriteGuard<'_, NodeInner<T>>,
(
NodeDiscrWriteLocks<'static, T>,
Option<RwLockWriteGuard<'static, NodeInner<T>>>,
),
)> {
loop {
let mut node = self.0.write();
if node.isolated {
break None;
}
let Some(mut sides) = node.try_write_sides() else {
drop(node);
std::thread::yield_now();
continue;
};
node.isolated = true;
sides.0.set_next(node.next());
if let Some(next) = &mut sides.1 {
next.prev = node.prev;
}
break Some((node, sides));
}
}
/// # Safety
///
/// Will remove this pointer from memory, there must be no external pointers to this as they
/// will point to invalid data and UB.
///
/// Will busy wait for no read/write locks to this slot and assume it's been completely
/// isolated then. Any access attempts while it's being freed (after waiting for locks) can
/// lead to weird UB.
pub unsafe fn remove(&self) {
unsafe {
let Some((node, edge_locks)) = self.isolate() else {
return;
};
// Drop the allocated data, edge ptrs remain valid meanwhile
drop(node); // let other readers/writers finish with this item
loop {
if self.0.is_locked() {
std::thread::yield_now();
} else {
break;
}
}
// Now that we are the only ref to ourselves its ok to take outselves as mutable
let myself = std::ptr::from_ref(self).cast_mut();
#[allow(clippy::items_after_statements)]
const OFFSET<T>: usize = offset_of!(NodeDiscr<T>, Node.0);
let myself_discr = myself.wrapping_byte_sub(OFFSET::<T>).cast::<NodeDiscr<T>>();
drop(Box::from_raw(myself_discr));
drop(edge_locks); // edge ptrs become invalid form now on
}
}
}
pub struct LinkedList<T: 'static> {
start: Option<&'static Node<T>>,
}
impl<T: 'static> Default for LinkedList<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> LinkedList<T> {
#[must_use]
pub fn new() -> Self {
Self { start: None }
}
/// # Safety
///
/// `head_ref` MUST be the [`NodeDiscr`] wrapped around the [`RwLock`] that `self` is locked
/// by. This is asserted in debug mode.
unsafe fn prepend(
mut self: RwLockWriteGuard<'_, Self>,
head_ref: &'static NodeDiscr<T>,
data: T,
) {
#[cfg(debug_assertions)]
{
let NodeDiscr::Head(ll_head) = head_ref else {
panic!("passed head_ref doesnt match lock");
};
debug_assert!(std::ptr::eq(RwLockWriteGuard::rwlock(&self), ll_head));
}
let next = self.start;
let (new_node_discr, new_node) = NodeDiscr::alloc_new_with_ptrs(data, next, head_ref);
if let Some(next) = next {
let mut next = next.0.write();
next.prev = MaybeUninit::new(new_node_discr);
}
self.start = Some(new_node);
}
}
pub struct LinkedListWrapper<T: 'static> {
// Safety: MUST be of the `Head` variant at all moments
inner: NodeDiscr<T>,
}
impl<T: 'static> Default for LinkedListWrapper<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: 'static> LinkedListWrapper<T> {
#[must_use]
pub fn new() -> Self {
Self {
inner: NodeDiscr::default(),
}
}
pub fn as_head(&'static self) -> &'static RwLock<LinkedList<T>> {
unsafe { self.inner.as_head_unchecked() }
}
/// # Safety
///
/// Nothing external must point to the item about to be popped.
pub unsafe fn pop(&'static self) {
loop {
let head_read = self.as_head().read();
let Some(node) = head_read.start else {
std::thread::yield_now();
continue;
};
unsafe {
drop(head_read);
node.remove();
break;
}
}
}
pub fn prepend(&'static self, data: T) {
let lock = self.as_head().write();
unsafe {
LinkedList::prepend(lock, &self.inner, data);
}
}
pub fn clone_into_vec(&'static self) -> Vec<T>
where
T: Clone,
{
let mut total = Vec::new();
let mut next_node = self.as_head().read().start;
while let Some(node) = next_node {
let read = node.0.read();
total.push(read.data.clone());
next_node = read.next();
}
total
}
}
#[cfg(test)]
mod tests;
pub use double::NodeHeadInner as DoublyLinkedList;

View File

@@ -1,79 +0,0 @@
use std::{
fmt::Debug,
sync::{
Barrier,
atomic::{AtomicUsize, Ordering},
},
thread,
};
use super::*;
static DROP_C: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
#[repr(transparent)]
struct StringWithDrop(String);
impl From<String> for StringWithDrop {
fn from(value: String) -> Self {
Self(value)
}
}
impl Drop for StringWithDrop {
fn drop(&mut self) {
DROP_C.fetch_add(1, Ordering::Relaxed);
println!("drop {self:?}");
}
}
impl Debug for StringWithDrop {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<String as Debug>::fmt(&self.0, f)
}
}
#[test]
fn it_works() {
let ll = Box::leak(Box::new(LinkedListWrapper::<StringWithDrop>::new()));
let barrier = Barrier::new(3);
println!("{:#?}", ll.clone_into_vec());
thread::scope(|s| {
s.spawn(|| {
barrier.wait();
for n in 0..100 {
ll.prepend(format!("A {n}").into());
}
});
s.spawn(|| {
barrier.wait();
for n in 0..100 {
ll.prepend(format!("B {n}").into());
}
});
s.spawn(|| {
barrier.wait();
for _ in 0..180 {
unsafe {
ll.pop();
}
}
});
});
let a = ll.clone_into_vec();
println!(
"{:?} len {} dropped {}",
a,
a.len(),
DROP_C.load(Ordering::Relaxed)
);
assert_eq!(4, 4);
}