From 64f503633d80f26138abc140cddf26267f139ad6 Mon Sep 17 00:00:00 2001 From: javalsai Date: Mon, 21 Jul 2025 20:34:09 +0200 Subject: [PATCH] feat: unsafely support ?Sized --- Cargo.lock | 10 ++- Cargo.toml | 1 + src/double/mod.rs | 56 +++++++++++------ src/double/node.rs | 147 ++++++++++++++++++++++++++------------------ src/double/tests.rs | 60 +++++++++++------- src/lib.rs | 7 ++- 6 files changed, 177 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 956d082..1d4f735 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,9 +24,15 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" name = "concurrent-linked-list" version = "0.1.0" dependencies = [ + "dst-clone", "parking_lot", ] +[[package]] +name = "dst-clone" +version = "0.1.0" +source = "git+https://git.javalsai.tuxcord.net/tuxcord/dst-clone#6a32bfa59455adafde07bafd3bc9f2182cd5127b" + [[package]] name = "libc" version = "0.2.174" @@ -68,9 +74,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" dependencies = [ "bitflags", ] diff --git a/Cargo.toml b/Cargo.toml index 9255cc1..4793425 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] +dst-clone = { git = "https://git.javalsai.tuxcord.net/tuxcord/dst-clone", version = "0" } parking_lot = { version = "0", default-features = false } diff --git a/src/double/mod.rs b/src/double/mod.rs index cfd3bcc..fbd1377 100644 --- a/src/double/mod.rs +++ b/src/double/mod.rs @@ -6,7 +6,7 @@ use std::{marker::PhantomPinned, mem::transmute, ops::Deref, pin::Pin}; use parking_lot::RwLock; -use crate::double::node::{BackNodeWriteLock, Node, NodeBackPtr}; +use crate::double::node::{BackNodeWriteLock, Node}; pub mod node; @@ -16,26 +16,26 @@ pub mod node; // case the task holding the left lock is moving rightwards. // Rightwards locking can be blocking. -pub struct NodeHeadInner<'ll, T> { +pub struct NodeHeadInner<'ll, T: ?Sized> { start: Option<&'ll node::Node<'ll, T>>, } -impl Default for NodeHeadInner<'_, T> { +impl Default for NodeHeadInner<'_, T> { fn default() -> Self { Self { start: None } } } -pub struct NodeHead<'ll, T>(RwLock>); +pub struct NodeHead<'ll, T: ?Sized>(RwLock>); -impl Default for NodeHead<'_, T> { +impl Default for NodeHead<'_, T> { #[must_use] fn default() -> Self { Self(RwLock::new(NodeHeadInner::default())) } } -impl<'ll, T> Deref for NodeHead<'ll, T> { +impl<'ll, T: ?Sized> Deref for NodeHead<'ll, T> { type Target = RwLock>; fn deref(&self) -> &Self::Target { @@ -43,7 +43,7 @@ impl<'ll, T> Deref for NodeHead<'ll, T> { } } -impl<'ll, T> NodeHead<'ll, T> { +impl<'ll, T: ?Sized> NodeHead<'ll, T> { #[must_use] pub fn new() -> Self { Self::default() @@ -85,13 +85,30 @@ impl<'ll, T> NodeHead<'ll, T> { while unsafe { self.pop().is_some() } {} } - pub fn prepend(&'ll self, data: T) { + pub fn prepend(&'ll self, data: T) -> &'ll Node<'ll, T> + where + T: Sized, + { 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); + let next_lock = next.map(|n| n.ptrs.write()); + let new_node = Node::new_leaked(data, node::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)) }; + new_node + } + + pub fn prepend_dst(&'ll self, data: &T) -> &'ll Node<'ll, T> + where + T: dst_clone::DstClone, + { + let self_lock = self.write(); + let next = self_lock.start; + let next_lock = next.map(|n| n.ptrs.write()); + let new_node = Node::leaked_from_dst(data, node::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)) }; + new_node } /// Returns [`None`] if there's no next node. @@ -102,8 +119,8 @@ impl<'ll, T> NodeHead<'ll, T> { 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()); + let pop_node_lock = pop_node.ptrs.write(); + let next_node_lock = pop_node_lock.next.map(|n| n.ptrs.write()); // SAFETY: locked all along and consecutive nodes unsafe { @@ -115,6 +132,7 @@ impl<'ll, T> NodeHead<'ll, T> { drop(pop_node_lock); // SAFETY: node has been isolated so no references out + // TODO: return a droppable guard with a ptr to the node instead unsafe { pop_node.wait_free() } Some(()) @@ -127,8 +145,8 @@ impl<'ll, T> NodeHead<'ll, T> { 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()); + let read = node.ptrs.read(); + total.push(node.data.clone()); next_node = read.next; } total @@ -140,12 +158,12 @@ impl<'ll, T> NodeHead<'ll, T> { /// Attempt to safe wrap around a [`NodeHead`] to allow a sound API with [`Drop`] implementation. /// /// Please see [`create_ll`] and [`del_ll`] -pub struct LinkedList<'ll, T> { +pub struct LinkedList<'ll, T: ?Sized> { head: NodeHead<'ll, T>, _pinned: PhantomPinned, } -impl<'ll, T> LinkedList<'ll, T> { +impl<'ll, T: ?Sized> LinkedList<'ll, T> { #[must_use] pub fn new() -> Self { Self::default() @@ -166,7 +184,7 @@ impl<'ll, T> LinkedList<'ll, T> { } } -impl Default for LinkedList<'_, T> { +impl Default for LinkedList<'_, T> { #[must_use] fn default() -> Self { Self { @@ -176,7 +194,7 @@ impl Default for LinkedList<'_, T> { } } -impl Drop for LinkedList<'_, T> { +impl Drop for LinkedList<'_, T> { fn drop(&mut self) { // Extend to 'static so the compiler doesn't cry, we know this covers 'll let myself = unsafe { self.extend_for(&()) }; @@ -227,8 +245,8 @@ impl Drop for LinkedList<'_, T> { /// ll.prepend("test2".to_string()); /// ``` pub macro create_ll($rhs:expr, $val:ident, $ref:ident) { - let $val = $rhs; let scope = (); + let $val = $rhs; let $ref = unsafe { $val.extend_for(&scope) }; } diff --git a/src/double/node.rs b/src/double/node.rs index 75f3a20..ab78124 100644 --- a/src/double/node.rs +++ b/src/double/node.rs @@ -1,13 +1,15 @@ -use std::{ops::Deref, ptr}; +use std::{ + alloc::{Layout, alloc, handle_alloc_error}, + ops::Deref, + ptr::{self, metadata}, +}; use parking_lot::{RwLock, RwLockReadGuard, RwLockUpgradableReadGuard, RwLockWriteGuard}; use super::NodeHeadInner; -#[repr(transparent)] -pub struct Node<'ll, T>(RwLock>); type NodeHead<'ll, T> = RwLock>; -pub enum NodeBackPtr<'ll, T> { +pub enum NodeBackPtr<'ll, T: ?Sized> { Head(&'ll NodeHead<'ll, T>), Node(&'ll Node<'ll, T>), } @@ -15,27 +17,18 @@ pub enum NodeBackPtr<'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> -// 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 struct NodePtrPair<'ll, T: ?Sized> { + pub prev: NodeBackPtr<'ll, T>, + pub next: Option<&'ll Node<'ll, T>>, +} + +#[repr(C)] +pub struct Node<'ll, T: ?Sized> { + pub ptrs: RwLock>, pub data: T, } -impl<'ll, T> Deref for Node<'ll, T> { - type Target = RwLock>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Deref for NodeInner<'_, T> { +impl Deref for Node<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -43,28 +36,27 @@ impl Deref for NodeInner<'_, T> { } } -impl Copy for NodeBackPtr<'_, T> {} -impl Clone for NodeBackPtr<'_, T> { +impl Copy for NodeBackPtr<'_, T> {} +impl Clone for NodeBackPtr<'_, T> { fn clone(&self) -> Self { *self } } type WriteAndBackDoublet<'ll, T> = ( BackNodeWriteLock<'ll, T>, - RwLockUpgradableReadGuard<'ll, NodeInner<'ll, T>>, + RwLockUpgradableReadGuard<'ll, NodePtrPair<'ll, T>>, ); type WriteSurroundTriplet<'ll, T> = ( BackNodeWriteLock<'ll, T>, - RwLockWriteGuard<'ll, NodeInner<'ll, T>>, - Option>>, + RwLockWriteGuard<'ll, NodePtrPair<'ll, T>>, + Option>>, ); type WriteOnlyAroundTriplet<'ll, T> = ( BackNodeWriteLock<'ll, T>, - Option>>, + Option>>, ); -impl<'ll, T> Node<'ll, T> { - // TODO: think about the isolaed state of the following 3 fn's +impl<'ll, T> Node<'ll, T> { /// 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. @@ -73,19 +65,10 @@ impl<'ll, T> Node<'ll, T> { /// 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 { - Box::new(self) - } - - /// Leaks [`self`] as a [`Box`] - #[must_use] - pub fn leak(self: Box) -> &'static mut Self { - Box::leak(self) + Self { + ptrs: RwLock::new(NodePtrPair { prev, next }), + data, + } } pub fn new_leaked( @@ -96,6 +79,52 @@ impl<'ll, T> Node<'ll, T> { Box::leak(Box::new(Self::new(data, prev, next))) } + /// Boxes [`self`] + #[must_use] + pub fn boxed(self) -> Box { + Box::new(self) + } +} + +impl<'ll, T: ?Sized> Node<'ll, T> { + /// # Panics + /// + /// On arithmetic overflow + fn layout_for(data: &T) -> (Layout, usize) { + Layout::new::>>() + .extend(Layout::for_value(data)) + .unwrap() + } + + /// [`Self::new_leaked`] from a DST type + pub fn leaked_from_dst( + data: &T, + prev: NodeBackPtr<'ll, T>, + next: Option<&'ll Node<'ll, T>>, + ) -> &'ll mut Self + where + T: dst_clone::DstClone, + { + let (layout, data_offset) = Self::layout_for(data); + let ptr = unsafe { alloc(layout) }; + if ptr.is_null() { + handle_alloc_error(layout) + } + + unsafe { + *(ptr.cast::>>()) = RwLock::new(NodePtrPair { prev, next }); + data.copy(ptr.add(data_offset)); + } + + Box::leak(unsafe { Box::from_raw(ptr::from_raw_parts_mut(ptr, metadata(data))) }) + } + + /// Leaks [`self`] as a [`Box`] + #[must_use] + pub fn leak(self: Box) -> &'static mut Self { + Box::leak(self) + } + /// # Safety /// /// The [`self`] pointer must come from a [`Box`] allocation like [`Self::boxed`] and @@ -111,8 +140,8 @@ impl<'ll, T> Node<'ll, T> { /// There must be no references left to [`self`] pub unsafe fn wait_free(&self) { loop { - if self.is_locked() { - drop(self.write()); + if self.ptrs.is_locked() { + drop(self.ptrs.write()); } else { break; } @@ -123,7 +152,7 @@ impl<'ll, T> Node<'ll, T> { } pub fn lock_and_back(&'ll self) -> WriteAndBackDoublet<'ll, T> { - let mut self_read = self.upgradable_read(); + let mut self_read = self.ptrs.upgradable_read(); // "soft" back lock match self_read.prev.try_write() { Some(prev_write) => (prev_write, self_read), @@ -154,7 +183,7 @@ impl<'ll, T> Node<'ll, T> { 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()); + let next_write = self_write.next.map(|n| n.ptrs.write()); (prev_write, self_write, next_write) } @@ -164,7 +193,7 @@ impl<'ll, T> Node<'ll, T> { /// 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>) { + pub unsafe fn isolate(self_read: &NodePtrPair<'ll, T>, locks: WriteOnlyAroundTriplet<'ll, T>) { let (mut back_write, next_write) = locks; back_write.set_next(self_read.next); @@ -209,19 +238,19 @@ impl<'ll, T> Node<'ll, T> { } /// Generic Write Lock of a [`NodeBackPtr`] -pub enum BackNodeWriteLock<'ll, T> { +pub enum BackNodeWriteLock<'ll, T: ?Sized> { Head(RwLockWriteGuard<'ll, NodeHeadInner<'ll, T>>), - Node(RwLockWriteGuard<'ll, NodeInner<'ll, T>>), + Node(RwLockWriteGuard<'ll, NodePtrPair<'ll, T>>), } /// Generic Read Lock of a [`NodeBackPtr`] -pub enum BackNodeReadLock<'ll, T> { +pub enum BackNodeReadLock<'ll, T: ?Sized> { Head(RwLockReadGuard<'ll, NodeHeadInner<'ll, T>>), - Node(RwLockReadGuard<'ll, NodeInner<'ll, T>>), + Node(RwLockReadGuard<'ll, NodePtrPair<'ll, T>>), } #[allow(clippy::enum_glob_use)] -impl<'ll, T> NodeBackPtr<'ll, T> { +impl<'ll, T: ?Sized> NodeBackPtr<'ll, T> { #[must_use] pub fn ptr_eq(&self, other: &Self) -> bool { use NodeBackPtr::*; @@ -250,7 +279,7 @@ impl<'ll, T> NodeBackPtr<'ll, T> { match self { Head(h) => WL::Head(h.write()), - Node(n) => WL::Node(n.write()), + Node(n) => WL::Node(n.ptrs.write()), } } @@ -262,7 +291,7 @@ impl<'ll, T> NodeBackPtr<'ll, T> { match self { Head(h) => RL::Head(h.read()), - Node(n) => RL::Node(n.read()), + Node(n) => RL::Node(n.ptrs.read()), } } @@ -274,7 +303,7 @@ impl<'ll, T> NodeBackPtr<'ll, T> { Some(match self { Head(h) => WL::Head(h.try_write()?), - Node(n) => WL::Node(n.try_write()?), + Node(n) => WL::Node(n.ptrs.try_write()?), }) } @@ -286,12 +315,12 @@ impl<'ll, T> NodeBackPtr<'ll, T> { Some(match self { Head(h) => RL::Head(h.try_read()?), - Node(n) => RL::Node(n.try_read()?), + Node(n) => RL::Node(n.ptrs.try_read()?), }) } } -impl<'ll, T> BackNodeWriteLock<'ll, T> { +impl<'ll, T: ?Sized> BackNodeWriteLock<'ll, T> { #[allow(clippy::enum_glob_use)] fn set_next(&mut self, next: Option<&'ll Node<'ll, T>>) { use BackNodeWriteLock::*; @@ -303,4 +332,4 @@ impl<'ll, T> BackNodeWriteLock<'ll, T> { } } -impl NodeInner<'_, T> {} +impl Node<'_, T> {} diff --git a/src/double/tests.rs b/src/double/tests.rs index 87346f4..70eb134 100644 --- a/src/double/tests.rs +++ b/src/double/tests.rs @@ -2,6 +2,7 @@ use std::{ fmt::Debug, + ptr, sync::{ Barrier, atomic::{AtomicUsize, Ordering}, @@ -11,31 +12,31 @@ use std::{ use super::*; -static DROP_C: AtomicUsize = AtomicUsize::new(0); - -#[derive(Clone)] -struct StringWithDrop(String); - -impl From 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 { - ::fmt(&self.0, f) - } -} - #[test] fn concurrency_and_scoped_drop() { + static DROP_C: AtomicUsize = AtomicUsize::new(0); + + #[derive(Clone)] + struct StringWithDrop(String); + + impl From 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 { + ::fmt(&self.0, f) + } + } + const CREATE1: usize = 100; const CREATE2: usize = 100; const NOT_POP: usize = 20; @@ -79,3 +80,16 @@ fn concurrency_and_scoped_drop() { assert_eq!(DROP_C.load(Ordering::Relaxed), CREATE1 + CREATE2); } } + +#[test] +fn unsized_store() { + create_ll!(LinkedList::::new(), ll_val, ll); + + let inserted = ll.prepend_dst("test"); + let first_node = ll.0.read().start.expect("first node to be some"); + assert!(ptr::addr_eq(inserted, first_node)); + + assert_eq!(&**inserted, "test"); + + del_ll!(ll_val, ll); +} diff --git a/src/lib.rs b/src/lib.rs index 2bdd64e..14fe744 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,10 @@ #![doc = include_str!("../README.md")] -#![feature(arbitrary_self_types, arbitrary_self_types_pointers, decl_macro)] +#![feature( + arbitrary_self_types, + arbitrary_self_types_pointers, + decl_macro, + ptr_metadata +)] #![warn(clippy::pedantic)] #[cfg(doc)]