mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Tristan de Cacqueray <tristan@midirus.com>
To: Takashi Iwai <tiwai@suse.de>, Miguel Ojeda <ojeda@kernel.org>
Cc: linux-sound@vger.kernel.org, rust-for-linux@vger.kernel.org,
	linux-kernel@vger.kernel.org,
	Tristan de Cacqueray <tristan@midirus.com>
Subject: [PATCH RFC 2/3] rust: snd: Add snd_rawmidi binding
Date: Sun, 11 Oct 2026 13:02:22 +0000	[thread overview]
Message-ID: <20261011-rust-snd-v1-2-7974ecdcd345@midirus.com> (raw)
In-Reply-To: <20261011-rust-snd-v1-0-7974ecdcd345@midirus.com>

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



  parent reply	other threads:[~2026-10-11 13:02 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
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
2026-10-12 19:46   ` Tristan de Cacqueray

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=20261011-rust-snd-v1-2-7974ecdcd345@midirus.com \
    --to=tristan@midirus.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-sound@vger.kernel.org \
    --cc=ojeda@kernel.org \
    --cc=rust-for-linux@vger.kernel.org \
    --cc=tiwai@suse.de \
    /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

all inboxes | Powered by JetHome®