Skip to main content

concurrent/
seqlock_payload.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use zerocopy::{FromBytes, Immutable, IntoBytes};
6
7use crate::common::{SYNC_OPT_ACQ_REL_OPS, SYNC_OPT_FENCE, SYNC_OPT_NONE, SyncOpt};
8use crate::copy::WellDefinedCopyable;
9use crate::seqlock::{SeqLock, WriteGuard};
10
11/// Payload wrapper for data protected by a [`SeqLock`].
12///
13/// `SeqLockPayload<T, LOCK_SYNC_OPT>` wraps a [`WellDefinedCopyable<T>`] and
14/// binds its synchronization mode (`COPY_SYNC_OPT`) to that required by
15/// `SeqLock<LOCK_SYNC_OPT>`.
16#[repr(transparent)]
17pub struct SeqLockPayload<
18    T: Copy + FromBytes + IntoBytes + Immutable,
19    const LOCK_SYNC_OPT: u8 = SYNC_OPT_FENCE,
20> {
21    // `Clone` and `Copy` are intentionally not implemented so shared instances
22    // cannot be copied non-atomically.
23    payload: WellDefinedCopyable<T>,
24}
25
26impl<T: Copy + FromBytes + IntoBytes + Immutable, const LOCK_SYNC_OPT: u8>
27    SeqLockPayload<T, LOCK_SYNC_OPT>
28{
29    /// The sync options we use for this payload are specified for us based on the
30    /// lock type we plan to use this payload with.
31    pub const COPY_SYNC_OPT: SyncOpt = SeqLock::<LOCK_SYNC_OPT>::COPY_WRAPPER_SYNC_OPT;
32
33    /// Forwarding constructor for our payload.
34    #[inline]
35    pub const fn new(instance: T) -> Self {
36        // The payload sync option selected by the lock should always be AcqRelOps,
37        // or None (in the case where the lock is using fences). Assert this at
38        // compile time.
39        const {
40            assert!(matches!(Self::COPY_SYNC_OPT, SyncOpt::AcqRelOps | SyncOpt::None));
41        };
42        Self { payload: WellDefinedCopyable::new(instance) }
43    }
44
45    /// Specific version of `read` which always uses the sync-opt dictated to
46    /// us by our associated lock type.
47    #[inline]
48    pub fn read(&self, dst: &mut T) {
49        if const { matches!(Self::COPY_SYNC_OPT, SyncOpt::AcqRelOps) } {
50            self.payload.read::<SYNC_OPT_ACQ_REL_OPS>(dst);
51        } else {
52            self.payload.read::<SYNC_OPT_NONE>(dst);
53        }
54    }
55
56    /// Specific version of `update` which always uses the sync-opt dictated to
57    /// us by our associated lock type.
58    #[inline]
59    pub fn update(&self, _guard: &WriteGuard<'_, LOCK_SYNC_OPT>, src: &T) {
60        if const { matches!(Self::COPY_SYNC_OPT, SyncOpt::AcqRelOps) } {
61            self.payload.update::<SYNC_OPT_ACQ_REL_OPS>(src);
62        } else {
63            self.payload.update::<SYNC_OPT_NONE>(src);
64        }
65    }
66
67    /// Gain R/W access to the payload in order to perform an in-place update of
68    /// the contents using fence-to-fence synchronization to protect the payload.
69    /// All stores to the payload accessed via this pointer should be done using
70    /// relaxed atomic stores.
71    #[inline]
72    #[must_use]
73    pub fn begin_in_place_update(&self, _guard: &WriteGuard<'_, LOCK_SYNC_OPT>) -> *mut T {
74        const {
75            assert!(
76                LOCK_SYNC_OPT == SYNC_OPT_FENCE,
77                "In-place updates can only be performed when using fence synchronization"
78            );
79        };
80        self.payload.instance.get()
81    }
82
83    /// Exposes [`WellDefinedCopyable::unsynchronized_get`] for this payload.
84    ///
85    /// Dereferencing the returned pointer is only safe if `_guard` belongs to the
86    /// [`SeqLock`] protecting this payload so that no concurrent writes can occur
87    /// while reading the instance.
88    #[inline]
89    #[must_use]
90    pub const fn unsynchronized_get(&self, _guard: &WriteGuard<'_, LOCK_SYNC_OPT>) -> *const T {
91        self.payload.unsynchronized_get()
92    }
93}
94
95impl<T: Copy + FromBytes + IntoBytes + Immutable + Default, const LOCK_SYNC_OPT: u8> Default
96    for SeqLockPayload<T, LOCK_SYNC_OPT>
97{
98    fn default() -> Self {
99        Self::new(T::default())
100    }
101}
102
103zr::static_assert!(size_of::<SeqLockPayload<u64>>() == size_of::<u64>());
104zr::static_assert!(align_of::<SeqLockPayload<u64>>() == align_of::<u64>());
105zr::static_assert!(size_of::<SeqLockPayload<[u64; 8]>>() == 64);
106zr::static_assert!(align_of::<SeqLockPayload<[u64; 8]>>() == align_of::<u64>());
107zr::static_assert!(
108    size_of::<SeqLockPayload<[u64; 3], SYNC_OPT_ACQ_REL_OPS>>() == size_of::<[u64; 3]>()
109);
110zr::static_assert!(
111    align_of::<SeqLockPayload<[u64; 3], SYNC_OPT_ACQ_REL_OPS>>() == align_of::<[u64; 3]>()
112);
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[repr(C)]
119    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, FromBytes, IntoBytes, Immutable)]
120    struct TestTransform {
121        a_offset: i64,
122        b_offset: i64,
123        numerator: u32,
124        denominator: u32,
125    }
126
127    impl TestTransform {
128        fn from_generation(generation: u64) -> Self {
129            Self {
130                a_offset: generation as i64,
131                b_offset: !(generation as i64),
132                numerator: generation as u32,
133                denominator: !(generation as u32),
134            }
135        }
136
137        fn is_consistent(&self) -> bool {
138            (self.a_offset == !self.b_offset)
139                && (self.numerator == !self.denominator)
140                && (self.numerator == self.a_offset as u32)
141        }
142    }
143
144    #[repr(C)]
145    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, FromBytes, IntoBytes, Immutable)]
146    struct TestParams {
147        values: [u64; 8],
148    }
149
150    impl TestParams {
151        fn from_generation(generation: u64) -> Self {
152            let mut values = [0; 8];
153            for (i, value) in values.iter_mut().enumerate() {
154                *value = generation.wrapping_add(i as u64);
155            }
156            Self { values }
157        }
158
159        fn is_consistent(&self) -> bool {
160            self.values
161                .iter()
162                .enumerate()
163                .all(|(i, value)| *value == self.values[0].wrapping_add(i as u64))
164        }
165    }
166
167    zr::static_assert!(size_of::<TestTransform>() == 24);
168    zr::static_assert!(size_of::<TestParams>() == 64);
169
170    type AcqRelPayload<T> = SeqLockPayload<T, SYNC_OPT_ACQ_REL_OPS>;
171
172    #[test]
173    fn test_copy_sync_opt() {
174        assert_eq!(SeqLockPayload::<u64>::COPY_SYNC_OPT, SyncOpt::None);
175        assert_eq!(AcqRelPayload::<u64>::COPY_SYNC_OPT, SyncOpt::AcqRelOps);
176
177        assert_eq!(
178            SeqLockPayload::<u64>::COPY_SYNC_OPT,
179            SeqLock::<SYNC_OPT_FENCE>::COPY_WRAPPER_SYNC_OPT
180        );
181        assert_eq!(
182            AcqRelPayload::<u64>::COPY_SYNC_OPT,
183            SeqLock::<SYNC_OPT_ACQ_REL_OPS>::COPY_WRAPPER_SYNC_OPT
184        );
185    }
186
187    #[test]
188    fn test_layout() {
189        assert_eq!(size_of::<SeqLockPayload<TestTransform>>(), size_of::<TestTransform>());
190        assert_eq!(align_of::<SeqLockPayload<TestTransform>>(), align_of::<TestTransform>());
191        assert_eq!(size_of::<SeqLockPayload<TestParams>>(), size_of::<TestParams>());
192        assert_eq!(align_of::<SeqLockPayload<TestParams>>(), align_of::<TestParams>());
193    }
194
195    #[test]
196    fn test_read_update_single_threaded() {
197        let lock: SeqLock = SeqLock::new();
198        let payload = SeqLockPayload::<TestTransform>::default();
199
200        let mut dst = TestTransform::from_generation(99);
201        payload.read(&mut dst);
202        assert_eq!(dst, TestTransform::default());
203
204        let expected = TestTransform::from_generation(42);
205        {
206            let guard = lock.acquire();
207            payload.update(&guard, &expected);
208
209            // SAFETY: The guard belongs to the lock protecting `payload`,
210            // and there is no concurrent writer.
211            assert_eq!(unsafe { *payload.unsynchronized_get(&guard) }, expected);
212        }
213
214        let mut observed = TestTransform::default();
215        lock.read_transaction(|| payload.read(&mut observed));
216        assert_eq!(observed, expected);
217    }
218
219    #[test]
220    fn test_read_update_acq_rel() {
221        let lock = SeqLock::<SYNC_OPT_ACQ_REL_OPS>::new();
222        let payload = AcqRelPayload::<TestParams>::new(TestParams::from_generation(1));
223
224        let mut dst = TestParams::default();
225        payload.read(&mut dst);
226        assert_eq!(dst, TestParams::from_generation(1));
227
228        let expected = TestParams::from_generation(9999);
229        {
230            let guard = lock.acquire();
231            payload.update(&guard, &expected);
232        }
233
234        let mut observed = TestParams::default();
235        lock.read_transaction(|| payload.read(&mut observed));
236        assert_eq!(observed, expected);
237    }
238
239    #[test]
240    fn test_in_place_update() {
241        use core::sync::atomic::{AtomicI64, Ordering};
242
243        let lock: SeqLock = SeqLock::new();
244        let payload = SeqLockPayload::<TestTransform>::default();
245
246        {
247            let guard = lock.acquire();
248            let ptr = payload.begin_in_place_update(&guard);
249
250            // SAFETY: `ptr` points to the live payload, `a_offset` is aligned for
251            // `AtomicI64`, and the payload is only accessed atomically.
252            unsafe {
253                AtomicI64::from_ptr(core::ptr::addr_of_mut!((*ptr).a_offset))
254                    .store(1234, Ordering::Relaxed);
255            }
256        }
257
258        let mut observed = TestTransform::default();
259        lock.read_transaction(|| payload.read(&mut observed));
260        assert_eq!(observed.a_offset, 1234);
261    }
262
263    #[test]
264    fn test_concurrent_readers_writers() {
265        use std::sync::Arc;
266        use std::thread;
267
268        struct Shared {
269            lock: SeqLock,
270            transform: SeqLockPayload<TestTransform>,
271            params: SeqLockPayload<TestParams>,
272        }
273
274        const ITERATIONS: u64 = 5000;
275
276        let shared = Arc::new(Shared {
277            lock: SeqLock::new(),
278            transform: SeqLockPayload::new(TestTransform::from_generation(0)),
279            params: SeqLockPayload::new(TestParams::from_generation(0)),
280        });
281
282        let writer = {
283            let shared = shared.clone();
284            thread::spawn(move || {
285                for i in 1..=ITERATIONS {
286                    let guard = shared.lock.acquire();
287                    shared.transform.update(&guard, &TestTransform::from_generation(i));
288                    shared.params.update(&guard, &TestParams::from_generation(i));
289                }
290            })
291        };
292
293        let mut successful_transactions: u64 = 0;
294        let mut observed: u64 = 0;
295        let mut transform = TestTransform::default();
296        let mut params = TestParams::default();
297        while observed < ITERATIONS {
298            let token = shared.lock.begin_read_transaction();
299            shared.transform.read(&mut transform);
300            shared.params.read(&mut params);
301            if shared.lock.end_read_transaction(token) {
302                assert!(
303                    transform.is_consistent(),
304                    "observed a torn transform in a successful transaction: {transform:?}"
305                );
306                assert!(
307                    params.is_consistent(),
308                    "observed a torn parameter block in a successful transaction: {params:?}"
309                );
310                assert_eq!(
311                    transform,
312                    TestTransform::from_generation(params.values[0]),
313                    "observed two payloads from different generations"
314                );
315
316                successful_transactions += 1;
317                observed = observed.max(params.values[0]);
318            }
319        }
320
321        shared.lock.read_transaction(|| {
322            shared.transform.read(&mut transform);
323            shared.params.read(&mut params);
324        });
325        assert!(transform.is_consistent());
326        assert!(params.is_consistent());
327
328        writer.join().unwrap();
329        assert!(successful_transactions > 0, "no read transaction ever succeeded");
330    }
331}