From: Danilo Krummrich <dakr@kernel.org>
To: tj@kernel.org, jiangshanlai@gmail.com, aliceryhl@google.com,
ojeda@kernel.org, boqun@kernel.org, gary@garyguo.net,
bjorn3_gh@protonmail.com, lossin@kernel.org,
a.hindborg@kernel.org, tmgross@umich.edu,
daniel.almeida@collabora.com, tamird@kernel.org,
acourbot@nvidia.com, work@onurozkan.dev, jhubbard@nvidia.com
Cc: rust-for-linux@vger.kernel.org, linux-kernel@vger.kernel.org,
driver-core@lists.linux.dev, Danilo Krummrich <dakr@kernel.org>
Subject: [PATCH 7/7] rust: workqueue: add ScopedWork for non-'static work items
Date: Tue, 4 Aug 2026 21:52:09 +0200 [thread overview]
Message-ID: <20260804195248.665636-8-dakr@kernel.org> (raw)
In-Reply-To: <20260804195248.665636-1-dakr@kernel.org>
Add ScopedWork<T>, a work item wrapper whose destructor calls
cancel_work_sync(), allowing T to carry non-'static lifetimes. Ownership
of the data is not transferred to the workqueue; instead, the
synchronous cancellation on drop guarantees the work function is not
running when the data is freed.
ScopedWork uses the existing Work/HasWork/WorkItem infrastructure with
NonNull<Self> as WorkItem::Pointer for the callback path, and implements
RawWorkItem for Pin<&ScopedWork<T>> for the enqueue path (requiring T:
Sync for cross-thread shared access safety).
Two enqueue paths are provided:
- Queue::enqueue_scoped() / ScopedQueue::enqueue_scoped() (unsafe):
the caller must ensure the work item is not forgotten.
- ScopedQueue::enqueue() (safe): when the work item's lifetime
satisfies the queue's 'scope bound.
Signed-off-by: Danilo Krummrich <dakr@kernel.org>
---
rust/kernel/workqueue/mod.rs | 43 +++-
rust/kernel/workqueue/scoped_queue.rs | 37 +++-
rust/kernel/workqueue/scoped_work.rs | 290 ++++++++++++++++++++++++++
3 files changed, 348 insertions(+), 22 deletions(-)
create mode 100644 rust/kernel/workqueue/scoped_work.rs
diff --git a/rust/kernel/workqueue/mod.rs b/rust/kernel/workqueue/mod.rs
index 2b87f935712a..11477d42020b 100644
--- a/rust/kernel/workqueue/mod.rs
+++ b/rust/kernel/workqueue/mod.rs
@@ -215,6 +215,13 @@
mod scoped_queue;
pub use self::scoped_queue::ScopedQueue;
+mod scoped_work;
+pub use self::scoped_work::{
+ new_scoped_work,
+ ScopedWork,
+ ScopedWorkItem, //
+};
+
/// Creates a [`Work`] initialiser with the given name and a newly-created lock class.
#[macro_export]
macro_rules! new_work {
@@ -286,20 +293,34 @@ pub unsafe fn from_raw<'a>(ptr: *const bindings::workqueue_struct) -> &'a Queue
pub fn enqueue<W, const ID: u64>(&self, w: W) -> W::EnqueueOutput
where
W: RawWorkItem<ID> + Send + 'static,
+ {
+ // SAFETY: `W: 'static` guarantees the work item remains valid indefinitely,
+ // so the `enqueue_scoped` requirement that the work item stays valid until
+ // the work function runs (or is cancelled) is trivially satisfied.
+ unsafe { self.enqueue_scoped(w) }
+ }
+
+ /// Enqueues a work item that may not be `'static`.
+ ///
+ /// Unlike [`Queue::enqueue`], this does not require the work item to be `'static`.
+ ///
+ /// The work item will be submitted using `WORK_CPU_UNBOUND`.
+ ///
+ /// # Safety
+ ///
+ /// The caller must ensure that the work item's destructor runs before any
+ /// lifetime it captures expires (i.e., the work item must not be forgotten).
+ pub unsafe fn enqueue_scoped<W, const ID: u64>(&self, w: W) -> W::EnqueueOutput
+ where
+ W: RawWorkItem<ID> + Send,
{
let queue_ptr = self.0.get();
- // SAFETY: We only return `false` if the `work_struct` is already in a workqueue. The other
- // `__enqueue` requirements are not relevant since `W` is `Send` and static.
- //
- // The call to `bindings::queue_work_on` will dereference the provided raw pointer, which
- // is ok because `__enqueue` guarantees that the pointer is valid for the duration of this
- // closure.
- //
- // Furthermore, if the C workqueue code accesses the pointer after this call to
- // `__enqueue`, then the work item was successfully enqueued, and `bindings::queue_work_on`
- // will have returned true. In this case, `__enqueue` promises that the raw pointer will
- // stay valid until we call the function pointer in the `work_struct`, so the access is ok.
+ // SAFETY: We only return `false` if the `work_struct` is already in a workqueue. The
+ // caller guarantees the work item remains valid until the work function runs or the item
+ // is cancelled, satisfying the `__enqueue` requirement that the pointer stays valid until
+ // the function pointer in the `work_struct` is called. `W: Send` satisfies the
+ // cross-thread safety requirement.
unsafe {
w.__enqueue(move |work_ptr| {
bindings::queue_work_on(
diff --git a/rust/kernel/workqueue/scoped_queue.rs b/rust/kernel/workqueue/scoped_queue.rs
index d4b9a03fd93c..21928d60a63b 100644
--- a/rust/kernel/workqueue/scoped_queue.rs
+++ b/rust/kernel/workqueue/scoped_queue.rs
@@ -152,20 +152,35 @@ pub unsafe fn new(name: &'static CStr) -> Result<Self> {
pub fn enqueue<W, const ID: u64>(&self, work: W) -> W::EnqueueOutput
where
W: RawWorkItem<ID> + Send + 'scope,
+ {
+ // SAFETY: `W: 'scope` and dropck keep borrowed data alive until this queue
+ // is dropped. The constructor requires that the queue is not leaked and
+ // dropping `inner` drains pending and running work, so the function pointer
+ // is not called after any lifetime in `W` expires.
+ unsafe { self.enqueue_scoped(work) }
+ }
+
+ /// Enqueues a work item without the `'scope` lifetime bound.
+ ///
+ /// Unlike [`ScopedQueue::enqueue`], this does not require `W: 'scope`.
+ ///
+ /// Prefer [`ScopedQueue::enqueue`] when the work item's lifetime satisfies
+ /// `'scope`.
+ ///
+ /// # Safety
+ ///
+ /// The caller must ensure that the work item's destructor runs before any
+ /// lifetime it captures expires (i.e., the work item must not be forgotten).
+ pub unsafe fn enqueue_scoped<W, const ID: u64>(&self, work: W) -> W::EnqueueOutput
+ where
+ W: RawWorkItem<ID> + Send,
{
let queue_ptr = self.inner.0.get();
- // SAFETY:
- // - Closure returns `false` only if `queue_work_on` returns `false`
- // and that means `work_ptr` is already in a workqueue.
- //
- // - `W: 'scope` and dropck keep borrowed data alive until this queue is
- // dropped. The constructor requires that the queue is not leaked and
- // dropping `inner` drains pending and running work so the function
- // pointer is not called after any lifetime in `W` expires.
- //
- // - The last requirement of `__enqueue` is not relevant here because `W`
- // is `Send`.
+ // SAFETY: The caller guarantees the work item remains valid until the work
+ // function runs or the item is cancelled. `W: Send` satisfies the
+ // cross-thread safety requirement. The closure only returns `false` if the
+ // `work_struct` is already in a workqueue.
unsafe {
work.__enqueue(move |work_ptr| {
bindings::queue_work_on(
diff --git a/rust/kernel/workqueue/scoped_work.rs b/rust/kernel/workqueue/scoped_work.rs
new file mode 100644
index 000000000000..bdc834a9a08d
--- /dev/null
+++ b/rust/kernel/workqueue/scoped_work.rs
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Scoped work items.
+//!
+//! Provides [`ScopedWork`] for work items whose inner data may carry non-`'static` lifetimes.
+//!
+//! Unlike [`Work`]-based work items, [`ScopedWork`] cancels work synchronously on drop via
+//! `cancel_work_sync()`, so ownership of the data is not transferred to the workqueue.
+//!
+//! # Examples
+//!
+//! Enqueue on the system workqueue (unsafe, caller must not forget the work):
+//!
+//! ```
+//! # use kernel::time::{Delta, delay::fsleep};
+//! use kernel::workqueue::{
+//! self,
+//! new_scoped_work,
+//! ScopedWork,
+//! ScopedWorkItem,
+//! };
+//!
+//! struct MyWork {
+//! value: u32,
+//! }
+//!
+//! impl ScopedWorkItem for MyWork {
+//! fn run(self: Pin<&Self>) {
+//! pr_info!("value = {}\n", self.value);
+//! }
+//! }
+//!
+//! let work = KBox::pin_init(
+//! new_scoped_work!("MyWork", MyWork { value: 42 }),
+//! GFP_KERNEL,
+//! )?;
+//!
+//! // SAFETY: `work` is not forgotten.
+//! unsafe { workqueue::system_dfl().enqueue_scoped(&*work) };
+//! # // Allow the worker thread to pick up and execute the work.
+//! # fsleep(Delta::from_millis(100));
+//! # Ok::<(), Error>(())
+//! ```
+//!
+//! Enqueue on a [`ScopedQueue`] using the safe path (work outlives the queue):
+//!
+//! ```
+//! use kernel::workqueue::{
+//! new_scoped_work,
+//! ScopedQueue,
+//! ScopedWork,
+//! ScopedWorkItem,
+//! };
+//!
+//! struct MyWork {
+//! value: u32,
+//! }
+//!
+//! impl ScopedWorkItem for MyWork {
+//! fn run(self: Pin<&Self>) {
+//! pr_info!("value = {}\n", self.value);
+//! }
+//! }
+//!
+//! let work = KBox::pin_init(
+//! new_scoped_work!("MyWork", MyWork { value: 42 }),
+//! GFP_KERNEL,
+//! )?;
+//!
+//! // SAFETY: The queue is not forgotten.
+//! let queue = unsafe { ScopedQueue::new(c"example_wq")? };
+//!
+//! // Safe since `work` outlives `queue`.
+//! queue.enqueue(&*work);
+//! # Ok::<(), Error>(())
+//! ```
+
+use super::{
+ impl_has_work,
+ HasWork,
+ RawWorkItem,
+ Work,
+ WorkItem,
+ WorkItemPointer, //
+};
+
+use crate::{
+ bindings,
+ prelude::*,
+ sync::LockClassKey, //
+};
+
+use core::{
+ ops::Deref,
+ ptr::NonNull, //
+};
+
+/// Trait for types that can be used as scoped work items.
+///
+/// Implementers define the work function that executes when the item is dequeued by a workqueue
+/// thread. The callback receives a shared pinned reference; mutation should use interior mutability
+/// (e.g., [`Mutex`](crate::sync::Mutex)).
+pub trait ScopedWorkItem {
+ /// Called when the work item is executed.
+ fn run(self: Pin<&Self>);
+}
+
+// SAFETY: The `run` callback uses the `work_struct` pointer to recover a pointer to `ScopedWork<T>`
+// via `HasWork`, wraps it in `NonNull`, and calls `WorkItem::run`.
+unsafe impl<T: ScopedWorkItem, const ID: u64> WorkItemPointer<ID> for NonNull<ScopedWork<T>>
+where
+ ScopedWork<T>: WorkItem<ID, Pointer = Self>,
+ ScopedWork<T>: HasWork<ScopedWork<T>, ID>,
+{
+ unsafe extern "C" fn run(ptr: *mut bindings::work_struct) {
+ let ptr = ptr.cast::<Work<ScopedWork<T>, ID>>();
+
+ // SAFETY: The `work_struct` is embedded in `ScopedWork<T>` via `HasWork`.
+ let ptr = unsafe { <ScopedWork<T> as HasWork<ScopedWork<T>, ID>>::work_container_of(ptr) };
+
+ // SAFETY: `work_container_of` returns a valid, non-null pointer.
+ let nn = unsafe { NonNull::new_unchecked(ptr) };
+
+ <ScopedWork<T> as WorkItem<ID>>::run(nn);
+ }
+}
+
+// Required because `WorkItemPointer<ID>: RawWorkItem<ID>` is a supertrait bound. This `__enqueue`
+// is never called; the enqueue path goes through the `RawWorkItem` impl for `&ScopedWork<T>`
+// instead.
+//
+// SAFETY: `__enqueue` is unreachable.
+unsafe impl<T: ScopedWorkItem, const ID: u64> RawWorkItem<ID> for NonNull<ScopedWork<T>>
+where
+ ScopedWork<T>: HasWork<ScopedWork<T>, ID>,
+{
+ type EnqueueOutput = bool;
+
+ unsafe fn __enqueue<F>(self, _queue_work_on: F) -> Self::EnqueueOutput
+ where
+ F: FnOnce(*mut bindings::work_struct) -> bool,
+ {
+ unreachable!()
+ }
+}
+
+// SAFETY: `&ScopedWork<T>` points to a valid, pinned `ScopedWork` with a valid `work_struct`.
+// The pointer remains valid until `cancel_work_sync()` completes.
+unsafe impl<'a, T: ScopedWorkItem + Sync, const ID: u64> RawWorkItem<ID> for &'a ScopedWork<T>
+where
+ ScopedWork<T>: HasWork<ScopedWork<T>, ID>,
+{
+ type EnqueueOutput = bool;
+
+ unsafe fn __enqueue<F>(self, queue_work_on: F) -> Self::EnqueueOutput
+ where
+ F: FnOnce(*mut bindings::work_struct) -> bool,
+ {
+ let self_ptr = core::ptr::from_ref(self);
+
+ // SAFETY: `self_ptr` points to a valid `ScopedWork` with a `Work` field.
+ let work_ptr = unsafe {
+ <ScopedWork<T> as HasWork<ScopedWork<T>, ID>>::raw_get_work(self_ptr.cast_mut())
+ };
+
+ // SAFETY: `work_ptr` points to a valid `Work`.
+ let work_ptr = unsafe { Work::raw_get(work_ptr) };
+
+ queue_work_on(work_ptr)
+ }
+}
+
+/// A scoped work item that cancels synchronously on drop.
+///
+/// `ScopedWork<T>` contains both a `work_struct` and the user data `T`. Its destructor calls
+/// `cancel_work_sync()`, guaranteeing the work function is not running when the data is dropped.
+///
+/// This allows `T` to carry non-`'static` lifetimes.
+///
+/// Construct via [`new_scoped_work!`] which returns an `impl PinInit` suitable for embedding
+/// in-place inside other pinned structs.
+#[pin_data(PinnedDrop)]
+pub struct ScopedWork<T: ScopedWorkItem> {
+ #[pin]
+ work: Work<Self>,
+ #[pin]
+ data: T,
+}
+
+impl_has_work! {
+ impl{T: ScopedWorkItem} HasWork<ScopedWork<T>> for ScopedWork<T> { self.work }
+}
+
+impl<T: ScopedWorkItem> WorkItem for ScopedWork<T> {
+ type Pointer = NonNull<Self>;
+
+ fn run(this: NonNull<Self>) {
+ // SAFETY: `this` points to a valid, pinned `ScopedWork`. `cancel_work_sync()` in
+ // `PinnedDrop` prevents use-after-drop.
+ let data = unsafe { Pin::new_unchecked(&(*this.as_ptr()).data) };
+
+ T::run(data);
+ }
+}
+
+impl<T: ScopedWorkItem> Deref for ScopedWork<T> {
+ type Target = T;
+
+ fn deref(&self) -> &T {
+ &self.data
+ }
+}
+
+impl<T: ScopedWorkItem> ScopedWork<T> {
+ /// Creates a pin-initializer for a new scoped work item.
+ ///
+ /// Use [`new_scoped_work!`] to automatically provide the lock class key.
+ pub fn new<E>(
+ name: &'static CStr,
+ key: Pin<&'static LockClassKey>,
+ init: impl PinInit<T, E>,
+ ) -> impl PinInit<Self, Error>
+ where
+ Error: From<E>,
+ {
+ try_pin_init!(Self {
+ work <- Work::new(name, key),
+ data <- init,
+ })
+ }
+
+ /// Synchronously cancels pending work and waits for any running work function to complete.
+ ///
+ /// Returns `true` if the work was pending, `false` otherwise.
+ pub fn cancel_work_sync(&self) -> bool {
+ self.work.cancel_work_sync()
+ }
+}
+
+#[pinned_drop]
+impl<T: ScopedWorkItem> PinnedDrop for ScopedWork<T> {
+ fn drop(self: Pin<&mut Self>) {
+ self.cancel_work_sync();
+ }
+}
+
+/// Creates a [`ScopedWork`] pin-initializer with a new lock class.
+///
+/// # Examples
+///
+/// ```
+/// use kernel::workqueue::{
+/// new_scoped_work,
+/// ScopedWork,
+/// ScopedWorkItem,
+/// };
+///
+/// struct MyWork {
+/// value: u32,
+/// }
+///
+/// impl ScopedWorkItem for MyWork {
+/// fn run(self: Pin<&Self>) {
+/// pr_info!("value = {}\n", self.value);
+/// }
+/// }
+///
+/// #[pin_data]
+/// struct MyData {
+/// #[pin]
+/// work: ScopedWork<MyWork>,
+/// }
+///
+/// fn init_data() -> impl PinInit<MyData, Error> {
+/// try_pin_init!(MyData {
+/// work <- new_scoped_work!("MyWork", MyWork { value: 7 }),
+/// })
+/// }
+/// ```
+#[macro_export]
+macro_rules! new_scoped_work {
+ ($name:literal, $init:expr) => {
+ $crate::workqueue::ScopedWork::new(
+ $crate::c_str!($name),
+ $crate::static_lock_class!(),
+ $init,
+ )
+ };
+}
+pub use new_scoped_work;
--
2.55.0
prev parent reply other threads:[~2026-08-04 19:53 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-04 19:52 [PATCH 0/7] workqueue OwnedQueue, ScopedQueue and ScopedWork Danilo Krummrich
2026-08-04 19:52 ` [PATCH 1/7] rust: workqueue: replace deprecated system_wq with system_{percpu,dfl}_wq Danilo Krummrich
2026-08-04 19:52 ` [PATCH 2/7] rust: workqueue: restrict delayed work to global wqs Danilo Krummrich
2026-08-04 19:52 ` [PATCH 3/7] rust: workqueue: create workqueue subdirectory Danilo Krummrich
2026-08-04 19:52 ` [PATCH 4/7] rust: workqueue: add creation of workqueues Danilo Krummrich
2026-08-04 19:52 ` [PATCH 5/7] rust: workqueue: add ScopedQueue for lifetime bound items Danilo Krummrich
2026-08-04 19:52 ` [PATCH 6/7] rust: workqueue: add Work::cancel_work_sync() Danilo Krummrich
2026-08-04 19:52 ` Danilo Krummrich [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260804195248.665636-8-dakr@kernel.org \
--to=dakr@kernel.org \
--cc=a.hindborg@kernel.org \
--cc=acourbot@nvidia.com \
--cc=aliceryhl@google.com \
--cc=bjorn3_gh@protonmail.com \
--cc=boqun@kernel.org \
--cc=daniel.almeida@collabora.com \
--cc=driver-core@lists.linux.dev \
--cc=gary@garyguo.net \
--cc=jhubbard@nvidia.com \
--cc=jiangshanlai@gmail.com \
--cc=linux-kernel@vger.kernel.org \
--cc=lossin@kernel.org \
--cc=ojeda@kernel.org \
--cc=rust-for-linux@vger.kernel.org \
--cc=tamird@kernel.org \
--cc=tj@kernel.org \
--cc=tmgross@umich.edu \
--cc=work@onurozkan.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox