[PATCH RFC 3/3] rust: snd: Add active rawmidi_substream helper

From: Tristan de Cacqueray

Date: Sun Oct 11 2026 - 09:03:52 EST


Add helper to share the substream across execution contexts in Rust.

Signed-off-by: Tristan de Cacqueray <tristan@xxxxxxxxxxx>
---
rust/kernel/snd.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
samples/rust/rust_midi_device.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 154 insertions(+), 6 deletions(-)

diff --git a/rust/kernel/snd.rs b/rust/kernel/snd.rs
index 7285575c6257..d9df5af155b9 100644
--- a/rust/kernel/snd.rs
+++ b/rust/kernel/snd.rs
@@ -7,13 +7,13 @@
//!
//! C header: [`include/sound/core.h`](srctree/include/sound/core.h)

-use core::marker::PhantomData;
+use core::{marker::PhantomData, ptr::NonNull};
use kernel::{
bindings,
device::Device,
error::{to_result, Result},
prelude::*,
- sync::Arc,
+ sync::{Arc, SpinLockIrq},
types::ForeignOwnable,
};

@@ -338,3 +338,66 @@ pub fn set_name(&mut self, name: &CStr) -> Result<()> {
copy_to_c_buf(&mut rmidi.name, name)
}
}
+
+/// A thread-safe helper for sharing ALSA substream across execution contexts.
+///
+/// # Safety
+///
+/// Implementations MUST clear any [`ActiveMidiSubstream`] before `close` returns.
+/// TODO: enforce in framework.
+#[pin_data(PinnedDrop)]
+pub struct ActiveMidiSubstream {
+ #[pin]
+ ptr: SpinLockIrq<Option<NonNull<bindings::snd_rawmidi_substream>>>,
+}
+
+impl ActiveMidiSubstream {
+ /// Creates a new initialiser for an empty substream.
+ pub fn new() -> impl PinInit<Self> {
+ pin_init!(Self {
+ ptr <- kernel::sync::new_spinlock_irq!(None),
+ })
+ }
+
+ /// Stores the given substream reference as the active target.
+ pub fn store(&self, substream: &RawMidiSubstream) {
+ let mut guard = self.ptr.lock();
+ *guard = NonNull::new(substream.ptr);
+ }
+
+ /// Clears the tracked substream reference.
+ pub fn clear(&self) {
+ let mut guard = self.ptr.lock();
+ *guard = None;
+ }
+
+ /// Executes a closure while holding the spinlock with a temporary reference
+ /// to the active substream, if present.
+ ///
+ /// Returns `None` if no stream is currently active.
+ pub fn with_active<F, R>(&self, f: F) -> Option<R>
+ where
+ F: FnOnce(&RawMidiSubstream) -> R,
+ {
+ let guard = self.ptr.lock();
+ let nn_ptr = (*guard)?;
+ // SAFETY: The raw ptr can only be stored using a valid rawmidi_substream.
+ let safe_substream = unsafe { RawMidiSubstream::from_raw(nn_ptr.as_ptr()) };
+
+ Some(f(&safe_substream))
+ }
+}
+
+#[pinned_drop]
+impl PinnedDrop for ActiveMidiSubstream {
+ fn drop(self: Pin<&mut Self>) {
+ self.clear()
+ }
+}
+
+// SAFETY: snd_rawmidi_substream is accessed only under the SpinLockIrq and it is
+// only ever valid between the ALSA `open` and `close` callbacks, during
+// which the ALSA core guarantees the substream is alive (see the type docs).
+unsafe impl Send for ActiveMidiSubstream {}
+// SAFETY: snd_rawmidi_substream is accessed only under the SpinLockIrq. See above.
+unsafe impl Sync for ActiveMidiSubstream {}
diff --git a/samples/rust/rust_midi_device.rs b/samples/rust/rust_midi_device.rs
index 1465ba9544d6..9c6a03baa42e 100644
--- a/samples/rust/rust_midi_device.rs
+++ b/samples/rust/rust_midi_device.rs
@@ -5,19 +5,88 @@
//! This driver demonstrates usage of the kernel::snd subsystem:
//! - Create a rawmidi device
//! - Print received event
+//! - Periodically send note events
+//!
+//! Its goal is to simulate a real device driver usage.

+use core::sync::atomic::{AtomicBool, Ordering};
use kernel::{
faux,
prelude::*,
snd,
sync::Arc,
+ workqueue,
+ workqueue::{DelayedWork, WorkItem},
};

