+25
.tangled/workflows/checks.yaml
+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
+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
+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
+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
+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
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
+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
+66
-1
tests/tests.rs
+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
}