Spec couche 1 — air-memory (allocateurs spécialisés + comptabilité mémoire)
Spécification technique — Version 1.0. Couche 1 « Primitives système ».
Position et méthode
air-memory fournit des allocateurs spécialisés — arène (bump), pool
d’objets, slab — et une comptabilité mémoire par composant (au service du
Principe 9 : tenir les budgets mémoire sur matériel modeste, Pi 4). Elle s’appuie
sur air-base-lib (AirError/AirResult). API Rust d’abord (ABI C
différée). Méthode doc-d’abord.
La crate couche 1 la plus «
unsafeinterne ». Un allocateur manipule de la mémoire brute (pointeur de bump,MaybeUninit, alignement, réutilisation de slots). Conséquences non négociables : aucune fonctionunsafeexposée (frontière couche 1),// SAFETY:sur chaque blocunsafe, Miri obligatoire (détection d’UB sur les internes), property-based sur les séquences alloc/reset, couverture 100 % (Principe 1).
air-memoryv1 ne consomme PAS la couche 0. Backing heap par défaut (allocateur global Rust) → pure-Rust, aucun syscall. Le backingMmapRegion(couche 0) est enfichable mais différé (cf. §5). C’est donc une crate couche 1 qui ne touche pas encore au journal de dette doc couche 0.
Conventions
- Mono-thread par défaut (décision) : les allocateurs allouent via
&mut self(exclusivité garantie par le borrow checker, zéro coût de synchro). Le partage entre threads est à la charge de l’appelant (Mutex, ou une instance par thread). Des variantesSync(lock / free-list atomique) sont différées à un besoin réel (cf. §7). - Comptabilité opt-in : un allocateur peut être rattaché à un
AirMemoryTracker; sans tracker, zéro surcoût. - Nommage ADR-029, arithmétique défensive (Principe 2 :
checked_*sur tout calcul de taille/offset/alignement), aucune dépendance externe en v1.
Section 1 — Comptabilité mémoire par composant
#![allow(unused)]
fn main() {
/// Compteur de mémoire d'un **composant** (nommé). Handle **partageable** et
/// interrogeable depuis un autre thread (compteurs atomiques) — même si les
/// allocateurs qui l'alimentent sont mono-thread.
#[derive(Clone)]
pub struct AirMemoryTracker { /* Arc<{ name, in_use: AtomicUsize, peak, live }> */ }
impl AirMemoryTracker {
pub fn new(component: &str) -> Self;
pub fn name(&self) -> &str;
/// Instantané atomique de la consommation courante.
pub fn usage(&self) -> AirMemoryUsage;
// Internes (`pub(crate)`), appelés par les allocateurs :
// fn record_alloc(&self, bytes: usize); // in_use += ; peak = max ; live += 1
// fn record_free(&self, bytes: usize); // in_use -= ; live -= 1
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AirMemoryUsage {
pub bytes_in_use: usize,
pub bytes_peak: usize,
pub live_allocations: usize,
}
}
Décision. La comptabilité est per-composant par construction : elle mesure
exactement « combien tient ce composant via les allocateurs Air » (la question
budget du Principe 9), pas le heap total du process. Coût : un +atomic par
alloc/free seulement si un tracker est attaché. Un allocateur sans tracker
ne paie rien. (Un éventuel allocateur global traçant exhaustif relève d’un outil
distinct, hors v1 — cf. §7.)
Tests. record_alloc/free mettent à jour in_use/peak/live
correctement (y compris peak monotone) ; usage() cohérent ; interrogeable
depuis un autre thread pendant qu’un allocateur mono-thread l’alimente
(property-based concurrent lecture seule côté tracker).
Section 2 — Arène (bump allocator) : AirArena
#![allow(unused)]
fn main() {
/// Arène à **pointeur de bump** : allocation O(1) par avancement d'un offset ;
/// **libération en masse** via `reset` (rembobine l'offset). Mono-thread.
pub struct AirArena { /* backing + offset + tracker optionnel */ }
impl AirArena {
/// Arène **heap** d'une capacité donnée.
/// # Errors `OutOfMemory` si l'allocation backing échoue.
pub fn with_capacity(capacity: usize) -> AirResult<Self>;
/// Arène sur un backing **enfichable** (cf. §5).
pub fn with_backing(backing: AirBacking) -> AirResult<Self>;
/// Rattache un tracker (builder).
pub fn tracked(self, tracker: &AirMemoryTracker) -> Self;
/// Alloue une valeur dans l'arène ; rend `&mut T` (durée de vie liée à `&mut self`).
/// # Errors `OutOfMemory` si capacité dépassée.
pub fn alloc<T>(&mut self, value: T) -> AirResult<&mut T>;
/// Alloue et copie une tranche `Copy`.
pub fn alloc_slice_copy<T: Copy>(&mut self, src: &[T]) -> AirResult<&mut [T]>;
/// Réserve une tranche **non initialisée** (l'appelant l'initialise).
pub fn alloc_uninit_slice<T>(&mut self, len: usize) -> AirResult<&mut [MaybeUninit<T>]>;
/// **Libère tout** d'un coup (rembobine le bump à 0). Le `&mut self` garantit
/// qu'aucune référence allouée ne survit (borrow checker).
pub fn reset(&mut self);
pub fn bytes_used(&self) -> usize;
pub fn capacity(&self) -> usize;
}
}
Contrat des destructeurs (à graver, précondition documentée).
AirArenaapplique une sémantique de libération en masse :reset/Dropde l’arène n’exécute PAS leDropdes valeurs allouées (rembobinage d’offset, pas de destruction individuelle). Conséquence : n’allouer dans une arène que des types dont sauter leDropest acceptable —Copy/POD, ou données de travail sans ressource possédée. Pour des types qui doivent être détruits (possèdent un FD, un buffer heap…), utiliserAirObjectPool(qui respecteDrop) ou les gérer à la main. Ce contrat est documenté en tête de chaque méthode d’allocation (pas de surprise silencieuse).
Sûreté. Alignement calculé avec align_offset/checked_* (jamais
d’arithmétique de pointeur nue non vérifiée) ; // SAFETY: sur l’écriture dans la
mémoire réservée et la fabrication des références (la mémoire est dans les bornes,
alignée, exclusive via &mut self). Miri sur alloc/reset, alignements
hétérogènes, types de tailles variées.
Performance. alloc = quelques instructions (comparer offset, avancer). Cas
canonique : mémoire de travail par tâche/par frame, libérée d’un coup.
Section 3 — Pool d’objets : AirObjectPool<T>
#![allow(unused)]
fn main() {
/// Pool recyclant des instances de `T` (évite alloc/free répétés d'un type
/// fréquent). **Respecte `Drop`** : les `T` retenus sont détruits au `Drop` du
/// pool ; au retour d'un objet, il est conservé (et réinitialisé si fourni).
/// Handle **mono-thread, clonable** (partage interne `Rc` + `RefCell`).
#[derive(Clone)]
pub struct AirObjectPool<T> { /* Rc<RefCell<{ free: Vec<T>, factory, reset, tracker }>> */ }
impl<T> AirObjectPool<T> {
/// Fabrique appelée quand le pool est vide.
pub fn new(factory: impl Fn() -> T + 'static) -> Self;
pub fn with_capacity(capacity: usize, factory: impl Fn() -> T + 'static) -> Self;
/// Fonction de réinitialisation appliquée au retour d'un objet (builder).
pub fn with_reset(self, reset: impl Fn(&mut T) + 'static) -> Self;
pub fn tracked(self, tracker: &AirMemoryTracker) -> Self;
/// Emprunte un objet (recyclé, sinon fabriqué). Le guard le **rend au pool**
/// à son `Drop`. `&self` (interior mutability) → **plusieurs objets** peuvent
/// être sortis simultanément.
pub fn acquire(&self) -> AirPooled<T>;
pub fn available(&self) -> usize;
}
/// Garde RAII : `Deref`/`DerefMut` vers `T` ; rend l'objet au pool au `Drop`.
pub struct AirPooled<T> { /* Rc<inner> + Option<T> */ }
impl<T> core::ops::Deref for AirPooled<T> { type Target = T; /* … */ }
impl<T> core::ops::DerefMut for AirPooled<T> { /* … */ }
}
Décision. Le pool utilise Rc<RefCell<…>> (mono-thread, pas Arc/Mutex
— zéro coût atomique) ; acquire(&self) permet N objets sortis en même temps
(contrairement à un &mut self qui n’en autoriserait qu’un). C’est le design
utilisable d’un pool. Le guard ne tient qu’un Rc (pas un emprunt du pool).
Sûreté / tests. acquire quand le pool est vide fabrique ; quand non vide
recycle (vérifier l’identité d’objet recyclé) ; reset appliqué au retour ; le
Drop du pool détruit bien tous les objets retenus (Miri : pas de fuite, pas
de double-drop) ; emprunts RefCell jamais en conflit (un seul emprunt à la fois
en interne, court). Property-based : séquences acquire/release arbitraires →
invariant available + sortis = total fabriqué.
Section 4 — Slab : AirSlab<T>
#![allow(unused)]
fn main() {
/// Stockage à **slots**, indices **stables** et **anti-ABA** (clé = index +
/// génération). Même schéma éprouvé que le slab S1 d'io_uring (Temps 1). Mono-thread.
pub struct AirSlab<T> { /* Vec<Entry<T>> + free-list d'index + générations */ }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AirSlabKey { /* index: u32, generation: u32 (opaque) */ }
impl<T> AirSlab<T> {
pub fn new() -> Self;
pub fn with_capacity(capacity: usize) -> Self;
/// Insère ; rend une clé stable (réutilisation de slot avec génération++).
pub fn insert(&mut self, value: T) -> AirSlabKey;
/// Retire ; `None` si la clé est périmée (slot réutilisé → génération ≠).
pub fn remove(&mut self, key: AirSlabKey) -> Option<T>;
pub fn get(&self, key: AirSlabKey) -> Option<&T>;
pub fn get_mut(&mut self, key: AirSlabKey) -> Option<&mut T>;
pub fn len(&self) -> usize;
pub fn is_empty(&self) -> bool;
}
}
Décision. La génération par slot empêche le bug ABA (une clé d’un slot
libéré puis réutilisé est rejetée — remove/get rendent None). Drop du slab
détruit les valeurs vivantes restantes (respecte Drop). Débordement de
génération géré par wrapping_add documenté (probabilité de collision négligeable,
ou saturation selon décision d’implémentation — à fixer, cf. slab io_uring).
Tests. insert/get/remove ; clé périmée rejetée (test ABA : insert→remove→
insert sur le même slot→ ancienne clé None) ; Drop détruit les vivants (Miri) ;
property-based (suite d’insert/remove → cohérence len / clés valides).
Section 5 — Backing enfichable : AirBacking
#![allow(unused)]
fn main() {
/// Source de mémoire brute d'une `AirArena`. **Enfichable** : heap par défaut ;
/// d'autres backings s'ajoutent **sans casser l'API** (type opaque + constructeurs).
pub struct AirBacking { /* enum interne #[non_exhaustive] : Heap(Box<[u8]>) | … */ }
impl AirBacking {
/// Backing **heap** (bloc `Box<[u8]>` de l'allocateur global).
/// # Errors `OutOfMemory`.
pub fn heap(capacity: usize) -> AirResult<Self>;
pub fn len(&self) -> usize;
// pub fn mmap(region: MmapRegion) -> Self; // DIFFÉRÉ (cf. §7)
}
}
Décision. v1 = heap uniquement, mais l’API est enfichable dès maintenant
(AirBacking opaque, variantes internes #[non_exhaustive]) : ajouter un backing
MmapRegion (couche 0 — aligné page, munmap déterministe,
io_uring-enregistrable au Temps 3a) plus tard ne cassera pas l’API d’AirArena.
Récapitulatif air-memory
| Domaine | API principale |
|---|---|
| Comptabilité | AirMemoryTracker, AirMemoryUsage (per-composant, opt-in, atomique) |
| Arène | AirArena (bump, libération en masse ; ne run pas Drop) |
| Pool d’objets | AirObjectPool<T>, AirPooled<T> (recycle, respecte Drop, Rc/RefCell) |
| Slab | AirSlab<T>, AirSlabKey (slots stables, anti-ABA, respecte Drop) |
| Backing | AirBacking (heap ; mmap différé, API non-cassante) |
Différé : backing MmapRegion, variantes Sync (thread-safe), allocateur
global traçant exhaustif, surface ABI C.
Dépendances (règle des 80 %, ADR-024)
air-base-lib(erreurs). Aucune dépendance externe en v1 (allocateur global Rust +core/alloc). Pas deMmapRegiontant que le backing mmap est différé.
Stratégie de tests
- Couverture 100 % lignes + branches (Principe 1).
- Miri (impératif, c’est du code mémoire bas niveau) :
AirArena(alloc/ alignement/reset),AirObjectPool(pas de fuite/double-drop),AirSlab(slots/ générations/Drop). Aucun UB. - Property-based (proptest) : séquences alloc/reset (arène) ; acquire/release (pool, invariant de conservation) ; insert/remove (slab, clés valides, ABA).
- Unitaires : arithmétique de taille/alignement aux bords (
checked_*, capacité dépassée →OutOfMemory),peakmonotone, clé périmée rejetée. - Doctests : exemples compilent et passent.
- (Pas de fuzzing dédié en v1 : pas de décodage de données externes — les entrées sont des tailles/alignements, couverts par proptest. À ajouter si un backing ingère des données externes.)
Décisions de fond
- Comptabilité per-composant (atomique, opt-in) — répond à la question budget du Principe 9 ; pas un allocateur global (qui mesurerait le total mais attribuerait mal). Zéro coût sans tracker.
- Mono-thread par défaut (
&mut self, zéro synchro) ;Syncdifféré (Principe 5 : ne pas payer l’atomique pour le cas courant des arenas). - Arène = libération en masse,
Dropnon exécuté — contrat documenté (pas de surprise) ; pour les types à détruire →AirObjectPool(qui respecteDrop). - Pool
Rc/RefCell— mono-thread mais N objets sortables (interior mutability), pasArc/Mutex. - Slab anti-ABA (clé = index + génération) — schéma éprouvé du slab io_uring.
- Backing enfichable — heap en v1,
MmapRegionajoutable sans casser l’API.
Travail à reprendre
- Backing
MmapRegion(AirBacking::mmap) — grandes arenas alignées page,munmapdéterministe, buffers io_uring-enregistrables (Temps 3a). Première consommation couche 0 d’air-memory. - Variantes
Syncdes allocateurs (lock / free-list atomique loom-vérifiée) si un besoin de pool partagé apparaît. - Surface ABI C d’
air-memory. - Autres crates couche 1 :
air-process,air-socket,air-crypto,air-device,air-thread; etair-base-lib(ABI C).
Licence du document : MPL 2.0
Statut : Spécification technique d’air-memory (couche 1). API Rust ; ABI C
différée. Backing heap (mmap enfichable différé) ; mono-thread (Sync différé).