Skip to main content

yip_io/
batch.rs

1//! Vectorized `recvmmsg` and `sendmmsg` socket engine.
2//!
3//! Provides [`BatchUdpSocket`] to send and receive datagram bursts of up to
4//! [`BATCH_SIZE`] (32 datagrams) in a single syscall, reducing syscall overhead
5//! and maximizing throughput in multi-core packet pipelines.
6
7use std::io;
8use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket};
9use std::os::fd::{AsRawFd, RawFd};
10
11use crate::MAX_WIRE_DATAGRAM;
12
13/// Maximum number of datagrams transferred in a single default batched syscall.
14pub const BATCH_SIZE: usize = 32;
15
16/// Expanded 64-datagram burst size for high-packet-rate 10G/25G interfaces.
17pub const BATCH_SIZE_64: usize = 64;
18
19/// Ultra-high 128-datagram burst size for maximum syscall amortization at 40G+ line rate.
20pub const BATCH_SIZE_128: usize = 128;
21
22/// Metadata for a single datagram received via [`BatchUdpSocket::recvmmsg_batch`].
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct ReceivedDatagram {
25    /// Actual byte length of the received datagram payload.
26    pub len: usize,
27    /// Source socket address from which the datagram originated.
28    pub src: SocketAddr,
29}
30
31impl ReceivedDatagram {
32    /// Return an empty `ReceivedDatagram` with length 0 and unspecified source address.
33    #[must_use]
34    pub const fn empty() -> Self {
35        Self {
36            len: 0,
37            src: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)),
38        }
39    }
40}
41
42impl Default for ReceivedDatagram {
43    fn default() -> Self {
44        Self::empty()
45    }
46}
47
48/// A non-owning wrapper around a UDP socket file descriptor providing vectorized
49/// batch send and receive operations via `libc::sendmmsg` and `libc::recvmmsg`.
50#[derive(Debug)]
51pub struct BatchUdpSocket {
52    fd: RawFd,
53}
54
55impl BatchUdpSocket {
56    /// Create a new `BatchUdpSocket` borrowing the raw file descriptor of `sock`.
57    #[must_use]
58    pub fn new(sock: &UdpSocket) -> Self {
59        Self {
60            fd: sock.as_raw_fd(),
61        }
62    }
63
64    /// Create a new `BatchUdpSocket` from an existing raw file descriptor.
65    #[must_use]
66    pub const fn from_raw_fd(fd: RawFd) -> Self {
67        Self { fd }
68    }
69
70    /// Return the underlying raw file descriptor.
71    #[must_use]
72    pub const fn raw_fd(&self) -> RawFd {
73        self.fd
74    }
75
76    /// Receive a burst of up to `N` datagrams into `buffers` via `libc::recvmmsg`.
77    ///
78    /// For each received datagram:
79    /// - The datagram payload bytes are written into `buffers[i]`.
80    /// - Metadata (byte length and source address) is recorded in `out[i]`.
81    ///
82    /// Returns the number of datagrams actually received (0 if the socket would block).
83    ///
84    /// # Errors
85    ///
86    /// Returns an [`io::Error`] if the underlying `recvmmsg` syscall fails with any error
87    /// other than `EAGAIN` or `EWOULDBLOCK`.
88    pub fn recvmmsg_batch<const N: usize>(
89        &mut self,
90        buffers: &mut [[u8; MAX_WIRE_DATAGRAM]; N],
91        out: &mut [ReceivedDatagram; N],
92    ) -> io::Result<usize> {
93        if N == 0 {
94            return Ok(0);
95        }
96
97        let mut storages: [libc::sockaddr_storage; N] = unsafe {
98            // SAFETY: `sockaddr_storage` is a plain-old-data struct consisting of integer
99            // and byte array fields. The all-zero bit pattern is a valid initial state that
100            // `libc::recvmmsg` overwrites upon receipt.
101            std::mem::zeroed()
102        };
103        let addrlens = [libc::socklen_t::try_from(std::mem::size_of::<libc::sockaddr_storage>())
104            .expect("sockaddr_storage size fits socklen_t"); N];
105        let mut iovecs = [libc::iovec {
106            iov_base: std::ptr::null_mut(),
107            iov_len: 0,
108        }; N];
109        let mut msgs = [crate::empty_mmsghdr(); N];
110
111        for i in 0..N {
112            iovecs[i].iov_base = buffers[i].as_mut_ptr().cast::<libc::c_void>();
113            iovecs[i].iov_len = MAX_WIRE_DATAGRAM;
114            msgs[i].msg_hdr.msg_iov = &raw mut iovecs[i];
115            msgs[i].msg_hdr.msg_iovlen = 1;
116            msgs[i].msg_hdr.msg_name = std::ptr::from_mut(&mut storages[i]).cast::<libc::c_void>();
117            msgs[i].msg_hdr.msg_namelen = addrlens[i];
118        }
119
120        loop {
121            // SAFETY: `msgs` is fully initialized with non-overlapping pointers:
122            // each `msg_iov` points to `iovecs[i]`, and each `msg_name` points to `storages[i]`,
123            // both residing on this stack frame. `iovecs[i].iov_base` points to caller-supplied
124            // `buffers[i]` which outlives this call. No pointers alias each other, and all remain
125            // valid throughout the syscall. `MSG_DONTWAIT` ensures the call returns immediately
126            // if no packets are queued.
127            let ret = unsafe {
128                libc::recvmmsg(
129                    self.fd,
130                    msgs.as_mut_ptr(),
131                    u32::try_from(N).expect("N fits u32"),
132                    libc::MSG_DONTWAIT as crate::MsgFlags,
133                    std::ptr::null_mut(),
134                )
135            };
136
137            if ret < 0 {
138                let err = io::Error::last_os_error();
139                let raw = err.raw_os_error().unwrap_or(0);
140                if raw == libc::EINTR {
141                    continue;
142                }
143                if raw == libc::EWOULDBLOCK || raw == libc::EAGAIN {
144                    return Ok(0);
145                }
146                return Err(err);
147            }
148
149            let received = usize::try_from(ret).expect("recvmmsg return fits usize");
150            assert!(received <= N, "recvmmsg returned count exceeding N");
151
152            for i in 0..received {
153                let len = usize::try_from(msgs[i].msg_len).expect("msg_len fits usize");
154                let src = crate::addr::sockaddr_to_std(&storages[i], msgs[i].msg_hdr.msg_namelen)
155                    .unwrap_or_else(|_| {
156                        SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0))
157                    });
158                out[i] = ReceivedDatagram { len, src };
159            }
160
161            return Ok(received);
162        }
163    }
164
165    /// Send a burst of up to [`BATCH_SIZE`] (32) datagrams in a single `libc::sendmmsg` syscall.
166    ///
167    /// If `packets.len() > BATCH_SIZE`, only the first [`BATCH_SIZE`] packets are submitted.
168    /// Returns the number of datagrams accepted by the kernel (0 if the socket would block).
169    ///
170    /// # Errors
171    ///
172    /// Returns an [`io::Error`] if the underlying `sendmmsg` syscall fails with an unrecoverable error.
173    pub fn sendmmsg_batch(&mut self, packets: &[(&[u8], SocketAddr)]) -> io::Result<usize> {
174        self.sendmmsg_burst::<BATCH_SIZE>(packets)
175    }
176
177    /// Send a burst of up to `N` datagrams in a single `libc::sendmmsg` syscall.
178    ///
179    /// If `packets.len() > N`, only the first `N` packets are submitted.
180    /// Returns the number of datagrams accepted by the kernel (0 if the socket would block).
181    ///
182    /// # Errors
183    ///
184    /// Returns an [`io::Error`] if the underlying `sendmmsg` syscall fails with an unrecoverable error.
185    pub fn sendmmsg_burst<const N: usize>(
186        &mut self,
187        packets: &[(&[u8], SocketAddr)],
188    ) -> io::Result<usize> {
189        let count = packets.len().min(N);
190        if count == 0 {
191            return Ok(0);
192        }
193
194        let mut storages: [libc::sockaddr_storage; N] = unsafe {
195            // SAFETY: `sockaddr_storage` is plain-old-data. An all-zero value is a valid
196            // initial state that is overwritten for the first `count` items below.
197            std::mem::zeroed()
198        };
199        let mut addrlens = [0 as libc::socklen_t; N];
200        let mut iovecs = [libc::iovec {
201            iov_base: std::ptr::null_mut(),
202            iov_len: 0,
203        }; N];
204        let mut msgs = [crate::empty_mmsghdr(); N];
205
206        for (i, &(payload, dst)) in packets[..count].iter().enumerate() {
207            let (storage, addr_len) = crate::addr::std_to_sockaddr(dst);
208            storages[i] = storage;
209            addrlens[i] = addr_len;
210            iovecs[i].iov_base = payload.as_ptr().cast_mut().cast::<libc::c_void>();
211            iovecs[i].iov_len = payload.len();
212            msgs[i].msg_hdr.msg_iov = &raw mut iovecs[i];
213            msgs[i].msg_hdr.msg_iovlen = 1;
214            msgs[i].msg_hdr.msg_name = std::ptr::from_mut(&mut storages[i]).cast::<libc::c_void>();
215            msgs[i].msg_hdr.msg_namelen = addrlens[i];
216        }
217
218        loop {
219            // SAFETY: `msgs[..count]` is fully initialized: each `msg_iov` points to `iovecs[i]`
220            // and `msg_name` points to `storages[i]` on this stack frame. The `payload` slices
221            // are borrowed from `packets` and remain valid for the duration of the call.
222            // `MSG_NOSIGNAL` suppresses SIGPIPE if a peer connection is reset.
223            let ret = unsafe {
224                libc::sendmmsg(
225                    self.fd,
226                    msgs.as_mut_ptr(),
227                    u32::try_from(count).expect("count fits u32"),
228                    libc::MSG_NOSIGNAL as crate::MsgFlags,
229                )
230            };
231
232            if ret < 0 {
233                let err = io::Error::last_os_error();
234                let raw = err.raw_os_error().unwrap_or(0);
235                if raw == libc::EINTR {
236                    continue;
237                }
238                if raw == libc::EWOULDBLOCK || raw == libc::EAGAIN || raw == libc::ENOBUFS {
239                    return Ok(0);
240                }
241                return Err(err);
242            }
243
244            return Ok(usize::try_from(ret).expect("sendmmsg return fits usize"));
245        }
246    }
247}
248
249impl AsRawFd for BatchUdpSocket {
250    fn as_raw_fd(&self) -> RawFd {
251        self.fd
252    }
253}
254
255/// Probe and optionally set socket-level `UDP_SEGMENT` size if supported by kernel.
256/// Returns `Ok(true)` if supported and configured, `Ok(false)` if unsupported by kernel/NIC.
257pub fn set_udp_gso_segment(sock: &UdpSocket, segment_size: u16) -> io::Result<bool> {
258    let val: libc::c_int = libc::c_int::from(segment_size);
259    // SAFETY: setsockopt with SOL_UDP / UDP_SEGMENT. If unsupported, EINVAL/ENOPROTOOPT/EOPNOTSUPP returned.
260    let ret = unsafe {
261        libc::setsockopt(
262            sock.as_raw_fd(),
263            libc::SOL_UDP,
264            libc::UDP_SEGMENT,
265            std::ptr::from_ref(&val).cast::<libc::c_void>(),
266            std::mem::size_of::<libc::c_int>() as libc::socklen_t,
267        )
268    };
269    if ret == 0 {
270        Ok(true)
271    } else {
272        let err = io::Error::last_os_error();
273        let raw = err.raw_os_error().unwrap_or(0);
274        if raw == libc::EINVAL || raw == libc::ENOPROTOOPT || raw == libc::EOPNOTSUPP {
275            Ok(false)
276        } else {
277            Err(err)
278        }
279    }
280}
281
282/// Send a GSO superpacket using a `UDP_SEGMENT` cmsg header.
283/// Returns `Ok(bytes_sent)` on success.
284pub fn send_gso_superpacket(
285    sock: &UdpSocket,
286    payload: &[u8],
287    segment_size: u16,
288    dst: SocketAddr,
289) -> io::Result<usize> {
290    let (mut storage, addr_len) = crate::addr::std_to_sockaddr(dst);
291    let mut iov = libc::iovec {
292        iov_base: payload.as_ptr().cast_mut().cast::<libc::c_void>(),
293        iov_len: payload.len(),
294    };
295    let mut control = [0u8; 64];
296    // SAFETY: msghdr is POD; zeroed is a valid initial state.
297    let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
298    msg.msg_name = std::ptr::from_mut(&mut storage).cast::<libc::c_void>();
299    msg.msg_namelen = addr_len;
300    msg.msg_iov = &raw mut iov;
301    msg.msg_iovlen = 1;
302    msg.msg_control = control.as_mut_ptr().cast::<libc::c_void>();
303    // SAFETY: CMSG_SPACE is a pure size computation.
304    let cmsg_space =
305        usize::try_from(unsafe { libc::CMSG_SPACE(std::mem::size_of::<u16>() as u32) })
306            .unwrap_or(control.len());
307    msg.msg_controllen = cmsg_space as crate::CmsgLen;
308
309    // SAFETY: msg points to valid control storage; write SOL_UDP / UDP_SEGMENT cmsg.
310    unsafe {
311        let cmsg = libc::CMSG_FIRSTHDR(&raw const msg);
312        if !cmsg.is_null() {
313            (*cmsg).cmsg_level = libc::SOL_UDP;
314            (*cmsg).cmsg_type = libc::UDP_SEGMENT;
315            (*cmsg).cmsg_len = usize::try_from(libc::CMSG_LEN(std::mem::size_of::<u16>() as u32))
316                .unwrap_or(0) as crate::CmsgLen;
317            let seg_ptr = libc::CMSG_DATA(cmsg).cast::<u16>();
318            *seg_ptr = segment_size;
319        }
320    }
321
322    loop {
323        // SAFETY: msg is fully initialized and points to valid stack frame structures.
324        let ret = unsafe { libc::sendmsg(sock.as_raw_fd(), &raw const msg, libc::MSG_NOSIGNAL) };
325        if ret < 0 {
326            let err = io::Error::last_os_error();
327            let raw = err.raw_os_error().unwrap_or(0);
328            if raw == libc::EINTR {
329                continue;
330            }
331            if raw == libc::EWOULDBLOCK || raw == libc::EAGAIN || raw == libc::ENOBUFS {
332                return Ok(0);
333            }
334            return Err(err);
335        }
336        return Ok(usize::try_from(ret).unwrap_or(0));
337    }
338}