Realtime safe, waitfree, concurrency library

Compare changes

Choose any two refs to compare.

+5
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 + 6 11 ## [0.2.2] - 2025-08-09 7 12 8 13 ### Changed
+2 -3
Cargo.toml
··· 1 1 [package] 2 2 name = "simple-left-right" 3 - version = "0.2.2" 3 + version = "0.2.3" 4 4 edition = "2021" 5 5 rust-version = "1.82" 6 6 readme = "README.md" ··· 10 10 license = "MIT OR Apache-2.0" 11 11 repository = "https://tangled.sh/did:plc:54jgbo4psy24qu2bk4njtpc4/simple-left-right/" 12 12 description = "Lockfree, realtime safe and copy-free Synchronisation" 13 - exclude = ["known_mutants_regex.txt"] 14 - # workspace = "../" 13 + exclude = ["known_mutants_regex.txt", ".tangled", "mutants.out", "mutants.out.old"] 15 14 16 15 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+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.
+29 -25
src/lib.rs
··· 36 36 #[derive(Debug)] 37 37 pub struct Reader<T> { 38 38 shared: NonNull<Shared<T>>, 39 + locked: bool, 39 40 /// for drop check 40 41 _own: PhantomData<Shared<T>>, 41 42 } ··· 49 50 50 51 /// this function never blocks. (`fetch_update` loop doesn't count) 51 52 pub fn lock(&mut self) -> ReadGuard<'_, T> { 52 - let shared_ref = self.shared_ref(); 53 - 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() }; 54 60 ReadGuard { 55 - shared: shared_ref, 56 - value: shared_ref.lock_read(), 57 - reader: PhantomData, 61 + value, 62 + reader: self, 58 63 } 59 64 } 60 65 } ··· 66 71 fn drop(&mut self) { 67 72 // SAFETY: self.shared is valid and not used after this. 68 73 unsafe { Shared::drop(self.shared) }; 74 + assert!(!self.locked, "ReadGuard was forgotten"); 69 75 } 70 76 } 71 77 ··· 75 81 /// Doesn't implement Clone as that would require refcounting to know when to unlock. 76 82 #[derive(Debug)] 77 83 pub struct ReadGuard<'a, T> { 78 - shared: &'a Shared<T>, 79 - value: Ptr, 80 - /// `PhantomData` makes the borrow checker prove that there only ever is one `ReadGuard`. 81 - /// This allows resetting the readstate without some kind of counter 82 - reader: PhantomData<&'a mut Reader<T>>, 84 + reader: &'a mut Reader<T>, 85 + value: &'a T, 83 86 } 84 87 85 88 impl<T> Deref for ReadGuard<'_, T> { 86 89 type Target = T; 87 90 88 91 fn deref(&self) -> &Self::Target { 89 - // SAFETY: ReadGuard was created, so the Writer knows not to write in this spot 90 - unsafe { self.shared.get_value_ref(self.value) } 92 + self.value 91 93 } 92 94 } 93 95 ··· 101 103 } 102 104 } 103 105 104 - // /// SAFETY: behaves like a ref to T. https://doc.rust-lang.org/std/marker/trait.Sync.html 105 - // unsafe impl<T: Sync> Send for ReadGuard<'_, T> {} 106 - // /// SAFETY: behaves like a ref to T. https://doc.rust-lang.org/std/marker/trait.Sync.html 107 - // unsafe impl<T: Sync> Sync for ReadGuard<'_, T> {} 108 - 109 106 impl<T> Drop for ReadGuard<'_, T> { 110 107 fn drop(&mut self) { 111 108 // release the read lock 112 - self.shared.release_read_lock(); 109 + self.reader.shared_ref().release_read_lock(); 110 + self.reader.locked = false; 113 111 } 114 112 } 115 113 ··· 122 120 write_ptr: Ptr, 123 121 // buffer is pushed at the back and popped at the front. 124 122 op_buffer: VecDeque<O>, 123 + locked: bool, 125 124 // needed for drop_check 126 125 _own: PhantomData<Shared<T>>, 127 126 } ··· 162 161 Reader { 163 162 shared: self.shared, 164 163 _own: PhantomData, 164 + locked: false, 165 165 } 166 166 }) 167 167 } ··· 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 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 + } 174 178 self.shared_ref() 175 179 .lock_write(self.write_ptr) 176 180 .ok() 177 181 // locking was successful 178 182 .map(|()| { 179 - // WriteGuard::new(self) 183 + self.locked = true; 180 184 let mut guard = WriteGuard { writer: self }; 181 185 while let Some(operation) = guard.writer.op_buffer.pop_front() { 182 186 guard.get_data_mut().absorb(operation); ··· 196 200 write_ptr, 197 201 op_buffer: VecDeque::new(), 198 202 _own: PhantomData, 203 + locked: false, 199 204 } 200 205 } 201 206 } ··· 211 216 write_ptr, 212 217 op_buffer: VecDeque::new(), 213 218 _own: PhantomData, 219 + locked: false, 214 220 } 215 221 } 216 222 } ··· 239 245 fn drop(&mut self) { 240 246 // SAFETY: self.shared is valid and not used after this. 241 247 unsafe { Shared::drop(self.shared) }; 248 + assert!(!self.locked, "WriteGuard was forgotten"); 242 249 } 243 250 } 244 251 ··· 250 257 /// Dropping this makes all changes available to the Reader. 251 258 #[derive(Debug)] 252 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. 253 262 writer: &'a mut Writer<T, O>, 254 263 } 255 264 ··· 298 307 } 299 308 } 300 309 301 - // /// SAFETY: behaves like a &mut T and &mut Vec<O>. https://doc.rust-lang.org/stable/std/marker/trait.Sync.html 302 - // unsafe impl<T: Send, O: Send> Send for WriteGuard<'_, T, O> {} 303 - 304 - // /// Safety: can only create shared refs to T, not to O. https://doc.rust-lang.org/stable/std/marker/trait.Sync.html 305 - // unsafe impl<T: Sync, O> Sync for WriteGuard<'_, T, O> {} 306 - 307 310 impl<T, O> Drop for WriteGuard<'_, T, O> { 308 311 fn drop(&mut self) { 309 312 self.writer.swap(); 313 + self.writer.locked = false; 310 314 } 311 315 } 312 316
+3 -2
src/shared.rs
··· 115 115 } 116 116 117 117 impl<T> Shared<T> { 118 - pub(crate) fn lock_read(&self) -> Ptr { 118 + pub(crate) fn lock_read(&self) -> &UnsafeCell<T> { 119 119 // fetch update loop could be replaced with: 120 120 // - set read state to both 121 121 // - read read ptr ··· 131 131 // SAFETY: fetch_update closure always returns Some, so the result is alwyays Ok 132 132 let result = unsafe { result.unwrap_unchecked() }; 133 133 // result is the previous value, so the read_state isn't set, only the read_ptr 134 - State::new(result).read_ptr() 134 + let ptr = State::new(result).read_ptr(); 135 + self.get_value(ptr) 135 136 } 136 137 137 138 pub(crate) fn release_read_lock(&self) {
+2 -1
tests/tests.rs
··· 327 327 } 328 328 329 329 #[test] 330 + #[should_panic] 330 331 fn forget_lock() { 331 332 let mut writer: Writer<i32, CounterAddOp> = Writer::new(0); 332 333 let mut reader = writer.build_reader().unwrap(); 333 334 334 335 let write = writer.try_lock().unwrap(); 335 336 core::mem::forget(write); 336 - let write = writer.try_lock().unwrap(); 337 + let _ = writer.try_lock().unwrap(); 337 338 338 339 let read = reader.lock(); 339 340 core::mem::forget(read);