mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH RFC 0/3] rust: snd: Introduce experimental ALSA rawmidi abstractions
@ 2026-10-11 13:02 Tristan de Cacqueray
  2026-10-11 13:02 ` [PATCH RFC 1/3] rust: snd: Add initial snd_card binding Tristan de Cacqueray
                   ` (3 more replies)
  0 siblings, 4 replies; 5+ messages in thread
From: Tristan de Cacqueray @ 2026-10-11 13:02 UTC (permalink / raw)
  To: Takashi Iwai, Miguel Ojeda
  Cc: linux-sound, rust-for-linux, linux-kernel, Tristan de Cacqueray

Hi Takashi and the Rust for Linux maintainers.

I just noticed your mail about an early Rust prototype for the sound
subsystem right before I was about to submit this RFC!
  https://lore.kernel.org/alsa-devel/878q48579z.wl-tiwai@suse.de/

Nevertheless, please allow me to share what I had in mind through this
small proof of concept for the `rawmidi` API. I am looking for feedback on
the scope, coordination, and the ownership model rather than a detailed
review for merging.

My use case is a new hardware controller using a custom serial protocol.
While developing its Rust driver, I needed an interface to ALSA rawmidi.
I'm particularly interested in handling rawmidi substream safely to avoid
issues like the one fixed by
  221253723dc5 ("ALSA: bcd2000: Fix race between rawmidi and disconnect")

So, I wrote some bindings to see how that would look like. This series adds
experimental snd_card and snd_rawmidi wrappers, input/output callback
traits, and a sample rawmidi faux device using delayed work to simulate
incoming MIDI data.

This is not merge-ready. In particular, the prototype does not yet enforce
the devres lifetime of the card or the lifetime of its rawmidi wrapper. The
substream helper also relies on the driver clearing it before close
returns.

Here are my questions:

  1. Does this rawmidi experiment overlaps with your prototype?
     If so, I would be happy to coordinate or adapt this experiment to it.
  2. Is rawmidi a useful initial target for ALSA Rust abstraction?
  3. Does your prototype mirror the C API 1:1, or for example, does it hide
     the substream lifecycle? That's the design choice I'm least sure about.

Testing:
  - build with CONFIG_RUST=y and CONFIG_SAMPLE_RUST_MIDI_DEVICE=m.
  - Run amidi client to receive events from Rust:
      amidi -l # to get the hw number
      amidi -d -S "424344" -p hw:1,0
  - Modprobe/rmmod works, but I haven't reproduced the race between
    rawmidi substream and disconnect.

Base: rust-next at 2dee3d3adcad

This is my first contribution to the kernel, suggestions on routing or
RFC scope are welcome.

Thanks!
-Tristan

Signed-off-by: Tristan de Cacqueray <tristan@midirus.com>
---
Tristan de Cacqueray (3):
      rust: snd: Add initial snd_card binding
      rust: snd: Add snd_rawmidi binding
      rust: snd: Add active rawmidi_substream helper

 rust/bindings/bindings_helper.h  |   2 +
 rust/kernel/lib.rs               |   2 +
 rust/kernel/snd.rs               | 403 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 samples/rust/Kconfig             |  11 +++
 samples/rust/Makefile            |   1 +
 samples/rust/rust_midi_device.rs | 159 ++++++++++++++++++++++++++++++++++++++++
 6 files changed, 578 insertions(+)
---
base-commit: 2dee3d3adcadcc57d62ba6608cd02f50b0c10264
change-id: 20261011-rust-snd-c5e596867624

Best regards,
--  
Tristan de Cacqueray <tristan@midirus.com>



^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH RFC 1/3] rust: snd: Add initial snd_card binding
  2026-10-11 13:02 [PATCH RFC 0/3] rust: snd: Introduce experimental ALSA rawmidi abstractions Tristan de Cacqueray
@ 2026-10-11 13:02 ` Tristan de Cacqueray
  2026-10-11 13:02 ` [PATCH RFC 2/3] rust: snd: Add snd_rawmidi binding Tristan de Cacqueray
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 5+ messages in thread
From: Tristan de Cacqueray @ 2026-10-11 13:02 UTC (permalink / raw)
  To: Takashi Iwai, Miguel Ojeda
  Cc: linux-sound, rust-for-linux, linux-kernel, Tristan de Cacqueray

Add minimal abstraction to create a snd_card in Rust.

Signed-off-by: Tristan de Cacqueray <tristan@midirus.com>
---
 rust/bindings/bindings_helper.h  |  1 +
 rust/kernel/lib.rs               |  2 ++
 rust/kernel/snd.rs               | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 samples/rust/Kconfig             | 11 +++++++++++
 samples/rust/Makefile            |  1 +
 samples/rust/rust_midi_device.rs | 41 +++++++++++++++++++++++++++++++++++++++++
 6 files changed, 145 insertions(+)

diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h
index c706dda5b5ca..c624f073db04 100644
--- a/rust/bindings/bindings_helper.h
+++ b/rust/bindings/bindings_helper.h
@@ -97,6 +97,7 @@
 #include <linux/xarray.h>
 #include <net/genetlink.h>
 #include <net/netlink.h>
+#include <sound/core.h>
 #include <trace/events/rust_sample.h>
 
 /*
diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
index 7225abc64084..974b3e5ce7ac 100644
--- a/rust/kernel/lib.rs
+++ b/rust/kernel/lib.rs
@@ -130,6 +130,8 @@
 #[cfg(CONFIG_RUST_SERIAL_DEV_BUS_ABSTRACTIONS)]
 pub mod serdev;
 pub mod sizes;
+#[cfg(CONFIG_SND)]
+pub mod snd;
 #[cfg(CONFIG_SOC_BUS)]
 pub mod soc;
 #[doc(hidden)]
diff --git a/rust/kernel/snd.rs b/rust/kernel/snd.rs
new file mode 100644
index 000000000000..aaffe942b360
--- /dev/null
+++ b/rust/kernel/snd.rs
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (C) 2026 Tristan de Cacqueray
+
+//! Abstractions for the snd subsystem.
+//!
+//! This module provides bindings for working with ALSA in kernel modules.
+//!
+//! C header: [`include/sound/core.h`](srctree/include/sound/core.h)
+
+use kernel::{
+    bindings,
+    device::Device,
+    error::{Result},
+    prelude::*,
+};
+
+/// A sound card instance.
+pub struct SoundCard {
+    ptr: *mut bindings::snd_card,
+}
+
+impl SoundCard {
+    /// Creates a new sound card bound to a device lifecycle
+    pub fn new(dev: &Device, id: &CStr, module: &'static kernel::ThisModule) -> Result<Self> {
+        let mut card_ptr: *mut bindings::snd_card = core::ptr::null_mut();
+
+        // SAFETY: C helper copies the arguments.
+        let ret = unsafe {
+            bindings::snd_devm_card_new(
+                dev.as_raw(),
+                -1,
+                id.as_char_ptr(),
+                module.as_ptr(),
+                0,
+                &mut card_ptr,
+            )
+        };
+
+        if ret < 0 {
+            return Err(Error::from_errno(ret));
+        }
+
+        Ok(Self { ptr: card_ptr })
+    }
+
+    /// Sets the driver field of the sound card.
+    pub fn set_driver(&mut self, name: &CStr) -> Result<()> {
+        // SAFETY: `self.ptr` is valid and points to an initialized `snd_card`.
+        let card = unsafe { &mut *self.ptr };
+        copy_to_c_buf(&mut card.driver, name)
+    }
+
+    /// Sets the shortname field of the sound card.
+    pub fn set_shortname(&mut self, name: &CStr) -> Result<()> {
+        // SAFETY: `self.ptr` is valid and points to an initialized `snd_card`.
+        let card = unsafe { &mut *self.ptr };
+        copy_to_c_buf(&mut card.shortname, name)
+    }
+
+    /// Triggers registration of the card components to the user-space ALSA layers
+    pub fn register(self) -> Result<RegisteredSoundCard> {
+        // SAFETY: `self.ptr` is valid and points to an initialized `snd_card`.
+        let ret = unsafe { bindings::snd_card_register(self.ptr) };
+        if ret < 0 {
+            return Err(Error::from_errno(ret));
+        }
+        Ok(RegisteredSoundCard { _inner: self })
+    }
+}
+
+/// Helper to copy `src` (including its NUL) into the fixed-size C buffer `dest`.
+/// Returns `EINVAL` if `src` does not fit.
+fn copy_to_c_buf(dest: &mut [ffi::c_char], src: &CStr) -> Result<()> {
+    let bytes = src.to_bytes_with_nul();
+    if bytes.len() > dest.len() {
+        return Err(EINVAL);
+    }
+    let (used, rest) = dest.split_at_mut(bytes.len());
+    for (d, &b) in used.iter_mut().zip(bytes) {
+        *d = b as ffi::c_char;
+    }
+    rest.fill(0);
+    Ok(())
+}
+
+/// A sound card that is already registered.
+pub struct RegisteredSoundCard {
+    _inner: SoundCard,
+}
diff --git a/samples/rust/Kconfig b/samples/rust/Kconfig
index 31d62533ef25..1cf8cf18cd2d 100644
--- a/samples/rust/Kconfig
+++ b/samples/rust/Kconfig
@@ -31,6 +31,17 @@ config SAMPLE_RUST_MINIMAL
 
 	  If unsure, say N.
 
+config SAMPLE_RUST_MIDI_DEVICE
+	tristate "MIDI device"
+	depends on SND_RAWMIDI
+	help
+	  This option builds the Rust MIDI device.
+
+	  To compile this as a module, choose M here:
+	  the module will be called rust_midi_device.
+
+	  If unsure, say N.
+
 config SAMPLE_RUST_MISC_DEVICE
 	tristate "Misc device"
 	help
diff --git a/samples/rust/Makefile b/samples/rust/Makefile
index b986b681cde5..170b7e3e5590 100644
--- a/samples/rust/Makefile
+++ b/samples/rust/Makefile
@@ -2,6 +2,7 @@
 ccflags-y += -I$(src)				# needed for trace events
 
 obj-$(CONFIG_SAMPLE_RUST_MINIMAL)		+= rust_minimal.o
+obj-$(CONFIG_SAMPLE_RUST_MIDI_DEVICE)		+= rust_midi_device.o
 obj-$(CONFIG_SAMPLE_RUST_MISC_DEVICE)		+= rust_misc_device.o
 obj-$(CONFIG_SAMPLE_RUST_PRINT)			+= rust_print.o
 obj-$(CONFIG_SAMPLE_RUST_DEBUGFS)		+= rust_debugfs.o
diff --git a/samples/rust/rust_midi_device.rs b/samples/rust/rust_midi_device.rs
new file mode 100644
index 000000000000..5561a0437b59
--- /dev/null
+++ b/samples/rust/rust_midi_device.rs
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Rust MIDI minimal sample.
+
+use kernel::{
+    faux,
+    prelude::*,
+    snd,
+};
+
+impl kernel::Module for RustMidiDeviceModule {
+    fn init(_module: &'static ThisModule) -> Result<Self> {
+        pr_debug!("Initializing sound card\n");
+        let reg = faux::Registration::new(c"rust-midi-device", None)?;
+        let fdev = reg.as_ref();
+        let mut card = snd::SoundCard::new(
+            fdev.as_ref(),
+            c"RustMIDI",
+            kernel::module::this_module::<RustMidiDeviceModule>(),
+        )?;
+        card.set_driver(c"rust-midi")?;
+        card.set_shortname(c"RustMIDI")?;
+
+        // Register the sndcard to ALSA.
+        card.register()?;
+
+        Ok(Self { _reg: reg })
+    }
+}
+
+struct RustMidiDeviceModule {
+    _reg: faux::Registration,
+}
+
+module! {
+    type: RustMidiDeviceModule,
+    name: "rust_midi_device",
+    authors: ["Rust for Linux Contributors"],
+    description: "Rust MIDI device sample",
+    license: "GPL",
+}

-- 
2.56.0



^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH RFC 2/3] rust: snd: Add snd_rawmidi binding
  2026-10-11 13:02 [PATCH RFC 0/3] rust: snd: Introduce experimental ALSA rawmidi abstractions Tristan de Cacqueray
  2026-10-11 13:02 ` [PATCH RFC 1/3] rust: snd: Add initial snd_card binding Tristan de Cacqueray
@ 2026-10-11 13:02 ` Tristan de Cacqueray
  2026-10-11 13:02 ` [PATCH RFC 3/3] rust: snd: Add active rawmidi_substream helper Tristan de Cacqueray
  2026-10-12 12:23 ` [PATCH RFC 0/3] rust: snd: Introduce experimental ALSA rawmidi abstractions Takashi Iwai
  3 siblings, 0 replies; 5+ messages in thread
From: Tristan de Cacqueray @ 2026-10-11 13:02 UTC (permalink / raw)
  To: Takashi Iwai, Miguel Ojeda
  Cc: linux-sound, rust-for-linux, linux-kernel, Tristan de Cacqueray

Add minimal abstraction to create a snd_rawmidi along with
the input/output ops in Rust.

Signed-off-by: Tristan de Cacqueray <tristan@midirus.com>
---
 rust/bindings/bindings_helper.h  |   1 +
 rust/kernel/snd.rs               | 253 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 samples/rust/rust_midi_device.rs |  33 +++++++++++++
 3 files changed, 286 insertions(+), 1 deletion(-)

diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h
index c624f073db04..71999651aebf 100644
--- a/rust/bindings/bindings_helper.h
+++ b/rust/bindings/bindings_helper.h
@@ -98,6 +98,7 @@
 #include <net/genetlink.h>
 #include <net/netlink.h>
 #include <sound/core.h>
+#include <sound/rawmidi.h>
 #include <trace/events/rust_sample.h>
 
 /*
diff --git a/rust/kernel/snd.rs b/rust/kernel/snd.rs
index aaffe942b360..7285575c6257 100644
--- a/rust/kernel/snd.rs
+++ b/rust/kernel/snd.rs
@@ -7,11 +7,14 @@
 //!
 //! C header: [`include/sound/core.h`](srctree/include/sound/core.h)
 
+use core::marker::PhantomData;
 use kernel::{
     bindings,
     device::Device,
-    error::{Result},
+    error::{to_result, Result},
     prelude::*,
+    sync::Arc,
+    types::ForeignOwnable,
 };
 
 /// A sound card instance.
@@ -87,3 +90,251 @@ fn copy_to_c_buf(dest: &mut [ffi::c_char], src: &CStr) -> Result<()> {
 pub struct RegisteredSoundCard {
     _inner: SoundCard,
 }
+
+/// A rawmidi substream instance. Must not be shared outside of the RawMidiInput/Output operations.
+pub struct RawMidiSubstream {
+    ptr: *mut bindings::snd_rawmidi_substream,
+}
+
+impl RawMidiSubstream {
+    /// # Safety
+    ///
+    /// The caller must ensure that `ptr` is a valid pointer provided by the ALSA core.
+    unsafe fn from_raw(ptr: *mut bindings::snd_rawmidi_substream) -> Self {
+        Self { ptr }
+    }
+
+    /// Copies data from hardware to the ALSA core input ring buffer.
+    ///
+    /// Returns the number of bytes successfully copied
+    pub fn receive(&self, buffer: &[u8]) -> Result<usize> {
+        let count: c_int = buffer.len().try_into().map_err(|_| EINVAL)?;
+        // SAFETY: `self.ptr` is valid and the buffer length matched the buffer ptr.
+        let ret = unsafe { bindings::snd_rawmidi_receive(self.ptr, buffer.as_ptr(), count) };
+        if ret < 0 {
+            Err(Error::from_errno(ret))
+        } else {
+            Ok(ret as usize)
+        }
+    }
+
+    /// Retrieves data from the ALSA core output ring buffer to be transmitted to hardware.
+    ///
+    /// Returns the number of bytes successfully copied into `buffer`.
+    pub fn transmit(&self, buffer: &mut [u8]) -> Result<usize> {
+        let count: c_int = buffer.len().try_into().map_err(|_| EINVAL)?;
+        // SAFETY: `self.ptr` is valid between `open` and `close`.
+        // C writes at most `count` bytes into the `buffer`.
+        let ret = unsafe { bindings::snd_rawmidi_transmit(self.ptr, buffer.as_mut_ptr(), count) };
+        if ret < 0 {
+            Err(Error::from_errno(ret))
+        } else {
+            Ok(ret as usize)
+        }
+    }
+}
+
+/// Operations for a MIDI input stream.
+pub trait RawMidiInput: Send + Sync {
+    /// Call when the input substream is opened.
+    fn open(&self, _substream: &RawMidiSubstream) -> Result<()> {
+        Ok(())
+    }
+    /// Called when the input substream is closed.
+    fn close(&self, _substream: &RawMidiSubstream) -> Result<()> {
+        Ok(())
+    }
+    /// Called to start or stop reading input data.
+    fn trigger(&self, _substream: &RawMidiSubstream, _up: bool);
+}
+
+/// Operations for a MIDI output stream.
+pub trait RawMidiOutput: Send + Sync {
+    /// Called when the output substream is opened.
+    fn open(&self, _substream: &RawMidiSubstream) -> Result<()> {
+        Ok(())
+    }
+    /// Called when the output substream is closed.
+    fn close(&self, _substream: &RawMidiSubstream) -> Result<()> {
+        Ok(())
+    }
+    /// Called to start or stop transmitting output data.
+    fn trigger(&self, _substream: &RawMidiSubstream, _up: bool);
+}
+
+struct RawMidiOps<T>(PhantomData<T>);
+impl<T: 'static> RawMidiOps<T> {
+    /// # Safety
+    ///
+    /// rawmidi must contain Arc<T> in private_data.
+    unsafe extern "C" fn private_free_cb(rmidi: *mut bindings::snd_rawmidi) {
+        // SAFETY: paired with `into_foreign` in `new`.
+        drop::<Arc<T>>(unsafe { Arc::from_foreign((*rmidi).private_data) });
+    }
+}
+
+impl<T: RawMidiInput + 'static> RawMidiOps<T> {
+    const INPUT: bindings::snd_rawmidi_ops = bindings::snd_rawmidi_ops {
+        open: Some(Self::open_input),
+        close: Some(Self::close_input),
+        trigger: Some(Self::trigger_input),
+        drain: None,
+    };
+
+    /// # SAFETY
+    ///
+    /// substream must contain Arc<T> in private_data.
+    unsafe extern "C" fn open_input(s: *mut bindings::snd_rawmidi_substream) -> c_int {
+        with_ctx_err::<T>(s, |dev, sub| T::open(dev, sub))
+    }
+
+    /// # SAFETY
+    ///
+    /// substream must contain Arc<T> in private_data.
+    unsafe extern "C" fn close_input(s: *mut bindings::snd_rawmidi_substream) -> c_int {
+        with_ctx_err::<T>(s, |dev, sub| T::close(dev, sub))
+    }
+
+    /// # SAFETY
+    ///
+    /// substream must contain Arc<T> in private_data.
+    unsafe extern "C" fn trigger_input(s: *mut bindings::snd_rawmidi_substream, up: c_int) {
+        with_ctx::<T, _>(s, |dev, sub| T::trigger(dev, sub, up != 0))
+    }
+}
+
+impl<T: RawMidiOutput + 'static> RawMidiOps<T> {
+    const OUTPUT: bindings::snd_rawmidi_ops = bindings::snd_rawmidi_ops {
+        open: Some(Self::open_output),
+        close: Some(Self::close_output),
+        trigger: Some(Self::trigger_output),
+        drain: None,
+    };
+
+    /// # SAFETY
+    ///
+    /// substream must contain Arc<T> in private_data.
+    unsafe extern "C" fn open_output(s: *mut bindings::snd_rawmidi_substream) -> c_int {
+        with_ctx_err::<T>(s, |dev, sub| T::open(dev, sub))
+    }
+
+    /// # SAFETY
+    ///
+    /// substream must contain Arc<T> in private_data.
+    unsafe extern "C" fn close_output(s: *mut bindings::snd_rawmidi_substream) -> c_int {
+        with_ctx_err::<T>(s, |dev, sub| T::close(dev, sub))
+    }
+
+    /// # SAFETY
+    ///
+    /// substream must contain Arc<T> in private_data.
+    unsafe extern "C" fn trigger_output(s: *mut bindings::snd_rawmidi_substream, up: c_int) {
+        with_ctx::<T, _>(s, |dev, sub| T::trigger(dev, sub, up != 0))
+    }
+}
+
+// Helper for C FFI
+fn with_ctx_err<T: 'static>(
+    s: *mut bindings::snd_rawmidi_substream,
+    f: impl FnOnce(&T, &RawMidiSubstream) -> Result,
+) -> c_int {
+    with_ctx::<T, _>(s, |dev, sub| match f(dev, sub) {
+        Ok(()) => 0,
+        Err(e) => e.to_errno(),
+    })
+}
+
+// Helper for C FFI
+fn with_ctx<T: 'static, R>(
+    s: *mut bindings::snd_rawmidi_substream,
+    f: impl FnOnce(&T, &RawMidiSubstream) -> R,
+) -> R {
+    // SAFETY: caller contract (see callbacks): `s` and `rmidi` are valid,
+    // `private_data` is a live foreign-owned `Arc<T>`.
+    let data = unsafe { <Arc<T> as ForeignOwnable>::borrow((*(*s).rmidi).private_data) };
+    // SAFETY: substream is valid.
+    let substream = unsafe { RawMidiSubstream::from_raw(s) };
+    f(&data, &substream)
+}
+
+/// A rawmidi instance.
+///
+/// TODO: track lifetime with the parent sndcard?
+pub struct RawMidi {
+    ptr: *mut bindings::snd_rawmidi,
+}
+
+impl RawMidi {
+    /// Creates a new rawmidi instance bound to a parent soundcard.
+    ///
+    /// Note: this presently requires both a Input and Output implementation
+    /// even if the input_count or output_count is 0.
+    /// TODO: enforce correctness in the API.
+    pub fn new<T: RawMidiInput + RawMidiOutput + 'static>(
+        card: &SoundCard,
+        id: &CStr,
+        device_idx: u16,
+        output_count: u16,
+        input_count: u16,
+        data: Arc<T>,
+    ) -> Result<Self> {
+        let mut rmidi_ptr: *mut bindings::snd_rawmidi = core::ptr::null_mut();
+
+        // SAFETY: `card.ptr` is a valid soundcard allocation.
+        // id is converted to match the C API.
+        // The bindings expect a mut but snd_rawmidi_init just uses strcpy.
+        to_result(unsafe {
+            bindings::snd_rawmidi_new(
+                card.ptr,
+                id.as_char_ptr().cast_mut(),
+                device_idx.into(),
+                output_count.into(),
+                input_count.into(),
+                &mut rmidi_ptr,
+            )
+        })?;
+
+        // SAFETY: `rmidi_ptr` is validated by the return check above.
+        let rmidi = unsafe { &mut *rmidi_ptr };
+
+        rmidi.private_data = Arc::into_foreign(data).cast();
+        // Note: private_free will drop the private_data from the foreign ptr.
+        // need to make sure it is freed in the event the cb is not called!
+        rmidi.private_free = Some(RawMidiOps::<T>::private_free_cb);
+
+        if input_count > 0 {
+            rmidi.info_flags |= bindings::SNDRV_RAWMIDI_INFO_INPUT;
+            // SAFETY: rmidi_ptr is validated above.
+            unsafe {
+                bindings::snd_rawmidi_set_ops(
+                    rmidi_ptr,
+                    bindings::SNDRV_RAWMIDI_STREAM_INPUT as i32,
+                    &RawMidiOps::<T>::INPUT,
+                )
+            };
+        }
+        if output_count > 0 {
+            rmidi.info_flags |= bindings::SNDRV_RAWMIDI_INFO_OUTPUT;
+            // SAFETY: rmidi_ptr is validated above.
+            unsafe {
+                bindings::snd_rawmidi_set_ops(
+                    rmidi_ptr,
+                    bindings::SNDRV_RAWMIDI_STREAM_OUTPUT as i32,
+                    &RawMidiOps::<T>::OUTPUT,
+                );
+            }
+        }
+        if input_count > 0 && output_count > 0 {
+            rmidi.info_flags |= bindings::SNDRV_RAWMIDI_INFO_DUPLEX;
+        }
+
+        Ok(Self { ptr: rmidi_ptr })
+    }
+
+    /// Set the rawmidi device name.
+    pub fn set_name(&mut self, name: &CStr) -> Result<()> {
+        // SAFETY: `self.ptr` is valid between `new` and card free.
+        let mut rmidi = unsafe { *self.ptr };
+        copy_to_c_buf(&mut rmidi.name, name)
+    }
+}
diff --git a/samples/rust/rust_midi_device.rs b/samples/rust/rust_midi_device.rs
index 5561a0437b59..1465ba9544d6 100644
--- a/samples/rust/rust_midi_device.rs
+++ b/samples/rust/rust_midi_device.rs
@@ -1,15 +1,44 @@
 // SPDX-License-Identifier: GPL-2.0
 
 //! Rust MIDI minimal sample.
+//!
+//! This driver demonstrates usage of the kernel::snd subsystem:
+//! - Create a rawmidi device
+//! - Print received event
 
 use kernel::{
     faux,
     prelude::*,
     snd,
+    sync::Arc,
 };
 
+struct RustMidiDevice;
+
+impl snd::RawMidiInput for RustMidiDevice {
+    fn trigger(&self, _substream: &snd::RawMidiSubstream, up: bool) {
+        pr_debug!("input trigger up:{up}!\n");
+    }
+}
+
+// Implement midi output ops: just display transmitted event.
+impl snd::RawMidiOutput for RustMidiDevice {
+    fn trigger(&self, substream: &snd::RawMidiSubstream, up: bool) {
+        pr_debug!("output trigger up:{up}!\n");
+        if up {
+            let mut buf = [0; 64];
+            match substream.transmit(&mut buf) {
+                Ok(count) => pr_info!("transmit got: {:?}\n", buf[..count]),
+                Err(err) => pr_err!("transmit failed: {err:?}"),
+            };
+        }
+    }
+}
+
 impl kernel::Module for RustMidiDeviceModule {
     fn init(_module: &'static ThisModule) -> Result<Self> {
+        let state = Arc::new(RustMidiDevice, GFP_KERNEL)?;
+
         pr_debug!("Initializing sound card\n");
         let reg = faux::Registration::new(c"rust-midi-device", None)?;
         let fdev = reg.as_ref();
@@ -21,6 +50,10 @@ fn init(_module: &'static ThisModule) -> Result<Self> {
         card.set_driver(c"rust-midi")?;
         card.set_shortname(c"RustMIDI")?;
 
+        pr_debug!("Initializing rawmidi card\n");
+        let mut rmidi = snd::RawMidi::new(&card, c"RustRMIDI", 0, 1, 1, state.clone())?;
+        rmidi.set_name(c"RustRMIDI")?;
+
         // Register the sndcard to ALSA.
         card.register()?;
 

-- 
2.56.0



^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH RFC 3/3] rust: snd: Add active rawmidi_substream helper
  2026-10-11 13:02 [PATCH RFC 0/3] rust: snd: Introduce experimental ALSA rawmidi abstractions Tristan de Cacqueray
  2026-10-11 13:02 ` [PATCH RFC 1/3] rust: snd: Add initial snd_card binding Tristan de Cacqueray
  2026-10-11 13:02 ` [PATCH RFC 2/3] rust: snd: Add snd_rawmidi binding Tristan de Cacqueray
@ 2026-10-11 13:02 ` Tristan de Cacqueray
  2026-10-12 12:23 ` [PATCH RFC 0/3] rust: snd: Introduce experimental ALSA rawmidi abstractions Takashi Iwai
  3 siblings, 0 replies; 5+ messages in thread
From: Tristan de Cacqueray @ 2026-10-11 13:02 UTC (permalink / raw)
  To: Takashi Iwai, Miguel Ojeda
  Cc: linux-sound, rust-for-linux, linux-kernel, Tristan de Cacqueray

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

Signed-off-by: Tristan de Cacqueray <tristan@midirus.com>
---
 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



^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH RFC 0/3] rust: snd: Introduce experimental ALSA rawmidi abstractions
  2026-10-11 13:02 [PATCH RFC 0/3] rust: snd: Introduce experimental ALSA rawmidi abstractions Tristan de Cacqueray
                   ` (2 preceding siblings ...)
  2026-10-11 13:02 ` [PATCH RFC 3/3] rust: snd: Add active rawmidi_substream helper Tristan de Cacqueray
@ 2026-10-12 12:23 ` Takashi Iwai
  3 siblings, 0 replies; 5+ messages in thread
From: Takashi Iwai @ 2026-10-12 12:23 UTC (permalink / raw)
  To: Tristan de Cacqueray
  Cc: Takashi Iwai, Miguel Ojeda, linux-sound, rust-for-linux, linux-kernel

On Sun, 11 Oct 2026 15:02:11 +0200,
Tristan de Cacqueray wrote:
> 
> Hi Takashi and the Rust for Linux maintainers.
> 
> I just noticed your mail about an early Rust prototype for the sound
> subsystem right before I was about to submit this RFC!
>   https://lore.kernel.org/alsa-devel/878q48579z.wl-tiwai@suse.de/
> 
> Nevertheless, please allow me to share what I had in mind through this
> small proof of concept for the `rawmidi` API. I am looking for feedback on
> the scope, coordination, and the ownership model rather than a detailed
> review for merging.
> 
> My use case is a new hardware controller using a custom serial protocol.
> While developing its Rust driver, I needed an interface to ALSA rawmidi.
> I'm particularly interested in handling rawmidi substream safely to avoid
> issues like the one fixed by
>   221253723dc5 ("ALSA: bcd2000: Fix race between rawmidi and disconnect")
> 
> So, I wrote some bindings to see how that would look like. This series adds
> experimental snd_card and snd_rawmidi wrappers, input/output callback
> traits, and a sample rawmidi faux device using delayed work to simulate
> incoming MIDI data.
> 
> This is not merge-ready. In particular, the prototype does not yet enforce
> the devres lifetime of the card or the lifetime of its rawmidi wrapper. The
> substream helper also relies on the driver clearing it before close
> returns.
> 
> Here are my questions:
> 
>   1. Does this rawmidi experiment overlaps with your prototype?
>      If so, I would be happy to coordinate or adapt this experiment to it.

No, my patchset doesn't contain rawmidi support yet.
The sound card abstraction would conflict quite, sure, but it wouldn't
be too hard to adapt, I guess.

>   2. Is rawmidi a useful initial target for ALSA Rust abstraction?

Sure, why not, if it's actually needed? :)

>   3. Does your prototype mirror the C API 1:1, or for example, does it hide
>      the substream lifecycle? That's the design choice I'm least sure about.

It's mostly a straightforward wrapping of C API, but there are a few
twists to adapt Rust-way.  The card can be managed in RAII style, too,
for example.

I haven't prepared for submission, but you can find the current
changes in test/sound-rust branch of sound git tree (either kernel.org
or github.com):
  https://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound.git/log/?h=test/sound-rust
  https://github.com/tiwai/sound/tree/test/sound-rust
  
Those are over 50 commits on top of 7.3-rc5, and I'll likely rebase a
bit to rearrange before submission, so beware of changes in future.

The commits begin with a few Rust kernel changes for fixing and
addition for the sound support, including the PM support from Beata
Michalska (just took what have been posted), followed by ALSA core
abstractions for the card, PCM and controls.

Then it continues to the implementations of 3 actual sound drivers for
PCM/control: the dummy driver (with platform and hrtimer), and the
ens1370 and intel8x0 drivers for PCI devices to be run on VM.

After that, the extension to USB abstraction follows, including the
isochronous support patches by Colin Braun, and then the USB-audio
driver implementation (that can run on an actual bare metal), as well
as the USB runtime PM support.


BTW, until now, I haven't planned for any serious consideration to
merge Rust stuff yet.  Partly because the above was a pure experiment
for me, but another big obstacle is that the whole Rust C binding is
limited to built-in vmlinux.  With a distribution kernel maintainer
hat on, this is absolutely NO-GO -- we really want to avoid built-in.
(Graphics is a kind of exception nowadays because of boot support,
though.)  Can this be worked out soon...?


thanks,

Takashi

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-10-12 12:23 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-11 13:02 [PATCH RFC 0/3] rust: snd: Introduce experimental ALSA rawmidi abstractions Tristan de Cacqueray
2026-10-11 13:02 ` [PATCH RFC 1/3] rust: snd: Add initial snd_card binding Tristan de Cacqueray
2026-10-11 13:02 ` [PATCH RFC 2/3] rust: snd: Add snd_rawmidi binding Tristan de Cacqueray
2026-10-11 13:02 ` [PATCH RFC 3/3] rust: snd: Add active rawmidi_substream helper Tristan de Cacqueray
2026-10-12 12:23 ` [PATCH RFC 0/3] rust: snd: Introduce experimental ALSA rawmidi abstractions Takashi Iwai

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®