Skip to content

Instantly share code, notes, and snippets.

@samcday
Created November 14, 2025 23:33
Show Gist options
  • Select an option

  • Save samcday/d23aaf95be678bcdd1dd62b4b79861ea to your computer and use it in GitHub Desktop.

Select an option

Save samcday/d23aaf95be678bcdd1dd62b4b79861ea to your computer and use it in GitHub Desktop.
Rust FunctionFS + DMA-BUF example
[package]
name = "ffs-dmabuf-example"
version = "0.1.0"
edition = "2024"
autobins = true
[dependencies]
usb-gadget = "0.7.6"
anyhow = "1.0.100"
dma-heap = "0.4.1"
nix = { version = "0.30.1", features = ["ioctl", "poll"] }
dma-buf = "0.5.0"
rusb = "0.9.4"
libc = "0.2.177"
[[bin]]
name = "gadget"
path = "gadget.rs"
[[bin]]
name = "host"
path = "host.rs"
use anyhow::Context;
use dma_heap::HeapKind;
use nix::errno::Errno;
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
use nix::{ioctl_readwrite, ioctl_write_ptr};
use std::fs::File;
use std::io::Write;
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd, RawFd};
use usb_gadget::function::custom::{Custom, Endpoint, EndpointDirection, Interface};
use usb_gadget::{Class, Config, Gadget, Id, Strings};
#[repr(C, packed)]
pub struct UsbFfsDmabufTransferReq {
pub fd: libc::c_int,
pub flags: u32,
pub length: u64,
}
const BUF_LEN: usize = 64;
const FUNCTIONFS_IOC_MAGIC: u8 = b'g';
const FUNCTIONFS_DMABUF_ATTACH_NR: u8 = 131;
const FUNCTIONFS_DMABUF_TRANSFER_NR: u8 = 133;
ioctl_write_ptr!(
ffs_dmabuf_attach,
FUNCTIONFS_IOC_MAGIC,
FUNCTIONFS_DMABUF_ATTACH_NR,
RawFd
);
ioctl_write_ptr!(
ffs_dmabuf_transfer,
FUNCTIONFS_IOC_MAGIC,
FUNCTIONFS_DMABUF_TRANSFER_NR,
UsbFfsDmabufTransferReq
);
#[repr(C)]
pub struct dma_buf_export_sync_file_req {
pub flags: u32,
pub fd: i32,
}
pub const DMA_BUF_SYNC_READ: u32 = 1 << 0;
pub const DMA_BUF_SYNC_WRITE: u32 = 1 << 1;
pub const DMA_BUF_SYNC_RW: u32 = DMA_BUF_SYNC_READ | DMA_BUF_SYNC_WRITE;
ioctl_readwrite!(
dma_buf_export_sync_file,
b'b',
2,
dma_buf_export_sync_file_req
);
pub fn wait_for_dmabuf_completion(dmabuf_fd: RawFd) -> anyhow::Result<()> {
let sync_fd = unsafe {
let mut req = dma_buf_export_sync_file_req {
flags: DMA_BUF_SYNC_RW,
fd: -1,
};
dma_buf_export_sync_file(dmabuf_fd, &mut req).map_err(|e| Errno::from_raw(e as _))?;
if req.fd < 0 {
return Err(Errno::EINVAL.into());
}
OwnedFd::from_raw_fd(req.fd.as_raw_fd())
};
let mut fds = [PollFd::new(sync_fd.as_fd(), PollFlags::POLLIN)];
loop {
let ret = poll(&mut fds, PollTimeout::NONE)?;
match ret {
0 => continue,
_ => {
let revents = fds[0].revents().unwrap_or(PollFlags::empty());
if revents.contains(PollFlags::POLLERR) {
return Err(Errno::EIO.into());
}
if revents.intersects(PollFlags::POLLIN) {
break;
}
}
}
}
Ok(())
}
fn main() -> anyhow::Result<()> {
usb_gadget::remove_all()?;
let system = dma_heap::Heap::new(HeapKind::System)?;
let rx_buf = dma_buf::DmaBuf::from(system.allocate(BUF_LEN)?);
let tx_buf = dma_buf::DmaBuf::from(system.allocate(BUF_LEN)?);
let mut tx_buf_map = tx_buf.memory_map()?;
let (_, rx_dir) = EndpointDirection::host_to_device();
let (_, tx_dir) = EndpointDirection::device_to_host();
fn bulk_ep(dir: EndpointDirection) -> Endpoint {
let mut ep = Endpoint::bulk(dir);
ep.max_packet_size_hs = BUF_LEN as _;
ep.max_packet_size_ss = BUF_LEN as _;
ep
}
let rx_ep = bulk_ep(rx_dir);
let tx_ep = bulk_ep(tx_dir);
let mut builder = Custom::builder().with_interface(
Interface::new(Class::vendor_specific(123, 123), "smoo")
.with_endpoint(rx_ep)
.with_endpoint(tx_ep),
);
builder.ffs_no_init = true;
let (ffs_descs, ffs_strings) = builder.ffs_descriptors_and_strings()?;
let (mut custom, handle) = builder.build();
let klass = Class::new(255, 255, 3);
let id = Id::new(0xDEAD, 0xBEEF);
let strings = Strings::new("foo", "bar", "bacon");
let udc = usb_gadget::default_udc()?;
println!("binding to {:?}", udc);
let gadget =
Gadget::new(klass, id, strings).with_config(Config::new("config").with_function(handle));
let reg = gadget.register().context("register")?;
let mut ep0 = File::options()
.read(true)
.write(true)
.open(custom.ffs_dir()?.join("ep0"))?;
ep0.write_all(&ffs_descs)?;
ep0.write_all(&ffs_strings)?;
reg.bind(Some(&udc))?;
let rx = File::options()
.read(true)
.write(true)
.open(custom.ffs_dir()?.join("ep1"))?;
let tx = File::options()
.read(true)
.write(true)
.open(custom.ffs_dir()?.join("ep2"))?;
// Attach DMA-BUFs to the bulk endpoints.
unsafe { ffs_dmabuf_attach(rx.as_raw_fd(), &rx_buf.as_raw_fd()) }?;
unsafe { ffs_dmabuf_attach(tx.as_raw_fd(), &tx_buf_map.as_raw_fd()) }?;
println!("queueing write on bulk IN");
// write some data into TX buffer, with proper begin/end CPU access via ioctls
tx_buf_map
.write(
move |buf, _arg: Option<()>| {
let mut v: u8 = 1;
for b in buf {
*b = v;
v = v.wrapping_add(1);
}
Ok(())
},
None::<_>,
)
.map_err(|e| anyhow::anyhow!("dma-buf write err: {}", e))?;
unsafe {
ffs_dmabuf_transfer(
tx.as_raw_fd(),
&UsbFfsDmabufTransferReq {
fd: tx_buf_map.as_raw_fd(),
flags: 0,
length: BUF_LEN as _,
},
)?;
}
println!("ffs_dmabuf_transfer()");
wait_for_dmabuf_completion(tx_buf_map.as_raw_fd())?;
println!("queueing read on bulk OUT");
unsafe {
ffs_dmabuf_transfer(
rx.as_raw_fd(),
&UsbFfsDmabufTransferReq {
fd: rx_buf.as_raw_fd(),
flags: 0,
length: BUF_LEN as _,
},
)?;
}
println!("ffs_dmabuf_transfer()");
wait_for_dmabuf_completion(rx_buf.as_raw_fd())?;
let rx_buf_map = rx_buf.memory_map()?;
rx_buf_map
.read(
move |buf, _arg: Option<()>| {
println!("finally: {:?}", &buf[0..BUF_LEN]);
Ok(())
},
None::<_>,
)
.map_err(|e| anyhow::anyhow!("dma-buf read err: {}", e))?;
Ok(())
}
use anyhow::Context;
use rusb::{Direction, TransferType};
use std::time::Duration;
fn main() -> anyhow::Result<()> {
let handle = rusb::open_device_with_vid_pid(0xDEAD, 0xBEEF).unwrap();
let desc = handle.device().active_config_descriptor()?;
let mut data_rx = None;
let mut data_tx = None;
for intf in desc.interfaces() {
for ep in intf.descriptors().next().unwrap().endpoint_descriptors() {
println!("ep: {:?}", ep);
if ep.transfer_type() == TransferType::Bulk {
if Direction::In == ep.direction() {
data_rx = Some(ep.address());
} else {
data_tx = Some(ep.address());
}
}
}
}
let data_rx = data_rx.unwrap();
let data_tx = data_tx.unwrap();
handle.claim_interface(0)?;
let mut buf = [0u8; 64];
// read the incoming data.
let read = handle.read_bulk(data_rx, &mut buf, Duration::from_secs(0))?;
println!("read {} from bulk IN: {:?}", read, buf);
for (v, b) in buf.iter_mut().enumerate() {
*b = (v % 256) as _;
}
let written = handle
.write_bulk(data_tx, &buf, Duration::from_secs(0))
.context("write bulk OUT")?;
println!("wrote {} to bulk OUT", written);
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment