air_sys_types/fd.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//! Descripteurs de fichiers propriétaires et empruntés — types FD natifs d'Air.
6//!
7//! Ce module définit les types FD de la couche 0 sans dépendre de
8//! `std::os::fd` : il n'utilise que `core` plus un appel `close(2)` en
9//! assembleur inline pour le `Drop`. C'est le préalable au passage
10//! `#![no_std]` de la crate (cf. ADR-048 Amendement 1) : `core::os::fd`
11//! n'existe pas sur Rust stable, Air doit donc posséder ses propres types.
12//!
13//! # Conception — miroir de `std::os::fd`
14//!
15//! L'API publique est calquée sur `std::os::fd` (mêmes noms de types,
16//! méthodes et traits, mêmes signatures) afin que la migration des sites
17//! d'appel soit purement mécanique et que le modèle mental reste celui de
18//! la bibliothèque standard :
19//!
20//! - [`RawFd`] : alias de `i32`, identique à `std::os::fd::RawFd`.
21//! - [`OwnedFd`] : possède le FD, le ferme à `Drop` (RAII).
22//! - [`BorrowedFd<'fd>`] : emprunte le FD pour la durée `'fd`, ne ferme rien.
23//! - traits [`AsRawFd`], [`AsFd`], [`FromRawFd`], [`IntoRawFd`].
24//!
25//! # Invariant de validité `-1` et absence de niche (contrainte stable)
26//!
27//! `std::os::fd` réserve la valeur `-1` (sentinelle d'erreur du kernel)
28//! comme **niche**, ce qui donne à `Option<OwnedFd>` la même taille qu'un
29//! `OwnedFd`. La std obtient cette niche via l'attribut interne
30//! `rustc_layout_scalar_valid_range_start/end`, qui exige la *feature*
31//! nightly `rustc_attrs` — **indisponible sur Rust stable** (et le passage
32//! stable est précisément la raison d'être d'ADR-048).
33//!
34//! **Cette niche ne peut pas être répliquée sur stable.** Une tentative de la
35//! simuler en stockant le FD sous forme transformée (`!raw` dans un
36//! `NonZeroI32`, biais `+1`, …) introduirait un bug **grave** : les types FD
37//! sont incrustés *tels quels* dans des structures `#[repr(C)]` miroir du
38//! kernel — au premier chef [`crate::poll::PollFd`], bit-pour-bit
39//! `struct pollfd` dont le champ `fd` **est** un [`BorrowedFd`]. Le kernel lit
40//! alors le motif binaire brut du champ : il **doit** valoir le FD réel. Toute
41//! transformation le corromprait (le kernel polît/ferme un mauvais FD).
42//!
43//! Air stocke donc le FD **brut** dans un `i32` (`#[repr(transparent)]`),
44//! exactement comme le motif mémoire de la std :
45//!
46//! - motif binaire == FD réel ⇒ incrustation `#[repr(C)]` correcte (PollFd…) ;
47//! - même layout mémoire que `std::os::fd::OwnedFd`/`BorrowedFd` (transparent
48//! sur `i32`), donc le pont [`RawFd`] est exact ;
49//! - **conséquence** : `size_of::<Option<OwnedFd>>() == 8` (pas de niche),
50//! contre 4 pour la std. C'est le seul écart observable, assumé et scellé
51//! comme contrat air-stable jusqu'à ce qu'un mécanisme de niche stable
52//! existe (le cas échéant, ré-ouvrir par RFC sans changer le motif binaire).
53//!
54//! L'invariant « `raw != -1` » reste une **précondition** documentée des
55//! constructeurs `unsafe` (`from_raw_fd`/`borrow_raw`), comme dans la std ;
56//! il n'est simplement plus encodé dans la représentation.
57
58use core::marker::PhantomData;
59
60/// Descripteur de fichier brut : l'entier signé manipulé par le kernel.
61///
62/// Identique à `std::os::fd::RawFd` (`i32`). Une valeur négative `-1` est la
63/// sentinelle d'erreur kernel ; elle n'est jamais un FD valide et reste
64/// interdite dans [`OwnedFd`] / [`BorrowedFd`].
65pub type RawFd = i32;
66
67// ─────────────────────────────────────────────────────────────────────────
68// close(2) — syscall minimal en assembleur inline, par architecture.
69//
70// `air-sys-types` n'embarquait aucun syscall jusqu'ici ; la spec couche 0
71// l'autorise pour le seul besoin du `Drop` de `OwnedFd`. Numéros de syscall
72// `asm-generic` : x86_64 = 3, aarch64 = 57.
73// ─────────────────────────────────────────────────────────────────────────
74
75#[cfg(target_arch = "x86_64")]
76const SYS_CLOSE: i64 = 3;
77#[cfg(target_arch = "aarch64")]
78const SYS_CLOSE: i64 = 57;
79
80/// Ferme `fd` en best-effort, **en ignorant le résultat**.
81///
82/// Utilisé uniquement par le `Drop` de [`OwnedFd`]. Sur Linux, le FD est
83/// libéré par le kernel même lorsque `close(2)` renvoie `EINTR` : il ne faut
84/// donc **jamais** ré-essayer (un retry fermerait un FD potentiellement
85/// recyclé entre-temps par un autre thread). La fonction explicite [`close`]
86/// de `air-sys-syscall` reste disponible quand l'appelant veut récupérer
87/// l'erreur.
88///
89/// [`close`]: https://man7.org/linux/man-pages/man2/close.2.html
90#[cfg(target_arch = "x86_64")]
91#[inline]
92fn close_ignoring_result(fd: RawFd) {
93 // SAFETY:
94 // - `SYS_CLOSE` (3) prend un unique argument entier dans `rdi` ; close(2)
95 // ne déréférence aucune mémoire utilisateur, l'appel est donc sûr pour
96 // tout entier.
97 // - `fd` provient d'un `OwnedFd` valide en cours de destruction : il est
98 // ouvert et possédé, et n'est utilisé qu'ici avant oubli.
99 // - clobbers `rcx`/`r11` (convention `syscall`), résultat ignoré.
100 unsafe {
101 core::arch::asm!(
102 "syscall",
103 in("rax") SYS_CLOSE,
104 in("rdi") i64::from(fd),
105 lateout("rax") _,
106 lateout("rcx") _,
107 lateout("r11") _,
108 options(nostack, preserves_flags),
109 );
110 }
111}
112
113/// Variante aarch64 de [`close_ignoring_result`].
114#[cfg(target_arch = "aarch64")]
115#[inline]
116fn close_ignoring_result(fd: RawFd) {
117 // SAFETY:
118 // - `SYS_CLOSE` (57) prend un unique argument entier dans `x0` ; close(2)
119 // ne déréférence aucune mémoire utilisateur, l'appel est donc sûr pour
120 // tout entier.
121 // - `fd` provient d'un `OwnedFd` valide en cours de destruction : il est
122 // ouvert et possédé, et n'est utilisé qu'ici avant oubli.
123 unsafe {
124 core::arch::asm!(
125 "svc 0",
126 in("x8") SYS_CLOSE,
127 inout("x0") i64::from(fd) => _,
128 options(nostack, preserves_flags),
129 );
130 }
131}
132
133// ─────────────────────────────────────────────────────────────────────────
134// OwnedFd
135// ─────────────────────────────────────────────────────────────────────────
136
137/// Un descripteur de fichier **possédé**, fermé automatiquement à `Drop`.
138///
139/// `OwnedFd` est le garant RAII de la couche 0 : tant qu'il vit, le FD est
140/// garanti ouvert ; quand il est détruit, `close(2)` est appelé une seule
141/// fois. Pour récupérer une éventuelle erreur de fermeture, consommer le FD
142/// via `air_sys_syscall::fs::close` plutôt que de laisser jouer le `Drop`.
143///
144/// Représentation : `#[repr(transparent)]` sur un [`RawFd`] (`i32`) contenant
145/// le FD **brut** — motif binaire identique à `std::os::fd::OwnedFd`. Pas de
146/// niche sur stable (cf. doc du module) : `size_of::<Option<OwnedFd>>() == 8`.
147#[repr(transparent)]
148pub struct OwnedFd {
149 /// FD brut possédé. Précondition d'`OwnedFd` : `!= -1` (sentinelle kernel).
150 fd: RawFd,
151}
152
153impl OwnedFd {
154 /// Retourne le [`RawFd`] brut détenu (usage interne).
155 #[inline]
156 fn raw(&self) -> RawFd {
157 self.fd
158 }
159}
160
161// Conformément à `std::os::fd`, les opérations publiques d'`OwnedFd`
162// (`as_fd`, `into_raw_fd`, `from_raw_fd`, `as_raw_fd`) sont exposées via les
163// **traits** miroir [`AsFd`], [`IntoRawFd`], [`FromRawFd`], [`AsRawFd`] (cf.
164// plus bas) — et non comme méthodes inhérentes. Cela calque exactement l'API
165// std : un site d'appel importe le trait (`use air_sys_types::fd::AsFd`) puis
166// appelle `owned.as_fd()`, comme il importait `std::os::fd::AsFd` avant.
167
168impl Drop for OwnedFd {
169 #[inline]
170 fn drop(&mut self) {
171 // Best-effort : on ferme une seule fois et on ignore le résultat.
172 // Linux libère le FD même sur EINTR ⇒ pas de retry (cf. ADR-021 §2).
173 close_ignoring_result(self.raw());
174 }
175}
176
177impl core::fmt::Debug for OwnedFd {
178 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
179 f.debug_struct("OwnedFd").field("fd", &self.raw()).finish()
180 }
181}
182
183// ─────────────────────────────────────────────────────────────────────────
184// BorrowedFd
185// ─────────────────────────────────────────────────────────────────────────
186
187/// Un descripteur de fichier **emprunté**, valide pour la durée `'fd`.
188///
189/// `BorrowedFd` n'a **pas** de `Drop` : il ne ferme jamais le FD. Sa durée de
190/// vie `'fd` garantit qu'il ne survit pas au propriétaire ([`OwnedFd`] ou
191/// ressource équivalente). C'est l'argument à passer à un syscall qui lit un
192/// FD sans en prendre l'ownership.
193///
194/// Même représentation que [`OwnedFd`] : `#[repr(transparent)]` sur un
195/// [`RawFd`] brut, plus un marqueur de durée de vie de taille nulle. C'est ce
196/// qui permet d'incruster un `BorrowedFd` dans une structure `#[repr(C)]`
197/// miroir du kernel (ex. [`crate::poll::PollFd`] = `struct pollfd`) : le champ
198/// `fd` y vaut bien le FD brut attendu par le kernel.
199#[repr(transparent)]
200#[derive(Clone, Copy)]
201pub struct BorrowedFd<'fd> {
202 /// FD brut emprunté. Précondition : `!= -1`.
203 fd: RawFd,
204 _phantom: PhantomData<&'fd OwnedFd>,
205}
206
207impl BorrowedFd<'_> {
208 /// Retourne le [`RawFd`] brut emprunté (usage interne).
209 #[inline]
210 fn raw(&self) -> RawFd {
211 self.fd
212 }
213
214 /// Construit un `BorrowedFd` empruntant `fd` pour la durée `'fd`.
215 ///
216 /// # Safety
217 ///
218 /// - `fd` doit être un descripteur ouvert et valide pour toute la durée
219 /// `'fd` choisie par l'appelant ;
220 /// - `fd` doit être **différent de `-1`** ;
221 /// - aucun ownership n'est transféré : le FD ne doit pas être fermé tant
222 /// que des `BorrowedFd` de durée `'fd` peuvent encore l'utiliser.
223 #[must_use]
224 #[inline]
225 pub unsafe fn borrow_raw<'fd>(fd: RawFd) -> BorrowedFd<'fd> {
226 BorrowedFd {
227 fd,
228 _phantom: PhantomData,
229 }
230 }
231}
232
233impl core::fmt::Debug for BorrowedFd<'_> {
234 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
235 f.debug_struct("BorrowedFd")
236 .field("fd", &self.raw())
237 .finish()
238 }
239}
240
241// ─────────────────────────────────────────────────────────────────────────
242// Traits miroir de `std::os::fd`
243// ─────────────────────────────────────────────────────────────────────────
244
245/// Expose le [`RawFd`] brut d'une ressource, sans transfert d'ownership.
246///
247/// Miroir de `std::os::fd::AsRawFd`.
248pub trait AsRawFd {
249 /// Retourne le FD brut sous-jacent.
250 fn as_raw_fd(&self) -> RawFd;
251}
252
253/// Emprunte le FD d'une ressource sous forme de [`BorrowedFd`].
254///
255/// Miroir de `std::os::fd::AsFd`. À préférer à [`AsRawFd`] : la durée de vie
256/// retournée empêche l'usage après fermeture.
257pub trait AsFd {
258 /// Emprunte le FD pour la durée de l'emprunt `&self`.
259 fn as_fd(&self) -> BorrowedFd<'_>;
260}
261
262/// Construit une ressource possédant un [`RawFd`].
263///
264/// Miroir de `std::os::fd::FromRawFd`.
265pub trait FromRawFd {
266 /// Prend l'ownership de `fd`.
267 ///
268 /// # Safety
269 ///
270 /// Mêmes préconditions que [`OwnedFd::from_raw_fd`] : `fd` ouvert, valide,
271 /// `!= -1`, ownership cédé.
272 unsafe fn from_raw_fd(fd: RawFd) -> Self;
273}
274
275/// Consomme une ressource pour en extraire le [`RawFd`] brut possédé.
276///
277/// Miroir de `std::os::fd::IntoRawFd`.
278pub trait IntoRawFd {
279 /// Consomme `self` et restitue le FD brut **sans le fermer**.
280 fn into_raw_fd(self) -> RawFd;
281}
282
283impl AsRawFd for OwnedFd {
284 #[inline]
285 fn as_raw_fd(&self) -> RawFd {
286 self.raw()
287 }
288}
289
290impl AsFd for OwnedFd {
291 #[inline]
292 fn as_fd(&self) -> BorrowedFd<'_> {
293 // Aucun transfert d'ownership : l'emprunt est borné par `&self`, donc
294 // ne peut survivre au `OwnedFd`.
295 // SAFETY: `self.raw()` provient d'un `OwnedFd` valide (ouvert, != -1).
296 unsafe { BorrowedFd::borrow_raw(self.raw()) }
297 }
298}
299
300impl FromRawFd for OwnedFd {
301 /// # Safety
302 ///
303 /// - `fd` doit être un descripteur ouvert et valide ;
304 /// - `fd` doit être **différent de `-1`** (sentinelle kernel interdite) ;
305 /// - l'appelant cède l'ownership : `fd` ne doit plus être fermé ailleurs,
306 /// sous peine de double-`close` (faille de sécurité classique).
307 #[inline]
308 unsafe fn from_raw_fd(fd: RawFd) -> Self {
309 // Validité/ownership et `fd != -1` garantis par l'appelant (contrat
310 // `unsafe`) ; on stocke le FD brut tel quel (motif binaire = FD réel).
311 Self { fd }
312 }
313}
314
315impl IntoRawFd for OwnedFd {
316 #[inline]
317 fn into_raw_fd(self) -> RawFd {
318 let raw = self.raw();
319 // Ne pas fermer : on transfère l'ownership du FD à l'appelant.
320 core::mem::forget(self);
321 raw
322 }
323}
324
325impl AsRawFd for BorrowedFd<'_> {
326 #[inline]
327 fn as_raw_fd(&self) -> RawFd {
328 self.raw()
329 }
330}
331
332impl AsFd for BorrowedFd<'_> {
333 #[inline]
334 fn as_fd(&self) -> BorrowedFd<'_> {
335 *self
336 }
337}
338
339// ─────────────────────────────────────────────────────────────────────────
340// Interop `std::os::fd` — TRANSITOIRE
341//
342// Ponts `From` entre les types FD d'Air et ceux de la std, pour les crates
343// couche 1+ qui doivent encore franchir la frontière std (`std::fs::File`,
344// `std::net`, …). La conversion passe toujours par le [`RawFd`] brut (i32
345// identique), jamais par `transmute` (les représentations internes diffèrent,
346// cf. doc du module). Gardés derrière `#[cfg(any(feature = "std", test))]` : la
347// crate est `#![no_std]`, ces impls sont la seule partie du module qui référence
348// `std`. Activer la feature `std` (off par défaut) pour franchir la frontière.
349// ─────────────────────────────────────────────────────────────────────────
350
351#[cfg(any(feature = "std", test))]
352impl From<OwnedFd> for std::os::fd::OwnedFd {
353 #[inline]
354 fn from(owned: OwnedFd) -> Self {
355 // SAFETY: `into_raw_fd` cède un FD ouvert valide (!= -1) ; la std en
356 // prend l'ownership exclusif.
357 unsafe { std::os::fd::FromRawFd::from_raw_fd(owned.into_raw_fd()) }
358 }
359}
360
361#[cfg(any(feature = "std", test))]
362impl From<std::os::fd::OwnedFd> for OwnedFd {
363 #[inline]
364 fn from(owned: std::os::fd::OwnedFd) -> Self {
365 use std::os::fd::IntoRawFd;
366 // SAFETY: idem, sens inverse — la std cède un FD ouvert valide (!= -1).
367 unsafe { Self::from_raw_fd(owned.into_raw_fd()) }
368 }
369}
370
371#[cfg(any(feature = "std", test))]
372impl<'fd> From<BorrowedFd<'fd>> for std::os::fd::BorrowedFd<'fd> {
373 #[inline]
374 fn from(borrowed: BorrowedFd<'fd>) -> Self {
375 // SAFETY: même FD emprunté, même durée de vie `'fd` ; `!= -1` garanti
376 // par l'invariant de `BorrowedFd`.
377 unsafe { std::os::fd::BorrowedFd::borrow_raw(borrowed.raw()) }
378 }
379}
380
381#[cfg(any(feature = "std", test))]
382impl<'fd> From<std::os::fd::BorrowedFd<'fd>> for BorrowedFd<'fd> {
383 #[inline]
384 fn from(borrowed: std::os::fd::BorrowedFd<'fd>) -> Self {
385 use std::os::fd::AsRawFd;
386 // SAFETY: même FD emprunté, même durée de vie `'fd` ; `!= -1` garanti
387 // par l'invariant du `BorrowedFd` std.
388 unsafe { Self::borrow_raw(borrowed.as_raw_fd()) }
389 }
390}
391
392#[cfg(test)]
393mod tests;