Realtime safe, waitfree, concurrency library

Compare changes

Choose any two refs to compare.

+25
.tangled/workflows/checks.yaml
··· 1 + when: 2 + - event: ["manual", "push"] 3 + branch: ["main"] 4 + 5 + engine: "nixery" 6 + 7 + # default clone 8 + 9 + dependencies: 10 + nixpkgs: 11 + - rustup 12 + - cargo-mutants 13 + # need a linker 14 + - gcc 15 + 16 + steps: 17 + - name: "install rust with miri" 18 + command: "rustup toolchain install nightly -c miri,rust-src --profile minimal" 19 + - name: "run regular tests" 20 + command: "cargo test" 21 + - name: "run mutants" 22 + command: 'cargo mutants -E "$(cat known_mutants_regex.txt)"' 23 + - name: "run miri tests" 24 + command: "cargo +nightly miri test" 25 +
+12
CHANGELOG.md
··· 3 3 This file is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) 4 4 This project follows semver and every release is checked by cargo-semver-checks. 5 5 6 + ## [0.2.3] - 2025-10-01 7 + 8 + ### Fixed 9 + - detect when a guard was forgotten to avoid UB 10 + 11 + ## [0.2.2] - 2025-08-09 12 + 13 + ### Changed 14 + - remove '#[inline]' annotations from public functions (mtomsoop) 15 + - remove '#[must_use]' from 'try_lock' because it doesn't really fit the usecase of must_use (oxotmzyv) 16 + - Change repo url to use the DID identifier 17 + 6 18 ## [0.2.1] - 2025-03-21 7 19 8 20 ### Added
+3 -3
Cargo.toml
··· 1 1 [package] 2 2 name = "simple-left-right" 3 - version = "0.2.1" 3 + version = "0.2.3" 4 4 edition = "2021" 5 5 rust-version = "1.82" 6 6 readme = "README.md" ··· 8 8 keywords = ["real-time", "lock-free", "read-write", "concurrency", "no-std"] 9 9 categories = ["concurrency"] 10 10 license = "MIT OR Apache-2.0" 11 - repository = "https://tangled.sh/@increasing.bsky.social/simple-left-right/" 11 + repository = "https://tangled.sh/did:plc:54jgbo4psy24qu2bk4njtpc4/simple-left-right/" 12 12 description = "Lockfree, realtime safe and copy-free Synchronisation" 13 - # workspace = "../" 13 + exclude = ["known_mutants_regex.txt", ".tangled", "mutants.out", "mutants.out.old"] 14 14 15 15 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
LICENSES/Apache-2.0.txt
··· 58 58 59 59 To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 60 60 61 - Copyright 2025 Lucas Baumann 61 + Copyright [yyyy] [name of copyright owner] 62 62 63 63 Licensed under the Apache License, Version 2.0 (the "License"); 64 64 you may not use this file except in compliance with the License.
+10
README.md
··· 13 13 as the write locking spins forever. 14 14 15 15 PRs should keep this state as much as possible. 16 + 17 + ## std::mem::forget 18 + Forgetting a Read/Write guard could lead to UB, so i detect that and panic if it happens. In order to make 19 + the detection cheap it doesn't differentiate between cases where a forget would lead to UB and cases where 20 + it doesn't. Just don't forget the guards and it won't panic. 21 + 22 + ## Git 23 + This project is hosted on [tangled](https://tangled.org/did:plc:54jgbo4psy24qu2bk4njtpc4/simple-left-right/), 24 + [github](https://github.com/luca3s/simple-left-right) and [codeberg](https://codeberg.org/increasing/simple-left-right). 25 + You can create issues and PRs on any platform you like.
+1
known_mutants_regex.txt
··· 1 + (replace \| with \^ in State::with_read|replace Shared<T>::release_read_lock with \(\)|replace State::can_write -> bool with false|replace Writer<T, O>::try_lock -> Option<WriteGuard<'_, T, O>> with None|replace <impl Drop for ReadGuard<'_, T>>::drop with \(\))
+35 -39
src/lib.rs
··· 1 + // SPDX-FileCopyrightText: 2025 Lucas Baumann 2 + // SPDX-FileCopyrightText: Lucas Baumann 3 + // 4 + // SPDX-License-Identifier: Apache-2.0 5 + // SPDX-License-Identifier: MIT 6 + 1 7 //! Simpler version of the left-right from Jon Gjengset library. 2 8 //! 3 9 //! Uses two copies of the value to allow doing small changes, while still allowing non-blocking reading. ··· 30 36 #[derive(Debug)] 31 37 pub struct Reader<T> { 32 38 shared: NonNull<Shared<T>>, 39 + locked: bool, 33 40 /// for drop check 34 41 _own: PhantomData<Shared<T>>, 35 42 } ··· 42 49 } 43 50 44 51 /// this function never blocks. (`fetch_update` loop doesn't count) 45 - #[inline] 46 52 pub fn lock(&mut self) -> ReadGuard<'_, T> { 47 - let shared_ref = self.shared_ref(); 48 - 53 + if self.locked { 54 + self.locked = false; 55 + panic!("ReadGuard was forgotten"); 56 + } 57 + self.locked = true; 58 + // SAFETY: value just locked 59 + let value = unsafe { &*self.shared_ref().lock_read().get() }; 49 60 ReadGuard { 50 - shared: shared_ref, 51 - value: shared_ref.lock_read(), 52 - reader: PhantomData, 61 + value, 62 + reader: self, 53 63 } 54 64 } 55 65 } ··· 58 68 unsafe impl<T: Send> Send for Reader<T> {} 59 69 60 70 impl<T> Drop for Reader<T> { 61 - #[inline] 62 71 fn drop(&mut self) { 63 72 // SAFETY: self.shared is valid and not used after this. 64 73 unsafe { Shared::drop(self.shared) }; 74 + assert!(!self.locked, "ReadGuard was forgotten"); 65 75 } 66 76 } 67 77 ··· 71 81 /// Doesn't implement Clone as that would require refcounting to know when to unlock. 72 82 #[derive(Debug)] 73 83 pub struct ReadGuard<'a, T> { 74 - shared: &'a Shared<T>, 75 - value: Ptr, 76 - /// `PhantomData` makes the borrow checker prove that there only ever is one `ReadGuard`. 77 - /// This allows resetting the readstate without some kind of counter 78 - reader: PhantomData<&'a mut Reader<T>>, 84 + reader: &'a mut Reader<T>, 85 + value: &'a T, 79 86 } 80 87 81 88 impl<T> Deref for ReadGuard<'_, T> { 82 89 type Target = T; 83 90 84 - #[inline] 85 91 fn deref(&self) -> &Self::Target { 86 - // SAFETY: ReadGuard was created, so the Writer knows not to write in this spot 87 - unsafe { self.shared.get_value_ref(self.value) } 92 + self.value 88 93 } 89 94 } 90 95 ··· 93 98 E: ?Sized, 94 99 T: AsRef<E>, 95 100 { 96 - #[inline] 97 101 fn as_ref(&self) -> &E { 98 102 self.deref().as_ref() 99 103 } 100 104 } 101 105 102 - // /// SAFETY: behaves like a ref to T. https://doc.rust-lang.org/std/marker/trait.Sync.html 103 - // unsafe impl<T: Sync> Send for ReadGuard<'_, T> {} 104 - // /// SAFETY: behaves like a ref to T. https://doc.rust-lang.org/std/marker/trait.Sync.html 105 - // unsafe impl<T: Sync> Sync for ReadGuard<'_, T> {} 106 - 107 106 impl<T> Drop for ReadGuard<'_, T> { 108 - #[inline] 109 107 fn drop(&mut self) { 110 108 // release the read lock 111 - self.shared.release_read_lock(); 109 + self.reader.shared_ref().release_read_lock(); 110 + self.reader.locked = false; 112 111 } 113 112 } 114 113 ··· 121 120 write_ptr: Ptr, 122 121 // buffer is pushed at the back and popped at the front. 123 122 op_buffer: VecDeque<O>, 123 + locked: bool, 124 124 // needed for drop_check 125 125 _own: PhantomData<Shared<T>>, 126 126 } ··· 152 152 } 153 153 154 154 /// get a Reader if none exists 155 - #[inline] 156 155 pub fn build_reader(&mut self) -> Option<Reader<T>> { 157 156 let shared_ref = self.shared_ref(); 158 157 // SAFETY: all is_unique_with_increase requirements are satisfied. ··· 162 161 Reader { 163 162 shared: self.shared, 164 163 _own: PhantomData, 164 + locked: false, 165 165 } 166 166 }) 167 167 } ··· 170 170 171 171 impl<T: Absorb<O>, O> Writer<T, O> { 172 172 /// doesn't block. Returns None if the Reader has a `ReadGuard` pointing to the old value. 173 - #[must_use] 174 173 pub fn try_lock(&mut self) -> Option<WriteGuard<'_, T, O>> { 174 + if self.locked { 175 + self.locked = false; 176 + panic!("WriteGuard was forgotten"); 177 + } 175 178 self.shared_ref() 176 179 .lock_write(self.write_ptr) 177 180 .ok() 178 181 // locking was successful 179 182 .map(|()| { 180 - // WriteGuard::new(self) 183 + self.locked = true; 181 184 let mut guard = WriteGuard { writer: self }; 182 185 while let Some(operation) = guard.writer.op_buffer.pop_front() { 183 186 guard.get_data_mut().absorb(operation); ··· 190 193 impl<T: Clone, O> Writer<T, O> { 191 194 /// Creates a new Writer by cloning the value once to get two values 192 195 /// `T::clone()` shoulnd't give a different value, as that would make this library pretty useless 193 - #[inline] 194 196 pub fn new(value: T) -> Self { 195 197 let (shared, write_ptr) = Shared::new(value, |value_1| value_1.clone()); 196 198 Self { ··· 198 200 write_ptr, 199 201 op_buffer: VecDeque::new(), 200 202 _own: PhantomData, 203 + locked: false, 201 204 } 202 205 } 203 206 } ··· 206 209 /// Creates a new Writer by calling `T::default()` twice to create the two values 207 210 /// 208 211 /// Default impl of T needs to give the same result every time. Not upholding this doens't lead to UB, but turns the library basically useless 209 - #[inline] 210 212 fn default() -> Self { 211 213 let (shared, write_ptr) = Shared::new(T::default(), |_| T::default()); 212 214 Self { ··· 214 216 write_ptr, 215 217 op_buffer: VecDeque::new(), 216 218 _own: PhantomData, 219 + locked: false, 217 220 } 218 221 } 219 222 } ··· 239 242 unsafe impl<T: Sync, O> Sync for Writer<T, O> {} 240 243 241 244 impl<T, O> Drop for Writer<T, O> { 242 - #[inline] 243 245 fn drop(&mut self) { 244 246 // SAFETY: self.shared is valid and not used after this. 245 247 unsafe { Shared::drop(self.shared) }; 248 + assert!(!self.locked, "WriteGuard was forgotten"); 246 249 } 247 250 } 248 251 ··· 254 257 /// Dropping this makes all changes available to the Reader. 255 258 #[derive(Debug)] 256 259 pub struct WriteGuard<'a, T, O> { 260 + // can't hold a mut ref to T, as then it wouldn't be possible to write to both at the same time, 261 + // which is an optimization i want to keep. 257 262 writer: &'a mut Writer<T, O>, 258 263 } 259 264 260 265 impl<T, O> WriteGuard<'_, T, O> { 261 266 /// Makes the changes available to the reader. Equivalent to `std::mem::drop(self)` 262 - #[inline] 263 267 pub fn swap(self) {} 264 268 265 269 /// Gets the value currently being written to. 266 - #[inline] 267 270 pub fn read(&self) -> &T { 268 271 // SAFETY: Only the WriteGuard can write to the values / create mut refs to them. 269 272 // The WriteGuard holds a mut ref to the writer so this function can't be called while a writeguard exists ··· 293 296 impl<T: Absorb<O>, O: Clone> WriteGuard<'_, T, O> { 294 297 /// applies operation to the current write Value and stores it to apply to the other later. 295 298 /// If there is no reader the operation is applied to both values immediately and not stored. 296 - #[inline] 297 299 pub fn apply_op(&mut self, operation: O) { 298 300 if let Some(shared) = self.writer.shared_mut() { 299 301 shared.value_1.get_mut().absorb(operation.clone()); ··· 305 307 } 306 308 } 307 309 308 - // /// SAFETY: behaves like a &mut T and &mut Vec<O>. https://doc.rust-lang.org/stable/std/marker/trait.Sync.html 309 - // unsafe impl<T: Send, O: Send> Send for WriteGuard<'_, T, O> {} 310 - 311 - // /// Safety: can only create shared refs to T, not to O. https://doc.rust-lang.org/stable/std/marker/trait.Sync.html 312 - // unsafe impl<T: Sync, O> Sync for WriteGuard<'_, T, O> {} 313 - 314 310 impl<T, O> Drop for WriteGuard<'_, T, O> { 315 - #[inline] 316 311 fn drop(&mut self) { 317 312 self.writer.swap(); 313 + self.writer.locked = false; 318 314 } 319 315 } 320 316
+9 -2
src/shared.rs
··· 1 + // SPDX-FileCopyrightText: 2025 Lucas Baumann 2 + // SPDX-FileCopyrightText: Lucas Baumann 3 + // 4 + // SPDX-License-Identifier: Apache-2.0 5 + // SPDX-License-Identifier: MIT 6 + 1 7 use core::{ 2 8 cell::UnsafeCell, 3 9 mem::MaybeUninit, ··· 109 115 } 110 116 111 117 impl<T> Shared<T> { 112 - pub(crate) fn lock_read(&self) -> Ptr { 118 + pub(crate) fn lock_read(&self) -> &UnsafeCell<T> { 113 119 // fetch update loop could be replaced with: 114 120 // - set read state to both 115 121 // - read read ptr ··· 125 131 // SAFETY: fetch_update closure always returns Some, so the result is alwyays Ok 126 132 let result = unsafe { result.unwrap_unchecked() }; 127 133 // result is the previous value, so the read_state isn't set, only the read_ptr 128 - State::new(result).read_ptr() 134 + let ptr = State::new(result).read_ptr(); 135 + self.get_value(ptr) 129 136 } 130 137 131 138 pub(crate) fn release_read_lock(&self) {
+66 -1
tests/tests.rs
··· 1 1 #[cfg(test)] 2 2 mod usage_test { 3 - use simple_left_right::{Absorb, WriteGuard, Writer}; 3 + use simple_left_right::{Absorb, ReadGuard, WriteGuard, Writer}; 4 4 use std::{cell::Cell, hint, time::Duration}; 5 5 6 6 fn spin_lock<T: Absorb<O>, O>(writer: &mut Writer<T, O>) -> WriteGuard<'_, T, O> { ··· 107 107 108 108 let read_lock = reader.lock(); 109 109 assert_eq!(*read_lock, 1); 110 + } 111 + 112 + #[test] 113 + fn assert_writeguard_none() { 114 + let mut writer = Writer::new(0); 115 + let mut reader = writer.build_reader().unwrap(); 116 + let read_lock = reader.lock(); 117 + let mut write_lock = writer.try_lock().unwrap(); 118 + write_lock.apply_op(CounterAddOp(1)); 119 + assert_eq!(*read_lock, 0); 120 + drop(write_lock); 121 + // read lock is still being held, so can't lock 122 + assert!(writer.try_lock().is_none()); 123 + drop(read_lock); 124 + // now do it all again, to also trigger code paths for the second value 125 + let read_lock = reader.lock(); 126 + let mut write_lock = writer.try_lock().unwrap(); 127 + write_lock.apply_op(CounterAddOp(1)); 128 + assert_eq!(*read_lock, 1); 129 + drop(write_lock); 130 + // read lock is still being held, so can't lock 131 + assert!(writer.try_lock().is_none()); 110 132 } 111 133 112 134 #[test] ··· 274 296 lock.apply_op(CounterAddOp(1)); 275 297 drop(lock); 276 298 } 299 + } 300 + 301 + #[test] 302 + // attempt to find similar breakge to: https://github.com/rust-lang/rust/issues/63787 303 + // i don't think it's possible, because you can't get a ref to the other value while still passing 304 + // a ref through the function boundary. You can only get a ref to the other value when passing Reader, 305 + // which already has a raw ptr. So i think i am not affected by this issue. 306 + fn break_ref() { 307 + #[derive(Clone, Copy)] 308 + struct SetToNone; 309 + impl Absorb<SetToNone> for Option<Box<i32>> { 310 + fn absorb(&mut self, _operation: SetToNone) { 311 + *self = None; 312 + } 313 + } 314 + 315 + let mut writer = Writer::new(Some(Box::new(0))); 316 + let mut reader = writer.build_reader().unwrap(); 317 + 318 + two_refs(&mut writer.try_lock().unwrap(), reader.lock()); 319 + 320 + fn two_refs( 321 + writer: &mut WriteGuard<'_, Option<Box<i32>>, SetToNone>, 322 + reader: ReadGuard<'_, Option<Box<i32>>>, 323 + ) { 324 + drop(reader); 325 + writer.apply_op(SetToNone); 326 + } 327 + } 328 + 329 + #[test] 330 + #[should_panic] 331 + fn forget_lock() { 332 + let mut writer: Writer<i32, CounterAddOp> = Writer::new(0); 333 + let mut reader = writer.build_reader().unwrap(); 334 + 335 + let write = writer.try_lock().unwrap(); 336 + core::mem::forget(write); 337 + let _ = writer.try_lock().unwrap(); 338 + 339 + let read = reader.lock(); 340 + core::mem::forget(read); 341 + reader.lock(); 277 342 } 278 343 }