From: Mike Lothian <mike@fireburn.co.uk>
To: rust-for-linux@vger.kernel.org
Cc: linux-usb@vger.kernel.org,
"Greg Kroah-Hartman" <gregkh@linuxfoundation.org>,
"Daniel Almeida" <daniel.almeida@collabora.com>,
"Miguel Ojeda" <ojeda@kernel.org>,
"Boqun Feng" <boqun@kernel.org>, "Gary Guo" <gary@garyguo.net>,
"Björn Roy Baron" <bjorn3_gh@protonmail.com>,
"Benno Lossin" <lossin@kernel.org>,
"Andreas Hindborg" <a.hindborg@kernel.org>,
"Alice Ryhl" <aliceryhl@google.com>,
"Trevor Gross" <tmgross@umich.edu>,
"Danilo Krummrich" <dakr@kernel.org>,
"Alexandre Courbot" <acourbot@nvidia.com>,
linux-kernel@vger.kernel.org,
"Mike Lothian" <mike@fireburn.co.uk>
Subject: [RFC PATCH v2 01/11] rust: usb: add synchronous bulk transfer support
Date: Fri, 3 Jul 2026 04:00:05 +0100 [thread overview]
Message-ID: <20260703030020.2694-2-mike@fireburn.co.uk> (raw)
In-Reply-To: <20260703030020.2694-1-mike@fireburn.co.uk>
The USB abstractions currently let a Rust driver bind a device
(probe/disconnect) but provide no way to move data, so any real driver
still has to drop to C. Add synchronous bulk IN/OUT transfers, the most
common need, as safe methods on `usb::Device`.
`Device` is made public and gains `bulk_send()` and `bulk_recv()`,
wrappers over `usb_bulk_msg()`. The endpoint pipe is built with the
`usb_sndbulkpipe()` / `usb_rcvbulkpipe()` macros, exposed to Rust via new
`rust_helper_*` shims (they are function-like macros that bindgen cannot
bind directly). Both methods take the endpoint's `bEndpointAddress` as it
appears in the descriptor (e.g. 0x84) -- matching the later `clear_halt()`
-- and use its low four bits as the endpoint number; the direction is
fixed by the method. The transfer length and timeout are range-checked
into the C `int` types, and the timeout is a `Delta` (a zero `Delta`, or
any sub-millisecond value once truncated to whole ms, waits indefinitely,
matching `usb_bulk_msg()`).
`usb_bulk_msg()` DMAs directly to/from the transfer buffer, which must
therefore be kmalloc'd (DMA-capable) memory -- not the stack, `.rodata`
or vmalloc. Rather than push that onto callers as an unenforced
precondition (which would make these safe methods unsound for an
arbitrary `&[u8]`), the payload is copied through a kmalloc'd bounce
buffer internally, so any slice is accepted. A future zero-copy fast path
could take a dedicated DMA-buffer type once one exists; feedback on that
API shape is a main reason this is posted as an RFC.
Both methods block and sleep, so they must be called from process context
only.
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
rust/helpers/usb.c | 12 ++++++
rust/kernel/usb.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 108 insertions(+), 1 deletion(-)
diff --git a/rust/helpers/usb.c b/rust/helpers/usb.c
index eff1cf7be3c2..d398eb2f6669 100644
--- a/rust/helpers/usb.c
+++ b/rust/helpers/usb.c
@@ -7,3 +7,15 @@ rust_helper_interface_to_usbdev(struct usb_interface *intf)
{
return interface_to_usbdev(intf);
}
+
+__rust_helper unsigned int
+rust_helper_usb_sndbulkpipe(struct usb_device *dev, unsigned int endpoint)
+{
+ return usb_sndbulkpipe(dev, endpoint);
+}
+
+__rust_helper unsigned int
+rust_helper_usb_rcvbulkpipe(struct usb_device *dev, unsigned int endpoint)
+{
+ return usb_rcvbulkpipe(dev, endpoint);
+}
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index 7aff0c82d0af..c9acadb2deaf 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -19,6 +19,7 @@
},
prelude::*,
sync::aref::AlwaysRefCounted,
+ time::Delta,
types::Opaque,
ThisModule, //
};
@@ -426,7 +427,7 @@ unsafe impl Sync for Interface {}
///
/// [`struct usb_device`]: https://www.kernel.org/doc/html/latest/driver-api/usb/usb.html#c.usb_device
#[repr(transparent)]
-struct Device<Ctx: device::DeviceContext = device::Normal>(
+pub struct Device<Ctx: device::DeviceContext = device::Normal>(
Opaque<bindings::usb_device>,
PhantomData<Ctx>,
);
@@ -435,6 +436,100 @@ impl<Ctx: device::DeviceContext> Device<Ctx> {
fn as_raw(&self) -> *mut bindings::usb_device {
self.0.get()
}
+
+ /// Issues a synchronous bulk OUT transfer of `data` to bulk endpoint
+ /// `endpoint` and returns the number of bytes actually transferred.
+ ///
+ /// This is a blocking, sleeping call and therefore must only be invoked from
+ /// a process (sleepable) context, never from atomic or interrupt context.
+ ///
+ /// `endpoint` is the endpoint's `bEndpointAddress` as it appears in the
+ /// descriptor (e.g. `0x02`); the transfer direction is fixed by the method
+ /// (OUT), so only the low four bits — the endpoint number — are used.
+ /// `timeout` is the maximum time to wait, rounded down to whole
+ /// milliseconds; a [`Delta`] of zero — or any non-zero value below 1 ms —
+ /// waits indefinitely (the `usb_bulk_msg()` `timeout == 0` contract).
+ ///
+ /// `usb_bulk_msg()` DMAs directly from the transfer buffer, so the buffer
+ /// must reside in DMA-capable (kmalloc'd) memory. `data` may be any slice —
+ /// it is copied into a kmalloc'd bounce buffer internally — so callers need
+ /// not arrange DMA-capable storage themselves.
+ ///
+ /// [`usb_bulk_msg()`]: https://docs.kernel.org/driver-api/usb/usb.html#c.usb_bulk_msg
+ pub fn bulk_send(&self, endpoint: u8, data: &[u8], timeout: Delta) -> Result<usize> {
+ let mut actual: kernel::ffi::c_int = 0;
+
+ // `usb_bulk_msg()` requires a DMA-capable buffer; `data` may live on the
+ // stack or in `.rodata`, so copy it into a kmalloc'd bounce buffer.
+ let mut buf = KVec::with_capacity(data.len(), GFP_KERNEL)?;
+ buf.extend_from_slice(data, GFP_KERNEL)?;
+
+ // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the type invariant.
+ let pipe = unsafe { bindings::usb_sndbulkpipe(self.as_raw(), endpoint.into()) };
+
+ // SAFETY: `self.as_raw()` is valid by the type invariant; `buf` is a kmalloc'd
+ // buffer valid for reads of `buf.len()` bytes; `actual` is a valid out-pointer.
+ to_result(unsafe {
+ bindings::usb_bulk_msg(
+ self.as_raw(),
+ pipe,
+ buf.as_mut_ptr().cast::<kernel::ffi::c_void>(),
+ buf.len().try_into()?,
+ &mut actual,
+ timeout.as_millis().try_into()?,
+ )
+ })?;
+
+ Ok(actual as usize)
+ }
+
+ /// Issues a synchronous bulk IN transfer of up to `data.len()` bytes from bulk
+ /// endpoint `endpoint` into `data` and returns the number of bytes received.
+ ///
+ /// This is a blocking, sleeping call and therefore must only be invoked from a
+ /// process (sleepable) context, never from atomic or interrupt context.
+ ///
+ /// `endpoint` is the endpoint's `bEndpointAddress` as it appears in the
+ /// descriptor (e.g. `0x84`); the transfer direction is fixed by the method
+ /// (IN), so only the low four bits — the endpoint number — are used.
+ /// `timeout` is the maximum time to wait, rounded down to whole
+ /// milliseconds; a [`Delta`] of zero — or any non-zero value below 1 ms —
+ /// waits indefinitely (the `usb_bulk_msg()` `timeout == 0` contract).
+ ///
+ /// `usb_bulk_msg()` DMAs directly into the transfer buffer, so the buffer
+ /// must reside in DMA-capable (kmalloc'd) memory. The data is received into
+ /// a kmalloc'd bounce buffer internally and then copied into `data`, so
+ /// `data` may be any slice.
+ ///
+ /// [`usb_bulk_msg()`]: https://docs.kernel.org/driver-api/usb/usb.html#c.usb_bulk_msg
+ pub fn bulk_recv(&self, endpoint: u8, data: &mut [u8], timeout: Delta) -> Result<usize> {
+ let mut actual: kernel::ffi::c_int = 0;
+
+ // `usb_bulk_msg()` requires a DMA-capable buffer; receive into a kmalloc'd
+ // bounce buffer and copy out, so `data` need not be DMA-capable itself.
+ let mut buf = KVec::from_elem(0u8, data.len(), GFP_KERNEL)?;
+
+ // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the type invariant.
+ let pipe = unsafe { bindings::usb_rcvbulkpipe(self.as_raw(), endpoint.into()) };
+
+ // SAFETY: `self.as_raw()` is valid by the type invariant; `buf` is a kmalloc'd
+ // buffer valid for writes of `buf.len()` bytes; `actual` is a valid out-pointer.
+ to_result(unsafe {
+ bindings::usb_bulk_msg(
+ self.as_raw(),
+ pipe,
+ buf.as_mut_ptr().cast::<kernel::ffi::c_void>(),
+ buf.len().try_into()?,
+ &mut actual,
+ timeout.as_millis().try_into()?,
+ )
+ })?;
+
+ // `usb_bulk_msg()` never reports more than the requested length.
+ let n = (actual as usize).min(data.len());
+ data[..n].copy_from_slice(&buf[..n]);
+ Ok(n)
+ }
}
// SAFETY: `Device` is a transparent wrapper of a type that doesn't depend on `Device`'s generic
--
2.55.0
next prev parent reply other threads:[~2026-07-03 3:00 UTC|newest]
Thread overview: 30+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-17 14:59 [RFC PATCH 0/9] rust: usb: synchronous bulk/control transfers + helpers Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 1/9] rust: usb: add synchronous bulk transfer support Mike Lothian
2026-06-17 16:07 ` Danilo Krummrich
2026-06-17 14:59 ` [RFC PATCH 2/9] rust: usb: add synchronous control " Mike Lothian
2026-06-22 9:54 ` Oliver Neukum
2026-06-17 14:59 ` [RFC PATCH 3/9] rust: usb: add usb::Device::set_interface() Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 4/9] rust: usb: add usb::Interface::number() Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 5/9] rust: usb: add usb::Device::clear_halt() Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 6/9] rust: usb: add usb::Device::interrupt_recv() Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 7/9] rust: usb: add usb::Device::reset_configuration() Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 8/9] rust: usb: add an asynchronous persistently-queued bulk IN reader Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 9/9] rust: usb: add an asynchronous pipelined bulk OUT queue Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 00/11] rust: usb: synchronous + asynchronous bulk/control transfers + helpers Mike Lothian
2026-07-03 3:00 ` Mike Lothian [this message]
2026-07-06 9:17 ` [RFC PATCH v2 01/11] rust: usb: add synchronous bulk transfer support Oliver Neukum
2026-07-03 3:00 ` [RFC PATCH v2 02/11] rust: usb: add synchronous control " Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 03/11] rust: usb: add usb::Device::set_interface() Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 04/11] rust: usb: add usb::Interface::number() Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 05/11] rust: usb: add usb::Device::clear_halt() Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 06/11] rust: usb: add usb::Device::interrupt_recv() Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 07/11] rust: usb: add usb::Device::reset_configuration() Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 08/11] rust: usb: add an asynchronous persistently-queued bulk IN reader Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 09/11] rust: usb: add an asynchronous pipelined bulk OUT queue Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 10/11] rust: usb: keep usb::Device private and gate transfers on Interface<Bound> Mike Lothian
2026-07-06 9:45 ` Oliver Neukum
2026-07-06 10:38 ` Danilo Krummrich
2026-07-06 13:46 ` Alan Stern
2026-07-06 10:40 ` Danilo Krummrich
2026-07-03 3:00 ` [RFC PATCH v2 11/11] rust: usb: let drivers choose the transfer allocation flags Mike Lothian
2026-07-06 9:47 ` Oliver Neukum
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=20260703030020.2694-2-mike@fireburn.co.uk \
--to=mike@fireburn.co.uk \
--cc=a.hindborg@kernel.org \
--cc=acourbot@nvidia.com \
--cc=aliceryhl@google.com \
--cc=bjorn3_gh@protonmail.com \
--cc=boqun@kernel.org \
--cc=dakr@kernel.org \
--cc=daniel.almeida@collabora.com \
--cc=gary@garyguo.net \
--cc=gregkh@linuxfoundation.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-usb@vger.kernel.org \
--cc=lossin@kernel.org \
--cc=ojeda@kernel.org \
--cc=rust-for-linux@vger.kernel.org \
--cc=tmgross@umich.edu \
/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