Skip to main content

kevy_uring/
prep.rs

1//! SQE preparation helpers — `prep_*` queue one io_uring submission entry
2//! into the SQ ring. Split out of [`crate::ring`] so that file stays under
3//! the 500-LOC house rule. Each helper returns `false` if the SQ is full;
4//! the caller is expected to submit and retry.
5
6use core::ptr;
7
8use crate::ffi::{
9    IORING_OP_ACCEPT, IORING_OP_NOP, IORING_OP_READ, IORING_OP_RECV, IORING_OP_TIMEOUT,
10    IORING_OP_WRITE, IORING_RECV_MULTISHOT, IOSQE_BUFFER_SELECT, IOSQE_FIXED_FILE, SOCK_CLOEXEC,
11    SOCK_NONBLOCK,
12};
13use crate::layout::{IoUringSqe, KernelTimespec};
14use crate::ring::IoUring;
15
16impl IoUring {
17    /// Queue a `read(fd)` of `len` bytes into `buf`, tagged with `user_data`.
18    /// Returns `false` if the SQ is full.
19    ///
20    /// # Safety
21    /// `buf` must point to `len` writable bytes and stay valid until the matching
22    /// completion is reaped.
23    pub unsafe fn prep_read(&mut self, fd: i32, buf: *mut u8, len: u32, user_data: u64) -> bool {
24        let Some(idx) = self.reserve() else {
25            return false;
26        };
27        // SAFETY: `idx` is a freshly reserved, in-bounds SQE slot we own alone.
28        unsafe {
29            ptr::write(
30                self.sqes_ptr().add(idx),
31                IoUringSqe::new(IORING_OP_READ, fd, buf as u64, len, user_data),
32            );
33        }
34        true
35    }
36
37    /// Queue a `write(fd)` of `len` bytes from `buf`, tagged with `user_data`.
38    /// Returns `false` if the SQ is full.
39    ///
40    /// # Safety
41    /// `buf` must point to `len` readable bytes and stay valid until the matching
42    /// completion is reaped.
43    pub unsafe fn prep_write(&mut self, fd: i32, buf: *const u8, len: u32, user_data: u64) -> bool {
44        let Some(idx) = self.reserve() else {
45            return false;
46        };
47        // SAFETY: `idx` is a freshly reserved, in-bounds SQE slot we own alone.
48        unsafe {
49            ptr::write(
50                self.sqes_ptr().add(idx),
51                IoUringSqe::new(IORING_OP_WRITE, fd, buf as u64, len, user_data),
52            );
53        }
54        true
55    }
56
57    /// Queue a **multishot** `recv(fd)` that draws its destination buffer from
58    /// the provided-buffer group `bgid` (see [`IoUring::register_buf_ring`]): one
59    /// SQE re-fires a completion per arrival, the kernel picking + reporting a
60    /// buffer id each time, until it terminates (error / `ENOBUFS`, signalled by
61    /// [`crate::Completion::has_more`] returning `false`). No per-recv SQE, no read
62    /// buffer to keep alive. Returns `false` if the SQ is full.
63    pub fn prep_recv_multishot(&mut self, fd: i32, bgid: u16, user_data: u64) -> bool {
64        let Some(idx) = self.reserve() else {
65            return false;
66        };
67        // SAFETY: `idx` is a freshly reserved, in-bounds SQE slot we own alone.
68        unsafe {
69            let sqe = self.sqes_ptr().add(idx);
70            // addr/len 0: the buffer comes from the group, not from us.
71            ptr::write(sqe, IoUringSqe::new(IORING_OP_RECV, fd, 0, 0, user_data));
72            (*sqe).ioprio = IORING_RECV_MULTISHOT;
73            (*sqe).flags = IOSQE_BUFFER_SELECT;
74            // `buf_index` aliases `buf_group` in the kernel ABI.
75            (*sqe).buf_index = bgid;
76        }
77        true
78    }
79
80    /// Same as [`Self::prep_write`] but addresses the destination by
81    /// registered-files **slot index** instead of raw fd. Sets
82    /// `IOSQE_FIXED_FILE`; the kernel skips its per-op `fget`/`fput`. Caller
83    /// must have populated `slot` via
84    /// [`crate::IoUring::update_file_slot`].
85    ///
86    /// # Safety
87    /// Same as `prep_write`.
88    pub unsafe fn prep_write_fixed(
89        &mut self,
90        slot: u32,
91        buf: *const u8,
92        len: u32,
93        user_data: u64,
94    ) -> bool {
95        let Some(idx) = self.reserve() else {
96            return false;
97        };
98        // SAFETY: `idx` is a freshly reserved, in-bounds SQE slot we own alone.
99        unsafe {
100            let sqe = self.sqes_ptr().add(idx);
101            ptr::write(
102                sqe,
103                IoUringSqe::new(IORING_OP_WRITE, slot as i32, buf as u64, len, user_data),
104            );
105            (*sqe).flags = IOSQE_FIXED_FILE;
106        }
107        true
108    }
109
110    /// Same as [`Self::prep_recv_multishot`] but addresses the source by
111    /// registered-files **slot index** instead of raw fd. Sets
112    /// `IOSQE_FIXED_FILE | IOSQE_BUFFER_SELECT`; the kernel skips its
113    /// per-op `fget`/`fput`. Caller must have populated `slot` via
114    /// [`crate::IoUring::update_file_slot`].
115    pub fn prep_recv_multishot_fixed(
116        &mut self,
117        slot: u32,
118        bgid: u16,
119        user_data: u64,
120    ) -> bool {
121        let Some(idx) = self.reserve() else {
122            return false;
123        };
124        // SAFETY: `idx` is a freshly reserved, in-bounds SQE slot we own alone.
125        unsafe {
126            let sqe = self.sqes_ptr().add(idx);
127            ptr::write(
128                sqe,
129                IoUringSqe::new(IORING_OP_RECV, slot as i32, 0, 0, user_data),
130            );
131            (*sqe).ioprio = IORING_RECV_MULTISHOT;
132            (*sqe).flags = IOSQE_BUFFER_SELECT | IOSQE_FIXED_FILE;
133            (*sqe).buf_index = bgid;
134        }
135        true
136    }
137
138    /// Queue an `accept` on `listen_fd`; the accepted fd arrives as the
139    /// completion's `res` (already `O_NONBLOCK | O_CLOEXEC`). Returns `false` if
140    /// the SQ is full.
141    pub fn prep_accept(&mut self, listen_fd: i32, user_data: u64) -> bool {
142        let Some(idx) = self.reserve() else {
143            return false;
144        };
145        // SAFETY: `idx` is a freshly reserved, in-bounds SQE slot we own alone.
146        unsafe {
147            let sqe = self.sqes_ptr().add(idx);
148            ptr::write(
149                sqe,
150                IoUringSqe::new(IORING_OP_ACCEPT, listen_fd, 0, 0, user_data),
151            );
152            (*sqe).rw_flags = SOCK_NONBLOCK | SOCK_CLOEXEC;
153        }
154        true
155    }
156
157    /// Queue a relative timeout: the completion (res = `-ETIME`) arrives once
158    /// `ts` elapses, or earlier with res = 0 / `-ECANCELED` if the ring shuts
159    /// down. Bounds a blocking [`IoUring::submit_and_wait`] the way a poller's
160    /// wait-timeout would. Returns `false` if the SQ is full.
161    ///
162    /// # Safety
163    /// `ts` must stay valid (not moved or dropped) until the matching
164    /// completion is reaped — the kernel reads it asynchronously.
165    pub unsafe fn prep_timeout(&mut self, ts: *const KernelTimespec, user_data: u64) -> bool {
166        let Some(idx) = self.reserve() else {
167            return false;
168        };
169        // SAFETY: `idx` is a freshly reserved, in-bounds SQE slot we own alone.
170        unsafe {
171            let sqe = self.sqes_ptr().add(idx);
172            // addr = timespec ptr, len = 1 (one timespec), off = 0 (pure
173            // timeout — no completion-count trigger), rw_flags = 0 (relative).
174            ptr::write(
175                sqe,
176                IoUringSqe::new(IORING_OP_TIMEOUT, -1, ts as u64, 1, user_data),
177            );
178        }
179        true
180    }
181
182    /// Queue a no-op tagged with `user_data` (used to prove the round-trip).
183    /// Returns `false` if the SQ is full.
184    pub fn prep_nop(&mut self, user_data: u64) -> bool {
185        let Some(idx) = self.reserve() else {
186            return false;
187        };
188        // SAFETY: `idx` is a freshly reserved, in-bounds SQE slot we own alone.
189        unsafe {
190            ptr::write(
191                self.sqes_ptr().add(idx),
192                IoUringSqe::new(IORING_OP_NOP, -1, 0, 0, user_data),
193            );
194        }
195        true
196    }
197}