Skip to main content

air_sys_syscall/io_uring/
registration.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! **Registration** (Temps 3a) : enregistrement de ressources fixes auprès du
6//! kernel via `io_uring_register(2)` (n° 427) — tables de descripteurs fixes
7//! ([`FixedFdTable`]), buffers enregistrés ([`RegisteredBuffers`]), ring fd
8//! enregistré, eventfd, personality, réglage du pool io-wq, NAPI, horloge. Ce
9//! Temps **débloque** les variantes « direct »/« fixed » référencées depuis les
10//! Temps 2a–2c.
11//!
12//! Référence normative : `docs/specs/layer-0/io-uring-3a-registration.md`.
13//!
14//! **Modèle d'ownership (ADR-028, ADR-032).** Les ressources enregistrées
15//! restent valides **tant qu'elles sont enregistrées** : [`FixedFdTable`]
16//! **possède** les [`OwnedFd`] des slots remplis ; [`RegisteredBuffers`]
17//! possède les buffers épinglés (`Vec<u8>` ou [`MmapRegion`]). Les références
18//! d'usage ([`FixedSlot`], [`RegisteredBufferSlice`]) y sont liées par
19//! *lifetime* → inutilisables après désenregistrement (sûreté par construction).
20//! Le désenregistrement **restitue l'intégralité** des ressources reprises
21//! (zéro discard) ; toute consommation explicite (FD remplacé par `set`, buffer
22//! remplacé par `update`) est documentée par contrat.
23//!
24//! **`io_uring_register` est synchrone** (pas une op de SQE) : ces façades
25//! appellent directement la couture `syscall::register` sur le FD du ring
26//! (emprunté `&mut` le temps de l'appel), et **n'enregistrent jamais
27//! automatiquement** (ADR-022 D4) — l'application le demande.
28//!
29//! **Legacy évacués** : seules les variantes `*2` taguées sont exposées (pas de
30//! `REGISTER_BUFFERS`/`FILES`/`FILES_UPDATE` v1).
31
32use super::owned::OwnedOp;
33use super::{Completion, IoUring, SubmissionToken, raw, syscall};
34use crate::mem::MmapRegion;
35use air_sys_types::Errno;
36use air_sys_types::fd::{AsRawFd, BorrowedFd, OwnedFd};
37use air_sys_types::fs::{DirFd, OpenHow};
38use air_sys_types::net::{AcceptFlags, SocketDomain, SocketType};
39use air_sys_types::system::CpuSet;
40use alloc::boxed::Box;
41use alloc::vec::Vec;
42use core::marker::PhantomData;
43use core::num::NonZeroU32;
44use core::ops::Range;
45
46// ───────────────────────────────────────────────────────────────────────────
47// Couture register : appel synchrone + décodage errno
48// ───────────────────────────────────────────────────────────────────────────
49
50/// Appelle `io_uring_register(2)` sur le FD du ring et décode le retour.
51///
52/// Rend la valeur non-négative renvoyée par le kernel (`0` en général, l'**id**
53/// pour `REGISTER_PERSONALITY`, un compte pour certains opcodes).
54///
55/// # Safety
56///
57/// `(opcode, arg, nr_args)` doivent être **cohérents** : `arg` pointe une
58/// structure valide et accessible du type attendu par `opcode`, dimensionnée
59/// selon la convention du register opcode (`nr_args`). L'appelant emprunte le
60/// ring `&mut` le temps de l'appel (pas de soumission concurrente).
61unsafe fn register_raw(ring: &IoUring, opcode: u32, arg: u64, nr_args: u32) -> Result<i32, Errno> {
62    // SAFETY: déléguée à l'appelant (cf. doc) ; `fd_raw` est un ring fd valide.
63    let ret = unsafe { syscall::register(ring.fd_raw(), opcode, arg, nr_args) };
64    if ret < 0 {
65        return Err(raw::errno_from_negative_syscall_ret(ret));
66    }
67    // Les register opcodes rendent un petit entier non-négatif (toujours ≤ i32).
68    Ok(i32::try_from(ret).unwrap_or(0))
69}
70
71/// Convertit un indice `u32` (slot/buffer) en `usize` (toujours valide sur la
72/// cible LP64). La borne effective est vérifiée par le `get`/`get_mut` qui suit
73/// (l'`expect` est structurellement inatteignable, comme `ring::usz`).
74fn usz(index: u32) -> usize {
75    usize::try_from(index).expect("u32 ⊆ usize sur cible LP64")
76}
77
78/// `nr_args` d'un register opcode dont l'argument est une **structure unique** :
79/// la **taille en octets** de cette structure (convention kernel de
80/// `FILES2`/`BUFFERS2` et `*_UPDATE2`). Les tailles (32 o) sont figées par les
81/// `assert` de layout de `raw.rs` ⇒ l'`unwrap_or` est structurellement inerte.
82fn struct_nr_args<T>() -> u32 {
83    u32::try_from(core::mem::size_of::<T>()).unwrap_or(0)
84}
85
86// ───────────────────────────────────────────────────────────────────────────
87// Types de la surface 3a
88// ───────────────────────────────────────────────────────────────────────────
89
90/// Cible d'un slot pour une variante « direct » : indice **précis**, ou
91/// **auto-allocation** par le kernel.
92///
93/// [`FixedSlotTarget::Alloc`] est un `enum` typé, **jamais** la sentinelle
94/// kernel `IORING_FILE_INDEX_ALLOC` (`~0U`) exposée (ADR-021 conv. 1).
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum FixedSlotTarget {
97    /// Slot précis (l'indice exact dans la table de FD fixes).
98    Index(u32),
99    /// Le kernel choisit un slot libre (dans la plage d'`set_alloc_range`) et le
100    /// rend dans `cqe->res` ([`Completion::allocated_slot`]) ; `-ENFILE` si plein.
101    Alloc,
102}
103
104impl FixedSlotTarget {
105    /// Encode le `file_index` du SQE : `Alloc` ⇒ sentinelle kernel `~0U` ;
106    /// `Index(n)` ⇒ `n + 1` (le kernel décrémente ; `0` = « pas de slot »).
107    fn to_file_index(self) -> Result<u32, Errno> {
108        match self {
109            Self::Alloc => Ok(raw::IORING_FILE_INDEX_ALLOC),
110            // `n + 1` : `u32::MAX` (qui collisionnerait avec `~0U`/Alloc) ⇒ EINVAL.
111            Self::Index(n) => n.checked_add(1).ok_or(Errno::EINVAL),
112        }
113    }
114}
115
116/// Identité (credentials) enregistrée dans le ring ([`IoUring::register_personality`]).
117///
118/// L'**id** opaque est placé dans `sqe.personality` (via
119/// [`IoUring::with_personality`]) pour exécuter une op avec ces credentials.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct Personality(u16);
122
123impl Personality {
124    /// Id kernel de la personality (placé dans `sqe.personality`).
125    #[must_use]
126    pub fn id(self) -> u16 {
127        self.0
128    }
129
130    /// Id brut, usage interne (façade `with_personality`).
131    pub(crate) fn raw(self) -> u16 {
132        self.0
133    }
134}
135
136/// Plafonds de workers io-wq rendus par [`IoUring::set_work_queue_max_workers`]
137/// (les valeurs **précédentes**, réécrites par le kernel).
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
139pub struct WorkQueueWorkerLimits {
140    /// Plafond de la catégorie **bornée** (`IO_WQ_BOUND` : I/O disque).
141    pub bounded: u32,
142    /// Plafond de la catégorie **non-bornée** (`IO_WQ_UNBOUND` : réseau).
143    pub unbounded: u32,
144}
145
146/// Configuration du busy-poll NAPI ([`IoUring::register_napi`]).
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
148pub struct NapiConfig {
149    /// Délai de busy-poll en **microsecondes** (`busy_poll_to`). `0` désactive.
150    pub busy_poll_us: u32,
151    /// Préfère le busy-poll (`prefer_busy_poll`).
152    pub prefer_busy_poll: bool,
153}
154
155/// Source d'horloge des timeouts du ring ([`IoUring::register_clock`]).
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum ClockSource {
158    /// `CLOCK_MONOTONIC` : monotone, hors veille.
159    Monotonic,
160    /// `CLOCK_BOOTTIME` : monotone incluant le temps de veille.
161    Boottime,
162    /// `CLOCK_REALTIME` : temps réel (peut sauter).
163    Realtime,
164}
165
166impl ClockSource {
167    /// Valeur kernel (`CLOCK_*`).
168    fn clockid(self) -> u32 {
169        match self {
170            Self::Monotonic => raw::CLOCK_MONOTONIC,
171            Self::Boottime => raw::CLOCK_BOOTTIME,
172            Self::Realtime => raw::CLOCK_REALTIME,
173        }
174    }
175}
176
177// ───────────────────────────────────────────────────────────────────────────
178// Table de descripteurs fixes — FixedFdTable
179// ───────────────────────────────────────────────────────────────────────────
180
181/// Table de descripteurs de fichiers **fixes** (`REGISTER_FILES2`, 13).
182///
183/// Possède les [`OwnedFd`] des slots remplis ; sparse par défaut (slots vides à
184/// remplir plus tard). Les opérations sur FD fixe ([`FixedSlot`]) évitent au
185/// kernel la résolution du FD à chaque op (`IOSQE_FIXED_FILE`).
186#[derive(Debug)]
187pub struct FixedFdTable {
188    /// Un slot par capacité : `Some(fd)` rempli, `None` vide (sparse). Possède
189    /// les FD tant qu'ils sont enregistrés (restitués au désenregistrement).
190    slots: Vec<Option<OwnedFd>>,
191}
192
193impl FixedFdTable {
194    /// `REGISTER_FILES2` (13) : table **sparse** de `capacity` slots vides.
195    ///
196    /// # Errors
197    ///
198    /// Erreurs de `io_uring_register` : [`Errno::EINVAL`] (capacité trop grande,
199    /// déjà enregistré), [`Errno::EMFILE`]/[`Errno::ENOMEM`].
200    pub fn register(ring: &mut IoUring, capacity: NonZeroU32) -> Result<Self, Errno> {
201        let nr = capacity.get();
202        let rr = raw::IoUringRsrcRegister {
203            nr,
204            flags: raw::IORING_RSRC_REGISTER_SPARSE,
205            resv2: 0,
206            data: 0,
207            tags: 0,
208        };
209        // SAFETY: FILES2 attend une `io_uring_rsrc_register` (nr_args == 1) ;
210        // sparse ⇒ `data` nul (aucun FD initial lu). `rr` vit pendant l'appel.
211        unsafe {
212            register_raw(
213                ring,
214                raw::IORING_REGISTER_FILES2,
215                core::ptr::from_ref(&rr) as u64,
216                struct_nr_args::<raw::IoUringRsrcRegister>(),
217            )?;
218        }
219        Ok(Self {
220            slots: (0..nr).map(|_| None).collect(),
221        })
222    }
223
224    /// `REGISTER_FILES2` (13) avec un jeu **initial** de FD (slots remplis dans
225    /// l'ordre, à partir de l'indice 0). Les FD sont possédés par la table.
226    ///
227    /// # Errors
228    ///
229    /// [`Errno::EINVAL`] si `fds` est vide ou déborde un `u32` ; erreurs de
230    /// `io_uring_register`.
231    pub fn register_with(ring: &mut IoUring, fds: Vec<OwnedFd>) -> Result<Self, Errno> {
232        let nr = u32::try_from(fds.len()).map_err(|_| Errno::EINVAL)?;
233        if nr == 0 {
234            return Err(Errno::EINVAL);
235        }
236        // Tableau de FD bruts lu par le kernel pendant l'enregistrement synchrone.
237        let raw_fds: Vec<i32> = fds.iter().map(AsRawFd::as_raw_fd).collect();
238        let rr = raw::IoUringRsrcRegister {
239            nr,
240            flags: 0,
241            resv2: 0,
242            data: raw_fds.as_ptr() as u64,
243            tags: 0,
244        };
245        // SAFETY: FILES2 (nr_args == 1) ; `data` pointe `nr` `i32` (`raw_fds`)
246        // vivants pour la durée de l'appel synchrone ; `rr` vit aussi.
247        unsafe {
248            register_raw(
249                ring,
250                raw::IORING_REGISTER_FILES2,
251                core::ptr::from_ref(&rr) as u64,
252                struct_nr_args::<raw::IoUringRsrcRegister>(),
253            )?;
254        }
255        Ok(Self {
256            slots: fds.into_iter().map(Some).collect(),
257        })
258    }
259
260    /// `REGISTER_FILES_UPDATE2` (14) : place/remplace le FD du slot `slot`.
261    ///
262    /// **Consomme** tout FD précédemment présent dans ce slot (fermé) — transfert
263    /// d'ownership voulu, gravé dans la signature (ADR-032 : consommation
264    /// explicite ≠ discard).
265    ///
266    /// # Errors
267    ///
268    /// [`Errno::EINVAL`] si `slot` est hors borne ; erreurs de `io_uring_register`.
269    pub fn set(&mut self, ring: &mut IoUring, slot: u32, fd: OwnedFd) -> Result<(), Errno> {
270        let slot_ref = self.slots.get_mut(usz(slot)).ok_or(Errno::EINVAL)?;
271        let raw_fd = fd.as_raw_fd();
272        let up = raw::IoUringRsrcUpdate2 {
273            offset: slot,
274            resv: 0,
275            data: core::ptr::from_ref(&raw_fd) as u64,
276            tags: 0,
277            nr: 1,
278            resv2: 0,
279        };
280        // SAFETY: FILES_UPDATE2 (nr_args == 1) ; `data` pointe un `i32` (`raw_fd`)
281        // vivant ; `up` vit pendant l'appel. Le slot n'est mis à jour côté Air
282        // qu'**après** le succès kernel (sinon `fd` est rendu via le `?`).
283        unsafe {
284            register_raw(
285                ring,
286                raw::IORING_REGISTER_FILES_UPDATE2,
287                core::ptr::from_ref(&up) as u64,
288                struct_nr_args::<raw::IoUringRsrcUpdate2>(),
289            )?;
290        }
291        // L'ancien FD (le cas échéant) est fermé par ce remplacement.
292        *slot_ref = Some(fd);
293        Ok(())
294    }
295
296    /// `REGISTER_FILES_UPDATE2` (14) avec `fd = -1` : **vide** le slot. Rend le
297    /// FD qu'il contenait (zéro discard), ou `None` s'il était déjà vide.
298    ///
299    /// # Errors
300    ///
301    /// [`Errno::EINVAL`] si `slot` est hors borne ; erreurs de `io_uring_register`.
302    pub fn clear(&mut self, ring: &mut IoUring, slot: u32) -> Result<Option<OwnedFd>, Errno> {
303        let slot_ref = self.slots.get_mut(usz(slot)).ok_or(Errno::EINVAL)?;
304        let clear_fd: i32 = -1;
305        let up = raw::IoUringRsrcUpdate2 {
306            offset: slot,
307            resv: 0,
308            data: core::ptr::from_ref(&clear_fd) as u64,
309            tags: 0,
310            nr: 1,
311            resv2: 0,
312        };
313        // SAFETY: FILES_UPDATE2 (nr_args == 1) ; `data` pointe un `i32` (`-1`)
314        // vivant ; `up` vit pendant l'appel.
315        unsafe {
316            register_raw(
317                ring,
318                raw::IORING_REGISTER_FILES_UPDATE2,
319                core::ptr::from_ref(&up) as u64,
320                struct_nr_args::<raw::IoUringRsrcUpdate2>(),
321            )?;
322        }
323        Ok(slot_ref.take())
324    }
325
326    /// `REGISTER_FILE_ALLOC_RANGE` (25) : borne la plage `[start, end)` des slots
327    /// éligibles à l'auto-allocation ([`FixedSlotTarget::Alloc`]).
328    ///
329    /// # Errors
330    ///
331    /// [`Errno::EINVAL`] si `range` est inversé ou déborde la capacité ; erreurs
332    /// de `io_uring_register`.
333    pub fn set_alloc_range(&mut self, ring: &mut IoUring, range: Range<u32>) -> Result<(), Errno> {
334        // `end - start` : `None` (donc EINVAL) si `start > end`.
335        let len = range.end.checked_sub(range.start).ok_or(Errno::EINVAL)?;
336        let capacity = u32::try_from(self.slots.len()).unwrap_or(u32::MAX);
337        if range.end > capacity {
338            return Err(Errno::EINVAL);
339        }
340        let index_range = raw::IoUringFileIndexRange {
341            off: range.start,
342            len,
343            resv: 0,
344        };
345        // SAFETY: FILE_ALLOC_RANGE attend une `io_uring_file_index_range`
346        // (nr_args == 1) ; `index_range` vit pendant l'appel.
347        unsafe {
348            register_raw(
349                ring,
350                raw::IORING_REGISTER_FILE_ALLOC_RANGE,
351                core::ptr::from_ref(&index_range) as u64,
352                0,
353            )?;
354        }
355        Ok(())
356    }
357
358    /// `UNREGISTER_FILES` (3) : désenregistre toute la table et **rend** les FD
359    /// restants (restitution intégrale, ADR-032).
360    ///
361    /// **Sûreté par construction** : un [`FixedSlot`] emprunté à la table ne peut
362    /// pas survivre à ce désenregistrement (qui consomme `self`) — le code
363    /// suivant **ne compile pas** :
364    ///
365    /// ```compile_fail
366    /// # use air_sys_syscall::io_uring::{FixedFdTable, IoUring};
367    /// # use core::num::NonZeroU32;
368    /// # fn demo(ring: &mut IoUring) {
369    /// let table = FixedFdTable::register(ring, NonZeroU32::new(4).unwrap()).unwrap();
370    /// let slot = table.slot(0).unwrap();         // emprunte `table`
371    /// let _fds = table.unregister(ring).unwrap(); // déplace `table`…
372    /// let _ = slot.index();                       // …emprunt encore vivant ⇒ ERREUR
373    /// # }
374    /// ```
375    ///
376    /// # Errors
377    ///
378    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`] si rien n'est
379    /// enregistré, [`Errno::EBUSY`]). En cas d'erreur, les FD possédés sont
380    /// fermés (la table est consommée).
381    pub fn unregister(self, ring: &mut IoUring) -> Result<Vec<OwnedFd>, Errno> {
382        // SAFETY: UNREGISTER_FILES ne lit aucune mémoire (arg/nr_args nuls).
383        unsafe {
384            register_raw(ring, raw::IORING_UNREGISTER_FILES, 0, 0)?;
385        }
386        Ok(self.slots.into_iter().flatten().collect())
387    }
388
389    /// Référence empruntée au slot `slot` **pour usage en opération**
390    /// (`IOSQE_FIXED_FILE`), si l'indice est **dans la capacité** de la table.
391    /// `None` si hors borne.
392    ///
393    /// La référence est bornée par l'indice, pas par le suivi d'ownership : un
394    /// slot peut avoir été rempli par une variante « direct » (côté kernel, sans
395    /// `OwnedFd` côté Air). Référencer un slot **vide** est sûr — l'op échoue
396    /// alors proprement à la complétion (`EBADF`), jamais d'UB.
397    #[must_use]
398    pub fn slot(&self, slot: u32) -> Option<FixedSlot<'_>> {
399        if usz(slot) < self.slots.len() {
400            Some(FixedSlot {
401                index: slot,
402                _marker: PhantomData,
403            })
404        } else {
405            None
406        }
407    }
408}
409
410/// Référence empruntée à un slot d'une [`FixedFdTable`], liée à la table par
411/// *lifetime* — inutilisable après son désenregistrement (sûreté par
412/// construction).
413#[derive(Debug, Clone, Copy)]
414pub struct FixedSlot<'t> {
415    /// Indice du slot dans la table.
416    index: u32,
417    /// Lie la référence à la table (empêche l'usage après `unregister`).
418    _marker: PhantomData<&'t FixedFdTable>,
419}
420
421impl FixedSlot<'_> {
422    /// Indice du slot dans la table de FD fixes.
423    #[must_use]
424    pub fn index(&self) -> u32 {
425        self.index
426    }
427}
428
429// ───────────────────────────────────────────────────────────────────────────
430// Buffers enregistrés — RegisteredBuffers
431// ───────────────────────────────────────────────────────────────────────────
432
433/// Origine des buffers d'une [`RegisteredBuffers`] (mémoire possédée).
434#[derive(Debug)]
435enum BufferBacking {
436    /// Buffers `Vec<u8>` possédés (épinglés par le kernel à l'enregistrement).
437    Owned(Vec<Vec<u8>>),
438    /// Régions mmap possédées (data plane : `memfd` partagé AirCom).
439    Mapped(Vec<MmapRegion>),
440    /// Buffers **clonés** d'un autre ring ([`RegisteredBuffers::clone_from`]) :
441    /// la mémoire est possédée par le ring **source** (qui doit rester vivant).
442    Cloned,
443}
444
445/// Buffers enregistrés (`REGISTER_BUFFERS2`, 15) : l'épinglage et la traduction
446/// d'adresses sont faits **une fois** ⇒ `read_fixed`/`write_fixed` évitent ce
447/// coût par op (cas chaud du data plane AirCom).
448///
449/// Possède les buffers épinglés ; les références d'usage
450/// ([`RegisteredBufferSlice`]) y sont liées par *lifetime*.
451#[derive(Debug)]
452pub struct RegisteredBuffers {
453    backing: BufferBacking,
454}
455
456impl RegisteredBuffers {
457    /// Construit les `iovec` désignant `bufs` puis enregistre via `BUFFERS2`.
458    /// Cœur commun [`Self::register`] / [`Self::register_mmap`].
459    fn register_iovecs(
460        ring: &mut IoUring,
461        iovecs: &[raw::Iovec],
462        backing: BufferBacking,
463    ) -> Result<Self, Errno> {
464        let nr = u32::try_from(iovecs.len()).map_err(|_| Errno::EINVAL)?;
465        if nr == 0 {
466            return Err(Errno::EINVAL);
467        }
468        let rr = raw::IoUringRsrcRegister {
469            nr,
470            flags: 0,
471            resv2: 0,
472            data: iovecs.as_ptr() as u64,
473            tags: 0,
474        };
475        // SAFETY: BUFFERS2 attend une `io_uring_rsrc_register` (nr_args == 1) ;
476        // `data` pointe `nr` `iovec` vivants pour la durée de l'appel ; le kernel
477        // épingle les pages référencées (synchrone). La mémoire des buffers est
478        // possédée par `backing` (vivante tant que la table existe).
479        unsafe {
480            register_raw(
481                ring,
482                raw::IORING_REGISTER_BUFFERS2,
483                core::ptr::from_ref(&rr) as u64,
484                struct_nr_args::<raw::IoUringRsrcRegister>(),
485            )?;
486        }
487        Ok(Self { backing })
488    }
489
490    /// `REGISTER_BUFFERS2` (15) : épingle des buffers `Vec<u8>` possédés.
491    ///
492    /// # Errors
493    ///
494    /// [`Errno::EINVAL`] si `buffers` est vide ou déborde un `u32` ; erreurs de
495    /// `io_uring_register` ([`Errno::ENOMEM`], [`Errno::EFAULT`]).
496    pub fn register(ring: &mut IoUring, buffers: Vec<Vec<u8>>) -> Result<Self, Errno> {
497        let iovecs: Vec<raw::Iovec> = buffers
498            .iter()
499            .map(|b| raw::Iovec {
500                iov_base: b.as_ptr() as *mut u8,
501                iov_len: b.len(),
502            })
503            .collect();
504        Self::register_iovecs(ring, &iovecs, BufferBacking::Owned(buffers))
505    }
506
507    /// `REGISTER_BUFFERS2` (15) adossé à des [`MmapRegion`] **possédées** (data
508    /// plane : `memfd` partagé). Réutilise le handle de vivacité de `family-mem`.
509    ///
510    /// # Errors
511    ///
512    /// Voir [`RegisteredBuffers::register`].
513    pub fn register_mmap(ring: &mut IoUring, regions: Vec<MmapRegion>) -> Result<Self, Errno> {
514        let iovecs: Vec<raw::Iovec> = regions
515            .iter()
516            .map(|r| raw::Iovec {
517                iov_base: r.as_ptr() as *mut u8,
518                iov_len: r.len(),
519            })
520            .collect();
521        Self::register_iovecs(ring, &iovecs, BufferBacking::Mapped(regions))
522    }
523
524    /// `REGISTER_BUFFERS_UPDATE` (16) : remplace le buffer d'indice `index`.
525    ///
526    /// **Consomme** le buffer précédent (remplacement explicite). Réservé au
527    /// backing `Vec<u8>` (les variantes mmap/clonées rendent [`Errno::EINVAL`]).
528    ///
529    /// # Errors
530    ///
531    /// [`Errno::EINVAL`] si `index` est hors borne, ou si le backing n'est pas
532    /// `Vec<u8>` ; erreurs de `io_uring_register`.
533    pub fn update(&mut self, ring: &mut IoUring, index: u32, buffer: Vec<u8>) -> Result<(), Errno> {
534        let BufferBacking::Owned(buffers) = &mut self.backing else {
535            return Err(Errno::EINVAL);
536        };
537        let slot = buffers.get_mut(usz(index)).ok_or(Errno::EINVAL)?;
538        let iov = raw::Iovec {
539            iov_base: buffer.as_ptr() as *mut u8,
540            iov_len: buffer.len(),
541        };
542        let up = raw::IoUringRsrcUpdate2 {
543            offset: index,
544            resv: 0,
545            data: core::ptr::from_ref(&iov) as u64,
546            tags: 0,
547            nr: 1,
548            resv2: 0,
549        };
550        // SAFETY: BUFFERS_UPDATE (nr_args == 1) ; `data` pointe un `iovec` vivant
551        // désignant `buffer` (vivant) ; `up` vit pendant l'appel. Le slot n'est
552        // remplacé côté Air qu'**après** le succès kernel.
553        unsafe {
554            register_raw(
555                ring,
556                raw::IORING_REGISTER_BUFFERS_UPDATE,
557                core::ptr::from_ref(&up) as u64,
558                struct_nr_args::<raw::IoUringRsrcUpdate2>(),
559            )?;
560        }
561        *slot = buffer;
562        Ok(())
563    }
564
565    /// `REGISTER_CLONE_BUFFERS` (30) : clone les buffers enregistrés du ring
566    /// `src` (partage thread-per-core sans réépinglage).
567    ///
568    /// La mémoire reste possédée par `src` : la table rendue ne possède rien et
569    /// n'est valide que **tant que `src` garde ses buffers enregistrés et
570    /// vivants** (consommation par contrat). Les [`RegisteredBufferSlice`] à
571    /// utiliser proviennent de la table de `src` (mêmes adresses, même process).
572    ///
573    /// # Errors
574    ///
575    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`], [`Errno::EBUSY`] si la
576    /// table de destination est déjà peuplée).
577    pub fn clone_from(ring: &mut IoUring, src: &IoUring) -> Result<Self, Errno> {
578        let clone = raw::IoUringCloneBuffers {
579            src_fd: src.fd_raw().cast_unsigned(),
580            flags: 0,
581            pad: [0; 6],
582        };
583        // SAFETY: CLONE_BUFFERS attend une `io_uring_clone_buffers` ; `clone` vit
584        // pendant l'appel ; `src_fd` est le FD du ring source vivant.
585        unsafe {
586            register_raw(
587                ring,
588                raw::IORING_REGISTER_CLONE_BUFFERS,
589                core::ptr::from_ref(&clone) as u64,
590                1,
591            )?;
592        }
593        Ok(Self {
594            backing: BufferBacking::Cloned,
595        })
596    }
597
598    /// `UNREGISTER_BUFFERS` (1) : désenregistre et **rend** les buffers `Vec<u8>`
599    /// possédés (restitution intégrale, ADR-032).
600    ///
601    /// Pour un backing mmap, les régions sont **consommées** (munmap au dernier
602    /// drop — la signature `Vec<Vec<u8>>` ne peut les porter ; clonez la
603    /// [`MmapRegion`] avant si besoin). Pour un clone, rien à rendre.
604    ///
605    /// **Sûreté par construction** : un [`RegisteredBufferSlice`] ne peut pas
606    /// survivre à ce désenregistrement — le code suivant **ne compile pas** :
607    ///
608    /// ```compile_fail
609    /// # use air_sys_syscall::io_uring::{IoUring, RegisteredBuffers};
610    /// # fn demo(ring: &mut IoUring) {
611    /// let buffers = RegisteredBuffers::register(ring, vec![vec![0u8; 16]]).unwrap();
612    /// let slice = buffers.slice(0, 0..8).unwrap();  // emprunte `buffers`
613    /// let _ = buffers.unregister(ring).unwrap();     // déplace `buffers`…
614    /// let _ = slice.len();                           // …emprunt encore vivant ⇒ ERREUR
615    /// # }
616    /// ```
617    ///
618    /// # Errors
619    ///
620    /// Erreurs de `io_uring_register`. En cas d'erreur, les buffers possédés sont
621    /// libérés (la table est consommée).
622    pub fn unregister(self, ring: &mut IoUring) -> Result<Vec<Vec<u8>>, Errno> {
623        // SAFETY: UNREGISTER_BUFFERS ne lit aucune mémoire (arg/nr_args nuls).
624        unsafe {
625            register_raw(ring, raw::IORING_UNREGISTER_BUFFERS, 0, 0)?;
626        }
627        match self.backing {
628            BufferBacking::Owned(buffers) => Ok(buffers),
629            // Régions munmappées / clone sans mémoire propre : rien à restituer.
630            BufferBacking::Mapped(_) | BufferBacking::Cloned => Ok(Vec::new()),
631        }
632    }
633
634    /// Tranche `[range)` du buffer enregistré d'indice `index`, pour
635    /// `read_fixed`/`write_fixed`. `None` si l'indice/range est hors borne, ou si
636    /// le backing est cloné (géométrie inconnue côté destination).
637    #[must_use]
638    pub fn slice(&self, index: u32, range: Range<usize>) -> Option<RegisteredBufferSlice<'_>> {
639        let idx = usz(index);
640        let (base, buf_len) = match &self.backing {
641            BufferBacking::Owned(buffers) => {
642                let buf = buffers.get(idx)?;
643                (buf.as_ptr() as u64, buf.len())
644            }
645            BufferBacking::Mapped(regions) => {
646                let region = regions.get(idx)?;
647                (region.as_ptr() as u64, region.len())
648            }
649            BufferBacking::Cloned => return None,
650        };
651        if range.end > buf_len {
652            return None;
653        }
654        // `end - start` : `None` si `start > end`.
655        let span = range.end.checked_sub(range.start)?;
656        let start = u64::try_from(range.start).ok()?;
657        let addr = base.checked_add(start)?;
658        Some(RegisteredBufferSlice {
659            index,
660            addr,
661            len: span,
662            _marker: PhantomData,
663        })
664    }
665}
666
667/// Référence à une tranche d'un buffer enregistré, liée par *lifetime* à la
668/// [`RegisteredBuffers`] — `read_fixed` sur une tranche périmée ne compile pas.
669#[derive(Debug, Clone, Copy)]
670pub struct RegisteredBufferSlice<'b> {
671    /// Indice du buffer enregistré (`buf_index` du SQE).
672    index: u32,
673    /// Adresse de la tranche (dans la mémoire épinglée).
674    addr: u64,
675    /// Longueur de la tranche en octets.
676    len: usize,
677    /// Lie la tranche à la table (empêche l'usage après `unregister`).
678    _marker: PhantomData<&'b RegisteredBuffers>,
679}
680
681impl RegisteredBufferSlice<'_> {
682    /// Indice du buffer enregistré.
683    #[must_use]
684    pub fn index(&self) -> u32 {
685        self.index
686    }
687
688    /// Longueur de la tranche en octets.
689    #[must_use]
690    pub fn len(&self) -> usize {
691        self.len
692    }
693
694    /// `true` si la tranche est vide.
695    #[must_use]
696    pub fn is_empty(&self) -> bool {
697        self.len == 0
698    }
699}
700
701// ───────────────────────────────────────────────────────────────────────────
702// Registrations portées par l'IoUring (ring fd, eventfd, personality, io-wq,
703// napi, horloge) + variantes « direct »/« fixed » et fixed_fd_install.
704// ───────────────────────────────────────────────────────────────────────────
705
706impl IoUring {
707    // ── Ring fd enregistré (§4) ───────────────────────────────────────────
708
709    /// `REGISTER_RING_FDS` (20) : enregistre le FD du ring → les
710    /// `io_uring_enter` suivants utilisent `IORING_ENTER_REGISTERED_RING` (pas
711    /// de résolution de FD). Idempotent côté façade : un second appel
712    /// ré-enregistre (le kernel rend l'index).
713    ///
714    /// # Errors
715    ///
716    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`] si déjà enregistré côté
717    /// kernel, [`Errno::ENXIO`]).
718    pub fn register_ring_fd(&mut self) -> Result<(), Errno> {
719        let mut update = raw::IoUringRsrcUpdate {
720            offset: u32::MAX,
721            resv: 0,
722            data: u64::from(self.fd_raw().cast_unsigned()),
723        };
724        // SAFETY: RING_FDS attend un tableau de `io_uring_rsrc_update`
725        // (nr_args == 1) ; `update` vit pendant l'appel et le kernel y **réécrit**
726        // l'index assigné.
727        let ret = unsafe {
728            syscall::register(
729                self.fd_raw(),
730                raw::IORING_REGISTER_RING_FDS,
731                core::ptr::from_mut(&mut update) as u64,
732                1,
733            )
734        };
735        if ret < 0 {
736            return Err(raw::errno_from_negative_syscall_ret(ret));
737        }
738        self.enter_ring_index = Some(update.offset);
739        Ok(())
740    }
741
742    /// `UNREGISTER_RING_FDS` (21) : désenregistre le FD du ring (retour aux
743    /// `enter` par FD ordinaire).
744    ///
745    /// # Errors
746    ///
747    /// [`Errno::EINVAL`] si le ring fd n'était pas enregistré ; erreurs de
748    /// `io_uring_register`.
749    pub fn unregister_ring_fd(&mut self) -> Result<(), Errno> {
750        let index = self.enter_ring_index.ok_or(Errno::EINVAL)?;
751        let mut update = raw::IoUringRsrcUpdate {
752            offset: index,
753            resv: 0,
754            data: 0,
755        };
756        // SAFETY: UNREGISTER_RING_FDS attend un tableau de `io_uring_rsrc_update`
757        // (nr_args == 1) ; `update` vit pendant l'appel. On désigne le FD
758        // ordinaire (jamais l'index) pour ce register.
759        let ret = unsafe {
760            syscall::register(
761                self.fd_raw(),
762                raw::IORING_UNREGISTER_RING_FDS,
763                core::ptr::from_mut(&mut update) as u64,
764                1,
765            )
766        };
767        if ret < 0 {
768            return Err(raw::errno_from_negative_syscall_ret(ret));
769        }
770        self.enter_ring_index = None;
771        Ok(())
772    }
773
774    // ── eventfd (§5) ──────────────────────────────────────────────────────
775
776    /// Cœur commun `register_eventfd`/`_async` : enregistre `efd` sous `opcode`.
777    fn register_eventfd_op(&mut self, efd: BorrowedFd<'_>, opcode: u32) -> Result<(), Errno> {
778        let fd = efd.as_raw_fd();
779        // SAFETY: REGISTER_EVENTFD[_ASYNC] attend un `*const i32` (nr_args == 1) ;
780        // `fd` (variable locale) vit pendant l'appel.
781        unsafe {
782            register_raw(self, opcode, core::ptr::from_ref(&fd) as u64, 1)?;
783        }
784        Ok(())
785    }
786
787    /// `REGISTER_EVENTFD` (4) : lie un `eventfd` (famille `ipc`) aux complétions
788    /// (le kernel y écrit à chaque CQE posté → réveil d'un reactor via epoll).
789    ///
790    /// # Errors
791    ///
792    /// [`Errno::EINVAL`] si un eventfd est déjà enregistré ; [`Errno::EBADF`] si
793    /// `efd` est invalide.
794    pub fn register_eventfd(&mut self, efd: BorrowedFd<'_>) -> Result<(), Errno> {
795        self.register_eventfd_op(efd, raw::IORING_REGISTER_EVENTFD)
796    }
797
798    /// `REGISTER_EVENTFD_ASYNC` (7) : comme [`IoUring::register_eventfd`] mais ne
799    /// notifie que pour les complétions traitées en **asynchrone** (filtre le
800    /// bruit des complétions inline).
801    ///
802    /// # Errors
803    ///
804    /// Voir [`IoUring::register_eventfd`].
805    pub fn register_eventfd_async(&mut self, efd: BorrowedFd<'_>) -> Result<(), Errno> {
806        self.register_eventfd_op(efd, raw::IORING_REGISTER_EVENTFD_ASYNC)
807    }
808
809    /// `UNREGISTER_EVENTFD` (5) : détache l'eventfd.
810    ///
811    /// # Errors
812    ///
813    /// [`Errno::EINVAL`] si aucun eventfd n'était enregistré.
814    pub fn unregister_eventfd(&mut self) -> Result<(), Errno> {
815        // SAFETY: UNREGISTER_EVENTFD ne lit aucune mémoire (arg/nr_args nuls).
816        unsafe {
817            register_raw(self, raw::IORING_UNREGISTER_EVENTFD, 0, 0)?;
818        }
819        Ok(())
820    }
821
822    // ── Personality (§6) ──────────────────────────────────────────────────
823
824    /// `REGISTER_PERSONALITY` (9) : enregistre les credentials du process et rend
825    /// un id ([`Personality`]) — une op peut ensuite s'exécuter avec ces
826    /// credentials (`sqe.personality`, via [`IoUring::with_personality`]).
827    ///
828    /// # Errors
829    ///
830    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`] si la table de
831    /// personalities est pleine).
832    pub fn register_personality(&mut self) -> Result<Personality, Errno> {
833        // SAFETY: REGISTER_PERSONALITY ne lit aucune mémoire (arg/nr_args nuls) ;
834        // le kernel rend l'id (> 0) en valeur de retour.
835        let id = unsafe { register_raw(self, raw::IORING_REGISTER_PERSONALITY, 0, 0)? };
836        let id = u16::try_from(id).map_err(|_| Errno::EINVAL)?;
837        Ok(Personality(id))
838    }
839
840    /// `UNREGISTER_PERSONALITY` (10) : retire la personality `p` (id passé en
841    /// `nr_args`).
842    ///
843    /// # Errors
844    ///
845    /// [`Errno::EINVAL`] si l'id n'est pas enregistré.
846    pub fn unregister_personality(&mut self, p: Personality) -> Result<(), Errno> {
847        // SAFETY: UNREGISTER_PERSONALITY ne lit aucune mémoire ; l'id voyage dans
848        // `nr_args` (convention kernel).
849        unsafe {
850            register_raw(
851                self,
852                raw::IORING_UNREGISTER_PERSONALITY,
853                0,
854                u32::from(p.raw()),
855            )?;
856        }
857        Ok(())
858    }
859
860    // ── Réglage du pool io-wq (§7) ────────────────────────────────────────
861
862    /// `REGISTER_IOWQ_AFF` (17) : fixe l'affinité CPU des workers io-wq.
863    ///
864    /// # Errors
865    ///
866    /// [`Errno::EINVAL`] (masque invalide) ; erreurs de `io_uring_register`.
867    pub fn set_work_queue_affinity(&mut self, cpus: &CpuSet) -> Result<(), Errno> {
868        let bytes = cpus.as_bytes();
869        let nr = u32::try_from(bytes.len()).map_err(|_| Errno::EINVAL)?;
870        // SAFETY: IOWQ_AFF attend un `cpumask` de `nr` octets ; `bytes` (vue du
871        // `CpuSet` emprunté) vit pendant l'appel.
872        unsafe {
873            register_raw(
874                self,
875                raw::IORING_REGISTER_IOWQ_AFF,
876                bytes.as_ptr() as u64,
877                nr,
878            )?;
879        }
880        Ok(())
881    }
882
883    /// `UNREGISTER_IOWQ_AFF` (18) : rétablit l'affinité par défaut des workers.
884    ///
885    /// # Errors
886    ///
887    /// Erreurs de `io_uring_register`.
888    pub fn clear_work_queue_affinity(&mut self) -> Result<(), Errno> {
889        // SAFETY: UNREGISTER_IOWQ_AFF ne lit aucune mémoire (arg/nr_args nuls).
890        unsafe {
891            register_raw(self, raw::IORING_UNREGISTER_IOWQ_AFF, 0, 0)?;
892        }
893        Ok(())
894    }
895
896    /// `REGISTER_IOWQ_MAX_WORKERS` (19) : plafonne les workers bornés/non-bornés
897    /// (`0` = inchangé). Rend les valeurs **précédentes** (réécrites par le
898    /// kernel) — maîtrise de l'empreinte CPU sur matériel modeste (Charte).
899    ///
900    /// # Errors
901    ///
902    /// Erreurs de `io_uring_register`.
903    pub fn set_work_queue_max_workers(
904        &mut self,
905        bounded: u32,
906        unbounded: u32,
907    ) -> Result<WorkQueueWorkerLimits, Errno> {
908        let mut limits = [bounded, unbounded];
909        // SAFETY: IOWQ_MAX_WORKERS attend un `[u32; 2]` (nr_args == 2) ; le kernel
910        // y réécrit les anciennes valeurs. `limits` vit pendant l'appel.
911        unsafe {
912            register_raw(
913                self,
914                raw::IORING_REGISTER_IOWQ_MAX_WORKERS,
915                limits.as_mut_ptr() as u64,
916                2,
917            )?;
918        }
919        Ok(WorkQueueWorkerLimits {
920            bounded: limits[raw::IO_WQ_BOUND],
921            unbounded: limits[raw::IO_WQ_UNBOUND],
922        })
923    }
924
925    // ── NAPI busy-poll (§8) ───────────────────────────────────────────────
926
927    /// `REGISTER_NAPI` (27) : active le busy-poll NAPI (latence réseau ↓, CPU ↑).
928    /// Rend la configuration **précédente** (réécrite par le kernel).
929    ///
930    /// # Errors
931    ///
932    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`] si NAPI indisponible).
933    pub fn register_napi(&mut self, config: NapiConfig) -> Result<NapiConfig, Errno> {
934        let mut napi = raw::IoUringNapi {
935            busy_poll_to: config.busy_poll_us,
936            prefer_busy_poll: u8::from(config.prefer_busy_poll),
937            pad: [0; 3],
938            resv: 0,
939        };
940        // SAFETY: REGISTER_NAPI attend une `io_uring_napi` ; le kernel y réécrit
941        // la config précédente. `napi` vit pendant l'appel (nr_args == 0, liburing).
942        unsafe {
943            register_raw(
944                self,
945                raw::IORING_REGISTER_NAPI,
946                core::ptr::from_mut(&mut napi) as u64,
947                0,
948            )?;
949        }
950        Ok(NapiConfig {
951            busy_poll_us: napi.busy_poll_to,
952            prefer_busy_poll: napi.prefer_busy_poll != 0,
953        })
954    }
955
956    /// `UNREGISTER_NAPI` (28) : désactive le busy-poll NAPI.
957    ///
958    /// # Errors
959    ///
960    /// Erreurs de `io_uring_register`.
961    pub fn unregister_napi(&mut self) -> Result<(), Errno> {
962        // Le kernel réécrit l'ancienne config dans le tampon passé ; ignorée ici.
963        let mut napi = raw::IoUringNapi::default();
964        // SAFETY: UNREGISTER_NAPI attend une `io_uring_napi` ; `napi` vit pendant
965        // l'appel (nr_args == 0, liburing).
966        unsafe {
967            register_raw(
968                self,
969                raw::IORING_UNREGISTER_NAPI,
970                core::ptr::from_mut(&mut napi) as u64,
971                0,
972            )?;
973        }
974        Ok(())
975    }
976
977    // ── Source d'horloge (§9) ─────────────────────────────────────────────
978
979    /// `REGISTER_CLOCK` (29) : fixe l'horloge des timeouts du ring (cohérent avec
980    /// `TimeoutFlags` du Temps 2c et `family-time`).
981    ///
982    /// # Errors
983    ///
984    /// Erreurs de `io_uring_register` ([`Errno::EINVAL`] si l'horloge n'est pas
985    /// supportée).
986    pub fn register_clock(&mut self, clock: ClockSource) -> Result<(), Errno> {
987        let reg = raw::IoUringClockRegister {
988            clockid: clock.clockid(),
989            resv: [0; 3],
990        };
991        // SAFETY: REGISTER_CLOCK attend une `io_uring_clock_register` ; `reg` vit
992        // pendant l'appel (nr_args == 0, liburing).
993        unsafe {
994            register_raw(
995                self,
996                raw::IORING_REGISTER_CLOCK,
997                core::ptr::from_ref(&reg) as u64,
998                0,
999            )?;
1000        }
1001        Ok(())
1002    }
1003
1004    // ── Variantes « direct descriptor » (§2.1) ────────────────────────────
1005
1006    /// `IORING_OP_OPENAT2` (28) **direct** : range le FD ouvert dans un slot fixe
1007    /// (`slot`) au lieu de le rendre en FD ordinaire. Pour [`FixedSlotTarget::Alloc`],
1008    /// le slot choisi est rendu dans `cqe->res` ([`Completion::allocated_slot`]).
1009    ///
1010    /// # Errors
1011    ///
1012    /// [`Errno::EINVAL`] si `slot` est `Index(u32::MAX)` ; [`Errno::EBUSY`] si la
1013    /// SQ/le slab sont pleins.
1014    pub fn submit_openat2_direct(
1015        &mut self,
1016        dirfd: DirFd<'_>,
1017        path: alloc::ffi::CString,
1018        how: OpenHow,
1019        slot: FixedSlotTarget,
1020    ) -> Result<SubmissionToken, Errno> {
1021        let file_index = slot.to_file_index()?;
1022        let dirfd = match dirfd {
1023            DirFd::Cwd => raw::AT_FDCWD,
1024            DirFd::Fd(fd) => fd.as_raw_fd(),
1025        };
1026        // Pas de `O_CLOEXEC` ici : le kernel le **rejette** (`EINVAL`) sur un
1027        // descripteur direct (kernel-managed, non hérité à l'exec par nature ;
1028        // le `O_CLOEXEC` est posé à la matérialisation `fixed_fd_install`).
1029        let how = Box::new(raw::OpenHowRaw {
1030            flags: how.flags.bits(),
1031            mode: u64::from(how.mode),
1032            resolve: how.resolve.bits(),
1033        });
1034        let how_ptr = core::ptr::from_ref(&*how) as u64;
1035        let path_ptr = path.as_ptr() as u64;
1036        self.submit_op(Some(OwnedOp::Open { path, how }), |sqe| {
1037            sqe.opcode = raw::IORING_OP_OPENAT2;
1038            sqe.fd = dirfd;
1039            sqe.addr_or_splice_off_in = path_ptr;
1040            sqe.len = 24; // sizeof(open_how), figé par l'assert de layout
1041            sqe.off_or_addr2 = how_ptr;
1042            sqe.splice_fd_in_or_file_index = file_index;
1043        })
1044    }
1045
1046    /// `IORING_OP_ACCEPT` (13) **direct** : range le socket accepté dans un slot
1047    /// fixe. Pas de restitution d'adresse pair.
1048    ///
1049    /// Le kernel **rejette** `SOCK_CLOEXEC` sur un descripteur direct
1050    /// (kernel-managed) : ne pas l'ajouter (`flags` transmis tel quel). Le
1051    /// `O_CLOEXEC` est posé à la matérialisation (`fixed_fd_install`).
1052    ///
1053    /// # Errors
1054    ///
1055    /// Voir [`IoUring::submit_openat2_direct`].
1056    pub fn submit_accept_direct(
1057        &mut self,
1058        listener: BorrowedFd<'_>,
1059        flags: AcceptFlags,
1060        slot: FixedSlotTarget,
1061    ) -> Result<SubmissionToken, Errno> {
1062        let file_index = slot.to_file_index()?;
1063        let fd = listener.as_raw_fd();
1064        let accept_flags = flags.bits().cast_unsigned();
1065        self.submit_op(None, |sqe| {
1066            sqe.opcode = raw::IORING_OP_ACCEPT;
1067            sqe.fd = fd;
1068            sqe.op_flags = accept_flags;
1069            sqe.splice_fd_in_or_file_index = file_index;
1070        })
1071    }
1072
1073    /// `IORING_OP_SOCKET` (45) **direct** : crée un socket rangé dans un slot fixe.
1074    ///
1075    /// Le kernel **rejette** `SOCK_CLOEXEC` sur un descripteur direct : ne pas
1076    /// l'ajouter. Le `O_CLOEXEC` est posé à la matérialisation (`fixed_fd_install`).
1077    ///
1078    /// # Errors
1079    ///
1080    /// Voir [`IoUring::submit_openat2_direct`].
1081    pub fn submit_socket_direct(
1082        &mut self,
1083        domain: SocketDomain,
1084        ty: SocketType,
1085        protocol: i32,
1086        slot: FixedSlotTarget,
1087    ) -> Result<SubmissionToken, Errno> {
1088        let file_index = slot.to_file_index()?;
1089        let domain = domain as i32;
1090        let ty = ty as i32;
1091        let protocol = protocol.cast_unsigned();
1092        self.submit_op(None, |sqe| {
1093            sqe.opcode = raw::IORING_OP_SOCKET;
1094            sqe.fd = domain;
1095            sqe.off_or_addr2 = u64::from(ty.cast_unsigned());
1096            sqe.len = protocol;
1097            sqe.splice_fd_in_or_file_index = file_index;
1098        })
1099    }
1100
1101    // ── Opérations sur FD fixe (IOSQE_FIXED_FILE) ─────────────────────────
1102
1103    /// `IORING_OP_READ` (22) via un [`FixedSlot`] (`IOSQE_FIXED_FILE`) : lit dans
1104    /// `buffer` (déplacé dans le slot). Complétion via
1105    /// [`Completion::into_buffer_result`].
1106    ///
1107    /// # Errors
1108    ///
1109    /// [`Errno::EINVAL`] si `buffer.len()` déborde un `u32` ; [`Errno::EBUSY`]
1110    /// (SQ/slab pleins).
1111    pub fn submit_read_fixed_file(
1112        &mut self,
1113        slot: FixedSlot<'_>,
1114        mut buffer: Vec<u8>,
1115        offset: Option<u64>,
1116    ) -> Result<SubmissionToken, Errno> {
1117        let len = u32::try_from(buffer.len()).map_err(|_| Errno::EINVAL)?;
1118        let addr = buffer.as_mut_ptr() as u64;
1119        let off = offset.unwrap_or(u64::MAX);
1120        let index = slot.index().cast_signed();
1121        self.pending_options = self.pending_options.fixed_file();
1122        self.submit_op(Some(OwnedOp::Bytes(buffer)), |sqe| {
1123            sqe.opcode = raw::IORING_OP_READ;
1124            sqe.fd = index;
1125            sqe.addr_or_splice_off_in = addr;
1126            sqe.len = len;
1127            sqe.off_or_addr2 = off;
1128        })
1129    }
1130
1131    /// `IORING_OP_WRITE` (23) via un [`FixedSlot`] : écrit `buffer` (déplacé dans
1132    /// le slot). Complétion via [`Completion::into_buffer_result`].
1133    ///
1134    /// # Errors
1135    ///
1136    /// Voir [`IoUring::submit_read_fixed_file`].
1137    pub fn submit_write_fixed_file(
1138        &mut self,
1139        slot: FixedSlot<'_>,
1140        buffer: Vec<u8>,
1141        offset: Option<u64>,
1142    ) -> Result<SubmissionToken, Errno> {
1143        let len = u32::try_from(buffer.len()).map_err(|_| Errno::EINVAL)?;
1144        let addr = buffer.as_ptr() as u64;
1145        let off = offset.unwrap_or(u64::MAX);
1146        let index = slot.index().cast_signed();
1147        self.pending_options = self.pending_options.fixed_file();
1148        self.submit_op(Some(OwnedOp::Bytes(buffer)), |sqe| {
1149            sqe.opcode = raw::IORING_OP_WRITE;
1150            sqe.fd = index;
1151            sqe.addr_or_splice_off_in = addr;
1152            sqe.len = len;
1153            sqe.off_or_addr2 = off;
1154        })
1155    }
1156
1157    /// `IORING_OP_FIXED_FD_INSTALL` (54) : matérialise un [`FixedSlot`] (FD
1158    /// direct) en FD **ordinaire** (`O_CLOEXEC` posé). Complétion via
1159    /// [`Completion::opened_fd`].
1160    ///
1161    /// # Errors
1162    ///
1163    /// [`Errno::EBUSY`] (SQ/slab pleins) ; à la complétion, `EBADF` si le slot
1164    /// est vide.
1165    pub fn submit_fixed_fd_install(
1166        &mut self,
1167        slot: FixedSlot<'_>,
1168    ) -> Result<SubmissionToken, Errno> {
1169        let index = slot.index().cast_signed();
1170        self.pending_options = self.pending_options.fixed_file();
1171        self.submit_op(None, |sqe| {
1172            sqe.opcode = raw::IORING_OP_FIXED_FD_INSTALL;
1173            sqe.fd = index;
1174            // install_fd_flags = 0 ⇒ O_CLOEXEC posé (défaut Air, cohérent couche 0).
1175            sqe.op_flags = 0;
1176        })
1177    }
1178
1179    // ── read_fixed / write_fixed (buffer enregistré) ──────────────────────
1180
1181    /// `IORING_OP_READ_FIXED` (4) : lit `fd` dans la tranche d'un buffer
1182    /// enregistré (`slice`). Les octets atterrissent dans la mémoire épinglée
1183    /// (relue via la [`RegisteredBuffers`]). Complétion via
1184    /// [`Completion::into_result`] (nombre d'octets).
1185    ///
1186    /// # Errors
1187    ///
1188    /// [`Errno::EINVAL`] si la tranche déborde un `u32`/`u16` ; [`Errno::EBUSY`].
1189    pub fn submit_read_fixed(
1190        &mut self,
1191        fd: BorrowedFd<'_>,
1192        slice: RegisteredBufferSlice<'_>,
1193        offset: Option<u64>,
1194    ) -> Result<SubmissionToken, Errno> {
1195        let fd = fd.as_raw_fd();
1196        let len = u32::try_from(slice.len()).map_err(|_| Errno::EINVAL)?;
1197        let buf_index = u16::try_from(slice.index()).map_err(|_| Errno::EINVAL)?;
1198        let off = offset.unwrap_or(u64::MAX);
1199        let addr = slice.addr;
1200        self.submit_op(None, |sqe| {
1201            sqe.opcode = raw::IORING_OP_READ_FIXED;
1202            sqe.fd = fd;
1203            sqe.addr_or_splice_off_in = addr;
1204            sqe.len = len;
1205            sqe.off_or_addr2 = off;
1206            sqe.buf_index_or_group = buf_index;
1207        })
1208    }
1209
1210    /// `IORING_OP_WRITE_FIXED` (5) : écrit dans `fd` depuis la tranche d'un buffer
1211    /// enregistré. Complétion via [`Completion::into_result`] (nombre d'octets).
1212    ///
1213    /// # Errors
1214    ///
1215    /// Voir [`IoUring::submit_read_fixed`].
1216    pub fn submit_write_fixed(
1217        &mut self,
1218        fd: BorrowedFd<'_>,
1219        slice: RegisteredBufferSlice<'_>,
1220        offset: Option<u64>,
1221    ) -> Result<SubmissionToken, Errno> {
1222        let fd = fd.as_raw_fd();
1223        let len = u32::try_from(slice.len()).map_err(|_| Errno::EINVAL)?;
1224        let buf_index = u16::try_from(slice.index()).map_err(|_| Errno::EINVAL)?;
1225        let off = offset.unwrap_or(u64::MAX);
1226        let addr = slice.addr;
1227        self.submit_op(None, |sqe| {
1228            sqe.opcode = raw::IORING_OP_WRITE_FIXED;
1229            sqe.fd = fd;
1230            sqe.addr_or_splice_off_in = addr;
1231            sqe.len = len;
1232            sqe.off_or_addr2 = off;
1233            sqe.buf_index_or_group = buf_index;
1234        })
1235    }
1236}
1237
1238impl Completion {
1239    /// Slot choisi par le kernel pour une variante « direct » à cible
1240    /// [`FixedSlotTarget::Alloc`] (rendu dans `cqe->res`).
1241    ///
1242    /// # Errors
1243    ///
1244    /// L'[`Errno`] décodé de la complétion (ex. [`Errno::ENFILE`] si la plage
1245    /// d'auto-allocation est pleine), ou [`Errno::EINVAL`] si `res` est négatif
1246    /// hors errno.
1247    pub fn allocated_slot(self) -> Result<u32, Errno> {
1248        let res = self.into_result()?;
1249        u32::try_from(res).map_err(|_| Errno::EINVAL)
1250    }
1251}
1252
1253// ───────────────────────────────────────────────────────────────────────────
1254// Tests
1255// ───────────────────────────────────────────────────────────────────────────
1256//
1257// Les tests **purs** (encodage `FixedSlotTarget`, propagation d'erreur via le
1258// simulateur de syscalls) tournent **sous Miri**. Les tests d'**intégration**
1259// (kernel réel : registration, variantes direct, read/write fixed) portent
1260// `#[cfg_attr(miri, ignore)]` — Miri ne modélise pas `io_uring_register`/`enter`.
1261// La **sûreté de lifetime** (une référence ne survit pas au désenregistrement)
1262// est prouvée par les doctests `compile_fail` sur `FixedFdTable::unregister` et
1263// `RegisteredBuffers::unregister`.
1264
1265#[cfg(test)]
1266mod tests;