Compare commits

...

2 Commits

Author SHA1 Message Date
0d8780017b add drop impl 2025-07-19 23:26:22 +02:00
50b35de725 clean docs and way chiller lifetimes and api 2025-07-19 19:44:12 +02:00
7 changed files with 625 additions and 474 deletions

9
README.md Normal file
View File

@@ -0,0 +1,9 @@
A [`LinkedList`] implementation avoiding the use of [`Arc`]s in favor of unsafe manual removal of nodes when the caller knows all possible references are left unused.
The point of this crate is to offer [`Pin`] guarantees on the references into the list while allowing it to be modified. The implementation of all this doesn't require mutable access to the linked list itself so as a side effect it's possible to use the list in concurrent manners.
This means that it will try as smartly as possible to allow concurrent modifications to it as long as the nodes affected are unrelated.
---
`cargo doc` is supported and is the main documentation of the library. But there's no official hosting of the document files.

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

@@ -0,0 +1,139 @@
//! Doubly non-Arc linked list.
//!
//! Doubly as each node points to the next and previous node.
use std::{mem::transmute, ops::Deref, pin::Pin};
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 }
}
}
pub struct LinkedList<'ll, T>(RwLock<NodeHeadInner<'ll, T>>);
impl<'ll, T> Drop for LinkedList<'ll, T> {
fn drop(&mut self) {
// SAFETY: this is the drop impl so we can guarantee the reference is valid for the
// lifetime of the struct itself and external references would be invalidated right after
// the [`Drop`] of it (this fn). I don't think there's a way to differenciate the lifetimes
// by a drop implementation so this would be safe as no external references lifetimes would
// be valid after drop finishes
let myself = unsafe { transmute::<&mut Self, &'ll mut Self>(self) };
while unsafe { myself.pop().is_some() } {}
}
}
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()
}
/// # Safety
///
/// The only context in which it's safe to call this when [`self`] was pinned immediately after
/// its creation so that it can be guaranteed that the returned reference is valid for all the
/// [`LinkedList`]'s lifetime.
///
/// The issue arises from the [`Drop`] implementation. It takes a `&mut` reference, that means
/// that all previous immutable references are dropped. But most methods of the linked require
/// you to promise the borrow of [`self`] is valid for all `'ll` and that's only true if no
/// destructor runs. This makes [`Drop`] incompatible with the use of methods of the form
/// `fn(&'myself self)`.
///
/// In turn to this, the [`Drop`] implementation must assume it's not the only reference even
/// thought it's `&mut`. Anyhow it should only be called when the scope of [`self`] is about to
/// end and the other references would be invalidated after the call to [`Drop`], though in
/// reality they really have no use before that call but to guarantee the "internal" self
/// references in the linked list remain valid through the destructor. It's kinda a really edge
/// case in the language with shared structures that require references with lifetimes as long
/// as self to guarantee their validity but still have [`Drop`] implementations.
#[must_use]
pub unsafe fn get_self_ref(myself: Pin<&Self>) -> &'ll Self {
unsafe { transmute::<&Self, &'ll Self>(myself.get_ref()) }
}
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};

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

@@ -0,0 +1,83 @@
#![allow(clippy::items_after_statements)]
use std::{
fmt::Debug,
pin::pin,
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 concurrency_and_scoped_drop() {
const CREATE1: usize = 100;
const CREATE2: usize = 100;
const NOT_POP: usize = 20;
{
// TODO: make this a macro or a "guard" struct that can be dropped
let ll = pin!(LinkedList::<StringWithDrop>::new());
let llref = unsafe { LinkedList::get_self_ref(ll.as_ref()) };
let barrier = Barrier::new(3);
thread::scope(|s| {
s.spawn(|| {
barrier.wait();
for n in 0..CREATE1 {
llref.prepend(format!("A {n}").into());
}
});
s.spawn(|| {
barrier.wait();
for n in 0..CREATE2 {
llref.prepend(format!("B {n}").into());
}
});
s.spawn(|| {
barrier.wait();
for _ in 0..(CREATE1 + CREATE2 - NOT_POP) {
unsafe {
while llref.pop().is_none() {
std::thread::yield_now();
}
}
}
});
});
assert_eq!(DROP_C.load(Ordering::Relaxed), CREATE1 + CREATE2 - NOT_POP);
}
assert_eq!(DROP_C.load(Ordering::Relaxed), CREATE1 + CREATE2);
}

View File

@@ -1,399 +1,13 @@
#![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);
}