-struct RustMidiDevice;
+// The internal state data to perform a hardware simulation.
+#[pin_data]
+struct RustMidiDevice {
+ // toggle sent note on and off.
+ note_playing: AtomicBool,
+ #[pin]
+ // substream to send note.
+ input_substream: snd::ActiveMidiSubstream,
+ #[pin]
+ // delayed work to send note periodically.
+ timer_work: DelayedWork<RustMidiDevice>,
+}
+
+kernel::impl_has_delayed_work! {
+ impl HasDelayedWork<Self> for RustMidiDevice { self.timer_work }
+}
+
+impl RustMidiDevice {
+ // Initialize the simulation internal state.
+ fn new() -> Result<Arc<Self>> {
+ Arc::pin_init(
+ pin_init!(RustMidiDevice {
+ note_playing: AtomicBool::new(false),
+ input_substream <- snd::ActiveMidiSubstream::new(),
+ timer_work <- kernel::new_delayed_work!("RustMidiDevice::timer_work"),
+ }),
+ GFP_KERNEL,
+ )
+ }
+}
+
+impl WorkItem for RustMidiDevice {
+ type Pointer = Arc<Self>;
+
+ // Send a note event, as-if coming from external interrupt.
+ fn run(this: Self::Pointer) {
+ this.input_substream.with_active(|substream| {
+ let playing = this.note_playing.fetch_not(Ordering::Relaxed);
+ let velocity = if playing { 0x0 } else { 0x7F };
+ let event = [0x90, 0x42, velocity];
+ match substream.receive(&event) {
+ Ok(count) => pr_info!("worker submitted {} ({:?})", count, event),
+ Err(err) => pr_err!("worker submit failed: {:?}", err),
+ };
+ });
+ pr_debug!("Periodic worker completed!");
+
+ // Re-enqueue continuously every 500 jiffies
+ let _ = workqueue::system().enqueue_delayed(this, 500);
+ }
+}

+// Implement midi input ops: store the substream for external context.
impl snd::RawMidiInput for RustMidiDevice {
- fn trigger(&self, _substream: &snd::RawMidiSubstream, up: bool) {
+ fn close(&self, _substream: &snd::RawMidiSubstream) -> Result<()> {
+ pr_debug!("input close!\n");
+ // note: maybe input_substream should be managed by the binding,
+ // otherwise the user may forget to call clear when the device is closed.
+ self.input_substream.clear();
+ Ok(())
+ }
+ fn trigger(&self, substream: &snd::RawMidiSubstream, up: bool) {
pr_debug!("input trigger up:{up}!\n");
+ if up {
+ self.input_substream.store(substream);
+ } else {
+ self.input_substream.clear();
+ }
}
}

@@ -37,7 +106,7 @@ fn trigger(&self, substream: &snd::RawMidiSubstream, up: bool) {

impl kernel::Module for RustMidiDeviceModule {
fn init(_module: &'static ThisModule) -> Result<Self> {
- let state = Arc::new(RustMidiDevice, GFP_KERNEL)?;
+ let state = RustMidiDevice::new()?;

pr_debug!("Initializing sound card\n");
let reg = faux::Registration::new(c"rust-midi-device", None)?;
@@ -57,14 +126,30 @@ fn init(_module: &'static ThisModule) -> Result<Self> {
// Register the sndcard to ALSA.
card.register()?;

- Ok(Self { _reg: reg })
+ pr_debug!("Kick off the background worker\n");
+ let _ = workqueue::system().enqueue_delayed(state.clone(), 0);
+
+ Ok(Self { _reg: reg, state })
}
}

struct RustMidiDeviceModule {
+ state: Arc<RustMidiDevice>,
_reg: faux::Registration,
}

+impl Drop for RustMidiDeviceModule {
+ fn drop(&mut self) {
+ // Safety: the timer_work is a valid delayed work. It seems necessary to cancel the work
+ // to avoid a panic when unloading the module. This is just for simulation purpose.
+ // TODO: add a safe cancel helper to kernel::workqueue and remove unsafe bindings here
+ unsafe {
+ let dwork_ptr = core::ptr::from_ref(&self.state.timer_work) as *mut _;
+ kernel::bindings::cancel_delayed_work_sync(dwork_ptr);
+ }
+ }
+}
+
module! {
type: RustMidiDeviceModule,
name: "rust_midi_device",

--
2.56.0