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
next prev 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®