Re: [PATCH 2/2] rust: sync: Introduce LazyInit
From: lyude
Date: Wed May 27 2026 - 12:03:09 EST
Whoops - I noticed two silly issues with this patch I will address in
the next respin, comments down below:
On Tue, 2026-05-26 at 16:41 -0400, Lyude Paul wrote:
> SetOnce is nice, but it does have one problem - you can't use it with
> fallible initializers. While we will be adding support for doing that with
> SetOnce, this leads into another problem: There's no way for racing callers
> to actually block on the initialization of SetOnce, which makes it a
> difficult type to use safely for situations where we want to initialize
> data fallibly once, and then provide access to it to multiple users at once
> until drop.
>
> So to solve this, introduce a new type: LazyInit. LazyInit is like SetOnce
> with a couple of important differences:
>
> * It can't be used in const context
> * It can handle in-place fallible initializers.
> * It uses a Mutex for synchronization instead of an atomic, allowing
> callers to block on initialization.
> * It requires its contents already be Send + Sync, since the Mutex protects
> initializing data and not the data itself.
>
> Signed-off-by: Lyude Paul <lyude@xxxxxxxxxx>
> ---
> rust/kernel/sync.rs | 2 +
> rust/kernel/sync/lazy_init.rs | 354 ++++++++++++++++++++++++++++++++++
> 2 files changed, 356 insertions(+)
> create mode 100644 rust/kernel/sync/lazy_init.rs
>
> diff --git a/rust/kernel/sync.rs b/rust/kernel/sync.rs
> index 993dbf2caa0e3..d18c26c56c4d2 100644
> --- a/rust/kernel/sync.rs
> +++ b/rust/kernel/sync.rs
> @@ -15,6 +15,7 @@
> pub mod barrier;
> pub mod completion;
> mod condvar;
> +mod lazy_init;
> pub mod lock;
> mod locked_by;
> pub mod poll;
> @@ -25,6 +26,7 @@
> pub use arc::{Arc, ArcBorrow, UniqueArc};
> pub use completion::Completion;
> pub use condvar::{new_condvar, CondVar, CondVarTimeoutResult};
> +pub use lazy_init::*;
> pub use lock::global::{global_lock, GlobalGuard, GlobalLock, GlobalLockBackend, GlobalLockedBy};
> pub use lock::mutex::{new_mutex, Mutex, MutexGuard};
> pub use lock::spinlock::{new_spinlock, SpinLock, SpinLockGuard};
> diff --git a/rust/kernel/sync/lazy_init.rs b/rust/kernel/sync/lazy_init.rs
> new file mode 100644
> index 0000000000000..707af369e965d
> --- /dev/null
> +++ b/rust/kernel/sync/lazy_init.rs
> @@ -0,0 +1,354 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +//! Thread-safe Lazy-[`Init`] and [`PinInit`]
> +use crate::{
> + prelude::*,
> + sync::{
> + LockClassKey,
> + Mutex,
> + MutexGuard, //
> + },
> +};
> +use core::{
> + mem::{
> + self,
> + MaybeUninit, //
> + },
> + ptr,
> +};
> +use pin_init::{
> + pin_init,
> + PinInit, //
> +};
> +
> +/// # Invariants
> +///
> +/// - `data` is guaranteed to be initialized if `is_init` is `true`.
> +/// - `data` is only written to once from `LazyInit::init()`, and once from `LazyInit::reset()`
> +/// (which cannot race with `LazyInit::init()` due to requiring a `&mut` reference to the
> +/// `LazyInit`).
> +#[pin_data(PinnedDrop)]
> +struct Inner<T: Send + Sync> {
> + #[pin]
> + data: MaybeUninit<T>,
> + is_init: bool,
> +}
> +
> +#[pinned_drop]
> +impl<T: Send + Sync> PinnedDrop for Inner<T> {
> + fn drop(self: Pin<&mut Self>) {
> + if self.is_init {
> + // SAFETY: The only contents from `this` that we move is `is_init`. `is_init` is Unpin,
> + // making this OK.
> + let this = unsafe { self.get_unchecked_mut() };
> +
> + // INVARIANT: This is the only place other then `LazyInit::populate()` where we write to
> + // `data`, and this function requires &mut self - ensuring it cannot race with
> + // `LazyInit::populate()`.
I renamed this function from populate() to init() before sending out
this series, so this definitely needs to be fixed
> + // SAFETY: Since `is_init` is true, our type invariants guarantee `data` is initialized.
> + unsafe { this.data.assume_init_drop() };
> +
> + this.is_init = false;
> + }
> + }
> +}
> +
> +/// Errors that can occur during [`LazyInit::new`].
> +#[derive(Debug)]
> +pub enum LazyInitError<'a, T: Send + Sync, E = Error> {
> + /// The [`LazyInit`] has already been initialized.
> + ///
> + /// `self.0` contains a reference to the previously initialized value.
> + AlreadyInit(&'a T),
> + /// An error occurred during initialization.
> + DuringInit(E),
> +}
> +
> +impl<'a, T: Send + Sync, E> From<E> for LazyInitError<'a, T, E> {
> + fn from(value: E) -> Self {
> + Self::DuringInit(value)
> + }
> +}
> +
> +/// Creates a [`LazyInit`] initialiser with the given name and newly-created lock class.
> +///
> +/// It uses the name if one is given, otherwise it generates one based on the file name and line
> +/// number.
> +#[macro_export]
> +macro_rules! new_lazy_init {
> + ($( $name:literal )?) => {
> + $crate::sync::LazyInit::new($crate::optional_name!($($name)?), $crate::static_lock_class!())
> + };
> +}
> +pub use new_lazy_init;
> +
> +/// A thread-safe container that allows thread-safe single-time initialization of its contents,
> +/// which can then have shared references taken to its contents. It is similar to [`SetOnce`] except
> +/// that it uses a [`Mutex`], it can be populated using fallible [`PinInit`] initializers, and
> +/// multiple callers attempting to initialize at the same time will block until the conetents of the
> +/// [`LazyInit`] is finished initializing.
> +///
> +/// This type cannot be used in `const` contexts, however, unlike [`SetOnce`] users of this type are
> +/// able to block if another thread is busy populating the [`SetOnce`]. This also allows the use of
> +/// fallible initializers for population.
> +///
> +/// This type internally uses a [`Mutex`] for synchronizing creation of `T`, but allows access to
> +/// `T` outside of lock post-init. This means that only the initialization of the value is protected
> +/// by the lock, and thus `T` must provide it's own synchronization by implementing `Send` + `Sync`.
> +///
> +/// # Examples
> +///
> +/// ```
> +/// use kernel::sync::{
> +/// LazyInit,
> +/// LazyInitError,
> +/// Arc,
> +/// new_lazy_init, //
> +/// };
> +///
> +/// struct Inner {
> +/// a: u8,
> +/// }
> +///
> +/// #[pin_data]
> +/// struct Example {
> +/// #[pin]
> +/// lazy: LazyInit<Arc<Inner>>,
> +/// }
> +///
> +/// impl Example {
> +/// fn new() -> impl PinInit<Self> {
> +/// pin_init!(Self {
> +/// lazy <- new_lazy_init!(),
> +/// })
> +/// }
> +/// }
> +///
> +/// // Allocate a boxed `Example`.
> +/// let e = KBox::pin_init(Example::new(), GFP_KERNEL)?;
> +///
> +/// assert!(e.lazy.init(Arc::init(init!(Inner { a: 42 }), GFP_KERNEL)).is_ok());
> +///
> +/// // `as_ref()` can be used to get a reference to the contents of a `LazyInit` if its initialized.
> +/// assert_eq!(e.lazy.as_ref().unwrap().a, 42);
> +///
> +/// // If `LazyInit::init()` fails due to the `LazyInit` already being initialized, a reference to
> +/// // the initialized data can still be retrieved from the error.
> +/// match e.lazy.init(Arc::init(init!(Inner { a: 54 }), GFP_KERNEL)) {
> +/// Err(LazyInitError::AlreadyInit(ret)) => assert_eq!(ret.a, 42),
> +/// _ => unreachable!(),
> +/// }
> +/// # Ok::<(), Error>(())
> +/// ```
> +///
> +/// [`SetOnce`]: super::SetOnce
> +#[pin_data]
> +pub struct LazyInit<T: Send + Sync> {
> + #[pin]
> + inner: Mutex<Inner<T>>,
> +}
> +
> +impl<T: Send + Sync> LazyInit<T> {
> + /// Create a new initializer for an empty [`LazyInit`].
> + pub fn new(name: &'static CStr, key: Pin<&'static LockClassKey>) -> impl PinInit<Self> {
> + pin_init!(Self {
> + inner <- Mutex::new(
> + pin_init!(Inner {
> + data <- MaybeUninit::uninit(),
> + is_init: false,
> + }),
> + name,
> + key
> + )
> + })
> + }
> +
> + /// Retrieve the contents of `inner.data` and extend their lifetime.
> + ///
> + /// # Safety
> + ///
> + /// The caller guarantees that `self.inner.data` has been previously initialized.
> + #[inline(always)]
> + unsafe fn data<'a>(&'a self, inner: &MutexGuard<'_, Inner<T>>) -> &'a T {
> + // SAFETY:
> + // - Our safety contract guarantees `inner.data` is initialized.
> + // - `T` is `Send` + `Sync`, and thus does not need the `Mutex` for synchronization, making
> + // it safe to hold onto outside of the lock.
> + // - We're guaranteed the container of `T` will not be written to via `Inner`s type
> + // invariants until `Drop`, ensuring it remains populated for the lifetime of 'a.
> + unsafe { mem::transmute::<&_, &'a _>(inner.data.assume_init_ref()) }
> + }
> +
> + /// Attempt to init the contents of [`LazyInit`] and return a reference to its contents.
> + ///
> + /// If the contents of the [`LazyInit`] were already initialized, they will not be
> + /// re-initialized.
> + ///
> + /// Returns:
> + ///
> + /// - `Ok(&T)` if the container was initialized successfully, or if it was already initialized
> + /// by another user.
> + /// - [`Err(LazyInitError::AlreadyInit)`](LazyInitError::AlreadyInit) if the container was
> + /// already initialized. This error contains a reference to the contents of the [`LazyInit`].
> + /// - [`Err(LazyInitError::DuringInit)`](LazyInitError::DuringInit) if an error was encountered
> + /// while trying to initialize this [`LazyInit`].
> + pub fn init<'a, E>(
> + &'a self,
> + init: impl PinInit<T, E>,
> + ) -> Result<&'a T, LazyInitError<'a, T, E>> {
> + let mut inner = self.inner.lock();
> + let do_init = !inner.is_init;
> +
> + if do_init {
> + // SAFETY: The only thing that we move is `is_init`, which is Unpin.
> + let inner = unsafe { inner.as_mut().get_unchecked_mut() };
> +
> + // INVARIANT: This is the only place we can write to `data` before Drop, fulfilling
> + // `Inner`'s relevant type invariant.
> + //
> + // SAFETY:
> + // - Via Inner's invariants, since we checked `is_init` we're guaranteed `data` is
> + // uninitialized.
> + // - We do not touch `data` at any point after this if we fail.
> + // - This does not move `data`.
> + unsafe { init.__pinned_init(inner.data.as_mut_ptr()) }?;
> +
> + inner.is_init = true;
> + }
> +
> + // INVARIANT: This code is only reachable if `data` was, either previously or just now,
> + // initialized with a valid instance of `T`. Otherwise we've never written to it and `data`
> + // is guaranteed to be uninitialized, fulfilling `Inner`s type invariants regarding
> + // `data` initialization state.
> +
> + // SAFETY: We confirmed `data` is initialized above.
> + let ret = unsafe { self.data(&inner) };
> +
> + match do_init {
> + true => Ok(ret),
> + false => Err(LazyInitError::AlreadyInit(ret)),
> + }
> + }
> +
> + /// Get a reference to the contained object.
> + ///
> + /// Returns [`None`] if this [`LazyInit`] is empty.
> + pub fn as_ref<'a>(&'a self) -> Option<&'a T> {
> + let inner = self.inner.lock();
> +
> + inner.is_init.then(|| {
> + // SAFETY: This closure can only execute if `inner.is_init` is true, guaranteeing
> + // `inner.data` is initialized via `Inner`s type invariants.
> + unsafe { self.data(&inner) }
> + })
> + }
> +
> + /// Release the contents of the [`LazyInit`].
> + ///
> + /// Since this call borrows the [`LazyInit`] mutably, no locking actually needs to take place -
> + /// as parallel lock acquisitions are prevented via the compiler enforcing Rust's borrowing
> + /// rules.
> + ///
> + /// Returns [`true`] if `self` was initialized before calling this function.
> + pub fn reset(self: Pin<&mut Self>) -> bool {
> + let inner = self.project().inner.get_mut_pinned();
> + let was_init = inner.is_init;
> +
> + // SAFETY:
> + // - We drop in place, and therefore do not move `inner`
> + // - The `PinnedDrop` implementation of `Inner` leaves it in a well-defined
> + // state, so we do not need to worry about UB from further usage.
> + unsafe { ptr::drop_in_place(inner.get_unchecked_mut()) };
> +
> + was_init
> + }
> +}
> +
> +#[kunit_tests(rust_lazy_init)]
> +mod tests {
> + use super::*;
> + use core::ops::Deref;
Deref is unused and should be dropped
> + use pin_init::{
> + init_from_closure,
> + stack_pin_init, //
> + };
> +
> + #[derive(Debug)]
> + #[pin_data]
> + struct LazyInitTest {}
> +
> + impl LazyInitTest {
> + fn init_ok() -> impl Init<Self> {
> + init!(LazyInitTest {})
> + }
> +
> + fn init_err(e: Error) -> impl Init<Self, Error> {
> + // SAFETY:
> + // This initializer is intended to fail for testing purposes, and does not actually
> + // initialize any data meaning:
> + //
> + // - We don't need to worry about returning Ok(()).
> + // - We have nothing to actually clean in the slot.
> + // - Since there is no data in the slot, nothing can meaningfully move and we have no
> + // pinning invariants.
> + unsafe { init_from_closure(move |_| Err(e)) }
> + }
> + }
> +
> + fn try_lazy_init(once: &LazyInit<LazyInitTest>) -> Result<(), Error> {
> + // It should start as being empty.
> + assert!((*once).as_ref().is_none());
> +
> + // Try populating it a single time, this should succeed.
> + let res = once.init(LazyInitTest::init_ok()).map_err(|_| EINVAL)?;
> + assert!((*once).as_ref().is_some());
> +
> + // Populating a second time should fail and return the contents.
> + match once.init(LazyInitTest::init_ok()) {
> + Err(LazyInitError::AlreadyInit(data)) => assert!(core::ptr::eq(data, res)),
> + r @ _ => panic!("Calling once.init() twice returned unexpected value: {r:?}"),
> + }
> +
> + // And it should still hold a value
> + assert!((*once).as_ref().is_some());
> +
> + Ok(())
> + }
> +
> + #[test]
> + fn lazy_init() -> Result {
> + stack_pin_init!(let once: LazyInit<LazyInitTest> = new_lazy_init!());
> +
> + try_lazy_init(&once)?;
> + Ok(())
> + }
> +
> + #[test]
> + fn lazy_init_error() -> Result {
> + stack_pin_init!(let once: LazyInit<LazyInitTest> = new_lazy_init!());
> +
> + // Make sure it starts as empty.
> + assert!((*once).as_ref().is_none());
> +
> + // Try populating it with a initializer that fails.
> + match once.init(LazyInitTest::init_err(EJUKEBOX)) {
> + Err(LazyInitError::DuringInit(e)) => assert_eq!(e, EJUKEBOX),
> + r @ _ => panic!(
> + "Calling once.init() with a failing initializer returned an unexpected value: {r:?}"
> + ),
> + }
> +
> + try_lazy_init(&once)?;
> + Ok(())
> + }
> +
> + #[test]
> + fn lazy_init_reset() -> Result {
> + stack_pin_init!(let once: LazyInit<LazyInitTest> = new_lazy_init!());
> +
> + try_lazy_init(&once)?;
> + assert_eq!(once.as_mut().reset(), true);
> + assert_eq!(once.as_mut().reset(), false);
> + try_lazy_init(&once)
> + }
> +}