Spec couche 1 — air-base-capi (ABI C → libair-base.so)
Spécification technique — Version 1.0. Première passe ABI C du projet (ADR-027
addendum C). Cadrage : docs/draft/abi-c-t1-design-fr.md.
Position et méthode
air-base-capi est la crate FFI dédiée qui expose air-base-lib (qui reste Rust
pur) en ABI C, produisant libair-base.so. Décisions de contrat :
- Erreurs :
AirStatusin-band, sanserrno,panic = "abort"— ADR-045. - Doc :
///export-quality → cbindgen → header committé → Doxygen + guides FR/EN — ADR-027. - Stabilité : ABI 10 ans, header = contrat diffé en CI — ADR-012.
Cette passe est le banc d’essai de la méthodologie de toute la future libc Air (vision libc : userland Rust, contrats redessinés, propriété mémoire explicite).
Structure de crate
crates/air-base-capi/
├── Cargo.toml # crate-type = ["cdylib","rlib"] ; dep air-base-lib ; [profile.release] panic="abort"
├── cbindgen.toml # config pinnée (déterminisme ADR-025)
├── include/air_base.h # HEADER GÉNÉRÉ + COMMITTÉ (contrat ABI, diffé en CI)
└── src/lib.rs # surface `pub extern "C"` + AirStatus + validation amont
Header régénéré par cargo xtask gen-capi-header (mainteneur) ; diff CI sur toute
dérive (air-abi-check/air-symver). cbindgen = dépendance d’outillage, à auditer
ADR-024/030 + consigner DEPENDENCIES.md/EXCEPTIONS.md à l’ajout.
Modèle d’erreurs — AirStatus (ADR-045)
#![allow(unused)]
fn main() {
#[repr(C)]
pub enum AirStatus {
Ok = 0,
NotFound = 1, PermissionDenied = 2, AlreadyExists = 3, InvalidInput = 4,
Interrupted = 5, WouldBlock = 6, Unsupported = 7, TimedOut = 8,
OutOfMemory = 9, BrokenResource = 10, Io = 11, InvalidData = 12, Other = 13,
NullArgument = 100, // pointeur requis NULL (validation amont, jamais de déréf)
BufferTooSmall = 101, // buffer fourni trop petit (cf. chaînes)
}
}
- Rendu in-band (valeur de retour) ; conversion
AirError → AirStatusmappe lekind(message dynamique non transporté en T1). air_status_message(AirStatus) -> *const c_char: chaîne statique (+0, jamais libérée).panic = "abort": aucuncatch_unwind, aucunAIR_PANIC(panic = bug Air → abort fail-fast ; tests enunwindvia Cargo).
Surface T1 — identifiants
Types POD #[repr(C)]. cbindgen émet :
typedef struct AirUuid { uint8_t bytes[16]; } AirUuid;
typedef struct AirId128 { uint8_t bytes[16]; } AirId128;
AirStatus air_uuid_new_v7(AirUuid *out); // out emprunté (+0), écrit sur AIR_STATUS_OK seul
AirStatus air_uuid_new_v4(AirUuid *out);
AirStatus air_uuid_to_hyphenated(const AirUuid *uuid, char *buf, size_t len); // len>=37 sinon BUFFER_TOO_SMALL
AirStatus air_uuid_parse(const char *s, AirUuid *out); // s emprunté, NUL-terminé
AirStatus air_id128_machine_id(AirId128 *out);
AirStatus air_id128_to_hex(const AirId128 *id, char *buf, size_t len); // len>=33
AirStatus air_monotonic_id_next(uint64_t *out);
const char *air_status_message(AirStatus status); // chaîne STATIQUE (ne pas libérer)
- Sorties chaînes par buffer fourni par l’appelant : zéro alloc côté Air, mémoire
+0 possédée par l’appelant, aucune
air_*_free(ownership trivial). Tailles documentées (UUID hyphené 37, hex 128 bits 33, NUL inclus). Buffer trop petit →BUFFER_TOO_SMALL(jamais de troncature silencieuse, ADR-032). - Clause de propriété dans chaque
///(ADR-027 §B) : retour possédé/emprunté, arguments +0, fonction de libération (ici : aucune en T1).
Tests de conformité ABI
Harnais C réel : un .c compilé contre include/air_base.h + linké sur
libair-base.so, exécuté en CI (compilateur C présent sur les runners) : round-trip
UUID parse/format, machine_id, monotone strictement croissant, BUFFER_TOO_SMALL sur
buffer court, NULL_ARGUMENT sur pointeur nul, air_status_message non-NULL. Plus le
diff du header (régénéré ≟ committé).
Couverture
Header généré exclu ; wrappers extern "C" minces ; pas de bras
catch_unwind/AIR_PANIC (supprimés) ; la logique réelle est déjà couverte à 100 %
dans air-base-lib. Bras d’erreur (NULL_ARGUMENT, BUFFER_TOO_SMALL) testés.
Tranches
- T1 : crate +
AirStatus+ validation amont + identifiants + cbindgen → header committé +xtask gen-capi-header+ harnais conformité ABI. - T2 : reste de la surface
air-base-lib(AirLog+ services) en C-ABI. - T3 : pipeline Doxygen + guides FR+EN (obligations dev C, propriété mémoire, thread-safety, exemples C compilables).
Tranche T2 — service de journalisation AirLog (C-ABI)
Décisions de conception (autonomie BDFL 2026-06-27). Établit le pattern handle opaque + propriété + thread-safety déclarée (ADR-027 §B) pour toute la future libc.
Types
AirLogLevel— enum#[repr(C)], valeurs syslog/journald0..=7(AIR_LOG_LEVEL_EMERGENCY=0…AIR_LOG_LEVEL_DEBUG=7). POD.AirLog— handle opaque (typedef struct AirLog AirLog;), jamais déréférencé par l’appelant C. Côté Rust :Box<air_base_lib::AirLog>(cœurArc<JournalSink>—Send+Sync).AirLogFields— handle opaque (builder de champs structurés).
Surface extern "C"
// ── AirLog : ouvert/fermé par l'appelant (propriété +1 au retour) ──────────
// namespace NULL = journal par défaut. *out reçoit un handle POSSÉDÉ par l'appelant
// (il DOIT air_log_close). Écrit *out sur AIR_STATUS_OK seul.
AirStatus air_log_open(const char *namespace_, AirLog **out);
// Libère le handle (ferme le socket). NULL → no-op. Après appel, `log` invalide.
void air_log_close(AirLog *log);
// Émet une entrée. log/message/fields EMPRUNTÉS (+0). fields NULL = aucun champ.
// N'échoue pas sur indisponibilité journald (compteur interne) ; rend
// AIR_STATUS_NULL_ARGUMENT si `log`/`message` NULL, AIR_STATUS_INVALID_DATA si
// `message` non-UTF-8.
AirStatus air_log_emit(const AirLog *log, AirLogLevel level,
const char *message, const AirLogFields *fields);
// Nombre d'entrées perdues (journald injoignable). out emprunté (+0).
AirStatus air_log_lost_count(const AirLog *log, uint64_t *out);
// ── AirLogFields : builder possédé par l'appelant (propriété +1 au retour) ──
AirStatus air_log_fields_new(AirLogFields **out); // +1 → air_log_fields_free
void air_log_fields_free(AirLogFields *fields); // NULL → no-op
// Ajoute un champ texte. fields emprunté-mut (+0). key/value empruntés, NUL-term.
// Clé non conforme journald ([A-Z][A-Z0-9_]*) → rejet silencieux (compteur interne).
AirStatus air_log_fields_add(AirLogFields *fields, const char *key, const char *value);
// Champ binaire (value de `len` octets, pas de NUL requis).
AirStatus air_log_fields_add_bytes(AirLogFields *fields, const char *key,
const uint8_t *value, size_t len);
Propriété mémoire (clause ADR-027 §B — à reporter dans chaque ///)
| Fonction | Retour | Arguments | Libération |
|---|---|---|---|
air_log_open | *out possédé +1 | namespace_ emprunté +0 | air_log_close |
air_log_emit | (statut) | log/message/fields empruntés +0 | — |
air_log_close | — | consomme log | (c’est la libération) |
air_log_fields_new | *out possédé +1 | — | air_log_fields_free |
air_log_fields_add[_bytes] | (statut) | fields emprunté-mut +0 ; key/value +0 | — |
Thread-safety déclarée (ADR-027 §B.4)
AirLog= ThreadSafe : cœurArc<JournalSink>+ envoi datagramme atomique ;air_log_emit/air_log_lost_countsont sûrs depuis plusieurs threads sur le même handle.air_log_closeexige l’absence d’usage concurrent (consomme).AirLogFields= non thread-safe : muté paradd; le construire sur un thread, puis le passer (emprunté en lecture) àair_log_emit.
Notes d’implémentation
panic = "abort"(déjà workspace) ; pas decatch_unwind. Validation null amont.- Pas de troncature/perte silencieuse (ADR-032) :
addreporte un statut ; le rejet de clé non conforme est compté (exposable plus tard via un accesseur si besoin). - Header régénéré (cbindgen) + committé ; diff vérifié par le hook barrière (T1).
- Couverture : viser ≥ plancher couche 1 ; exceptions STRUCTURAL doc in-code (ADR-035).
Tranche T3 — mesure du temps (AirInstant / AirDuration, POD)
Décisions de conception (autonomie BDFL 2026-06-27). Expose la mesure d’écoulement
monotone d’air-base-lib::time (cf. crates/air-base-lib/src/time.rs) en POD ABI C
purs #[repr(C)] — premier service C-ABI sans handle (calque le pattern POD de
T1 : AirStatus in-band, validation null amont, out écrit sur AIR_STATUS_OK seul).
Types
AirInstant— POD#[repr(C)] { int64_t nanos; }, instant monotone (CLOCK_MONOTONIC). On n’expose pas l’i128interne d’air-base-lib(non portable en ABI C) : uni64de nanosecondes couvre ~292 ans d’uptime. La valeur absolue est opaque (calque la doctrine d’AirInstant: pas d’« epoch monotone » comparable entre processus) — seules les différences ont un sens.AirDuration— POD#[repr(C)] { int64_t seconds; uint32_t nanoseconds; }, façonstruct timespec(nanoseconds ∈ [0, 1e9)). Non négative (cas d’usage : mesure d’écoulement). La durée signéei128interne n’est pas exposée : la projection délègue àLibDuration::to_std_saturating(couche 1, couverte à 100 %), donc durée négative ⇒{0, 0}(sondage, cohérentto_std_saturating) et secondes excédanti64⇒ bornées (TryFrom/unwrap_or(i64::MAX), jamais d’as).
Surface extern "C"
typedef struct AirInstant { int64_t nanos; } AirInstant;
typedef struct AirDuration { int64_t seconds; uint32_t nanoseconds; } AirDuration;
// Horloge monotone (couche 0). out +0, écrit sur AIR_STATUS_OK seul ; peut échouer
// (horloge) → miroir AirStatus.
AirStatus air_instant_now(AirInstant *out);
// now - *since (saturé à 0 si *since est dans le futur). since/out empruntés +0.
AirStatus air_instant_elapsed(const AirInstant *since, AirDuration *out);
// *later - *earlier (saturé à 0 si later < earlier). Pur calcul, n'échoue jamais
// hors NULL_ARGUMENT.
AirStatus air_instant_duration_since(const AirInstant *later,
const AirInstant *earlier, AirDuration *out);
// Constructeurs de durée (secondes débordantes bornées défensivement).
AirStatus air_duration_from_secs(uint64_t s, AirDuration *out);
AirStatus air_duration_from_millis(uint64_t ms, AirDuration *out);
AirStatus air_duration_from_nanos(uint64_t ns, AirDuration *out);
// seconds + nanoseconds/1e9 (conversion lossy localisée, calque AirDuration::as_secs_f64).
AirStatus air_duration_as_secs_f64(const AirDuration *d, double *out);
- Projection sans accesseur de valeur absolue :
air-base-librefuse à dessein d’exposer la valeur brute d’unAirInstant. La frontière utilise une origine monotone déterministe (from_monotonic_micros(0, 0),nanos = 0) comme point de référence neutre pour projeter un instant sur uni64de nanosecondes (et le reconstruire) via la seule API publique (duration_since/saturating_add). - Arithmétique défensive (Principe 2) : toute recomposition est
saturating_*; toute conversion lossy est commentée et localisée (jamais d’asnon justifié).
Propriété mémoire (clause ADR-027 §B — reportée dans chaque ///)
| Fonction | Retour | Arguments | Libération |
|---|---|---|---|
air_instant_now | *out POD +0 (sur OK) | — | — (POD trivial) |
air_instant_elapsed | *out POD +0 (sur OK) | since emprunté +0 | — |
air_instant_duration_since | *out POD +0 (sur OK) | later/earlier empruntés +0 | — |
air_duration_from_{secs,millis,nanos} | *out POD +0 (sur OK) | — | — |
air_duration_as_secs_f64 | *out (double) +0 (sur OK) | d emprunté +0 | — |
Thread-safety déclarée (ADR-027 §B.4)
- Toutes ces fonctions sont réentrantes / thread-safe : sans état partagé
(lecture d’horloge kernel + POD locaux à l’appelant). Sûres d’appel concurrent depuis
plusieurs threads, chacun avec ses propres
out. Aucune n’aliase de ressource.
Notes d’implémentation
- POD purs (
AirInstant/AirDuration) ajoutés à[export] includedecbindgen.toml. air_instant_now/air_instant_elapsedpeuvent relayer une erreur d’horloge (AirStatus::from) : bras STRUCTURAL (ADR-035) —clock_gettime(CLOCK_MONOTONIC)est structurellement infaillible sur un système démarré (cf.air-base-lib::time), doc in-code. Les chemins de saturation/bornage sont, eux, couverts par tests directs.- Header régénéré (cbindgen) + committé ; diff vérifié par le hook barrière.
Tranche T4 — date-heure calendaire (AirDateTime, POD façon struct tm)
Décisions de conception (autonomie BDFL 2026-06-27). Expose le temps mur
calendaire d’air-base-lib::time (AirDateTime/AirCalendar, cf.
crates/air-base-lib/src/time.rs) en POD ABI C purs #[repr(C)] — second service
C-ABI sans handle (calque le pattern POD de T1/T3). Aucune modification
d’air-base-lib : chaque opération round-trip par l’API publique.
Types
AirCalendar— enum#[repr(C)]miroir des 6 variantes de couche 1 (Gregorian=0,Buddhist=1,Japanese=2,Islamic=3,Hebrew=4,Persian=5), discriminants explicites et committés (ADR-012). La couche 1 marque sonAirCalendar#[non_exhaustive](d’autres calendriers pourront être ajoutés) : la doc C documente que le consommateur doit rester défensif (variante future hors liste possible —default:dans unswitch). Mapping explicite C → lib (pas de castenum as u8). Le formatage localisé (ères, mois bissextiles) relève de la couche 2 (ADR-016) ; ici, composantes numériques seulement.AirDateTimeParts— POD#[repr(C)]façonstruct tm:{ int32_t year; uint8_t month; uint8_t day; uint8_t hour; uint8_t minute; uint8_t second; AirCalendar calendar; }(month ∈ [1,12],day ∈ [1,31],hour ∈ [0,23],minute ∈ [0,59],second ∈ [0,60]leap). On n’expose pas le jour absolu (rata_die) interne (détailicu4x).
Surface extern "C"
typedef enum AirCalendar { AIR_CALENDAR_GREGORIAN = 0, /* … */ AIR_CALENDAR_PERSIAN = 5 } AirCalendar;
typedef struct AirDateTimeParts {
int32_t year; uint8_t month; uint8_t day; uint8_t hour; uint8_t minute;
uint8_t second; AirCalendar calendar;
} AirDateTimeParts;
// Instant mur courant (CLOCK_REALTIME, couche 0), composantes dans `cal`. Peut
// relayer une erreur d'horloge (miroir AirStatus, bras STRUCTURAL).
AirStatus air_datetime_now_utc(AirCalendar cal, AirDateTimeParts *out);
// Valide + normalise ; date/heure invalide ⇒ AIR_STATUS_INVALID_INPUT.
AirStatus air_datetime_from_components(AirCalendar cal, int32_t year, uint8_t month,
uint8_t day, uint8_t hour, uint8_t minute, uint8_t second, AirDateTimeParts *out);
// Même instant porté sur `cal` ; *in_parts revalidé (invalide ⇒ INVALID_INPUT).
AirStatus air_datetime_to_calendar(const AirDateTimeParts *in_parts, AirCalendar cal,
AirDateTimeParts *out);
// Décalage de jour absolu (signé) ; débordement i64 ⇒ INVALID_INPUT. Rendu dans le
// MÊME calendrier que l'entrée.
AirStatus air_datetime_add_days(const AirDateTimeParts *in_parts, int64_t days,
AirDateTimeParts *out);
- Round-trip par l’API publique (aucune modif d’
air-base-lib) : chaque opération reconstruit l’AirDateTimeinterne viafrom_componentsdepuis les parts (qui valide la date), opère (to_calendar/add_days), puis relit les composantes via les accesseurs (year/month/…) vers les parts. Lerata_dieinterne n’est jamais exposé. - Pas de variante
AirStatusajoutée : la couche 1 rendAirErrorKind::InvalidInputpour toute date/heure invalide et tout débordement de jour absolu — déjà miroité parAIR_STATUS_INVALID_INPUTvia le helper T1 (From<AirErrorKind>). L’enumAirStatus(donc ADR-045) reste inchangé (aucune churn ABI).
Propriété mémoire (clause ADR-027 §B — reportée dans chaque ///)
| Fonction | Retour | Arguments | Libération |
|---|---|---|---|
air_datetime_now_utc | *out POD +0 (sur OK) | cal par valeur | — (POD trivial) |
air_datetime_from_components | *out POD +0 (sur OK) | cal + composantes par valeur | — |
air_datetime_to_calendar | *out POD +0 (sur OK) | in_parts emprunté +0 ; cal par valeur | — |
air_datetime_add_days | *out POD +0 (sur OK) | in_parts emprunté +0 ; days par valeur | — |
Thread-safety déclarée (ADR-027 §B.4)
- Toutes ces fonctions sont réentrantes / thread-safe : sans état partagé
(
now_utclit l’horloge kernel ; les autres sont du pur calcul calendaireicu4x). Sûres d’appel concurrent depuis plusieurs threads, chacun avec ses propresout.
Notes d’implémentation
- POD purs (
AirCalendar/AirDateTimeParts) ajoutés à[export] includedecbindgen.toml. air_datetime_now_utcpeut relayer une erreur d’horloge (AirStatus::from) : bras STRUCTURAL (ADR-035) —clock_gettime(CLOCK_REALTIME)est structurellement infaillible sur un système démarré (cf.air-base-lib::time::AirDateTime::now_utc), doc in-code. Les chemins de validation/erreur (date invalide, débordementadd_days,in_partsmalformé) sont, eux, couverts par tests directs (Rust + conformité C).- Header régénéré (cbindgen) + committé ; diff vérifié par le hook barrière.
Tranche T5a — chemins (AirPath)
Décisions de conception (autonomie BDFL 2026-06-27). Expose la manipulation
lexicale de chemins d’air-base-lib::path (AirPath — type ré-exporté
depuis air-base-core depuis ADR-054,
cf. crates/air-base-core/src/path.rs) en handle opaque possédé — premier
service C-ABI à valeur octets. Aucune modification d’air-base-lib :
chaque opération délègue à l’API publique (le pont AirPath ↔ AirString passe
par l’extension air_base_lib::AirPathStringExt, frontière ABI inchangée). Sont reportées à T5b/T5c : components
(itérateur), to_air_string/from_air_string (pont AirString).
Types
AirPath— handle opaque ABI C (typedef struct AirPath AirPath;), enveloppe possédée d’air_base_lib::AirPath(unVec<u8>). Un chemin est une suite d’octets pouvant contenir du non-UTF-8 (Unix, Principe 3) : les accesseurs de contenu rendent donc des octets bruts (uint8_t *), pas deschar *. Calque le pattern propriété/handle de T2 (AirLog).
Surface extern "C"
typedef struct AirPath AirPath;
// Construction (propriété +1 : à libérer via air_path_free).
AirStatus air_path_from_bytes(const uint8_t *bytes, size_t len, AirPath **out);
AirStatus air_path_join(const AirPath *path, const AirPath *component, AirPath **out);
AirStatus air_path_parent(const AirPath *path, AirPath **out); // None ⇒ NOT_FOUND
AirStatus air_path_normalize_lexically(const AirPath *path, AirPath **out);
void air_path_free(AirPath *path); // NULL toléré (no-op)
// Accesseurs (+0). Sorties octets par buffer appelant : *written = longueur réelle
// TOUJOURS (même sur BUFFER_TOO_SMALL) ; pas de NUL (sauf to_c_string).
AirStatus air_path_as_bytes(const AirPath *path, uint8_t *buf, size_t cap, size_t *written);
AirStatus air_path_to_c_string(const AirPath *path, char *buf, size_t cap, size_t *written);
AirStatus air_path_file_name(const AirPath *path, uint8_t *buf, size_t cap, size_t *written); // None ⇒ NOT_FOUND
AirStatus air_path_extension(const AirPath *path, uint8_t *buf, size_t cap, size_t *written); // None ⇒ NOT_FOUND
AirStatus air_path_is_absolute(const AirPath *path, bool *out);
- Sorties octets (Principe 3) :
as_bytes/file_name/extensionrendent des octets bruts (potentiellement non-UTF-8) dans un buffer fourni par l’appelant.*writtenreçoit la longueur réelle — TOUJOURS, y compris surAIR_STATUS_BUFFER_TOO_SMALL(l’appelant redimensionne puis réessaie ; aucune troncature, ADR-032). AucunNULn’est ajouté. to_c_stringest le pont couche 0 (frontière syscall) : chaîne CNUL-terminée,*writteninclut leNUL. Un octetNULinterne ⇒AIR_STATUS_INVALID_INPUT(incompatible avec une chaîne C).char *=c_char(i8/x86_64, u8/aarch64) : la copie passe parbuf.cast::<u8>(), identique sur les deux arches (leçon #132).- Mapping
Option::None→AIR_STATUS_NOT_FOUND:parent(racine / chemin vide),file_name(racine, vide, ou fin./..),extension(pas de., ou.en tête type.bashrc).*out/buf/*writtennon écrits dans ce cas. - Pas de variante
AirStatusajoutée :INVALID_INPUT(octetNULinterne, via le miroir T1From<AirErrorKind>),NOT_FOUND,BUFFER_TOO_SMALL,NULL_ARGUMENTsuffisent. L’enumAirStatus(ADR-045) reste inchangé.
Propriété mémoire (clause ADR-027 §B — reportée dans chaque ///)
| Fonction | Retour | Arguments | Libération |
|---|---|---|---|
air_path_from_bytes | *out handle +1 (sur OK) | bytes emprunté +0 | air_path_free |
air_path_join | *out handle +1 (sur OK) | path/component empruntés +0 | air_path_free |
air_path_parent | *out handle +1 (sur OK ; None ⇒ NOT_FOUND) | path emprunté +0 | air_path_free |
air_path_normalize_lexically | *out handle +1 (sur OK) | path emprunté +0 | air_path_free |
air_path_as_bytes | *written longueur ; buf rempli | path emprunté +0 ; buf possédé appelant +0 | — |
air_path_to_c_string | *written (incl. NUL) ; buf rempli | path emprunté +0 ; buf possédé appelant +0 | — |
air_path_file_name | *written ; buf rempli (None ⇒ NOT_FOUND) | path emprunté +0 ; buf possédé appelant +0 | — |
air_path_extension | *written ; buf rempli (None ⇒ NOT_FOUND) | path emprunté +0 ; buf possédé appelant +0 | — |
air_path_is_absolute | *out POD +0 (sur OK) | path emprunté +0 | — |
air_path_free | — | consomme path (+1 → libéré) ; NULL no-op | (c’est la libération) |
Thread-safety déclarée (ADR-027 §B.4)
AirPathest un handle possédé non partagé : prévoir un handle par thread. Les accesseurs n’empruntent qu’en lecture (lecture concurrente du même handle sound en principe), maisair_path_freeconsomme le handle et exige l’absence d’usage concurrent — ne pas partager un mêmeAirPath *entre threads sans synchronisation externe.
Notes d’implémentation
- Handle opaque (pas dans
[export] includedecbindgen.toml) :typedef struct AirPath AirPath;émis automatiquement par les signaturesextern "C". - Helper interne
write_bytes(octets bruts +*writtentoujours écrit), pendant octets dewrite_c_string(chaîne ASCII NUL-terminée T1). - Aucun bras STRUCTURAL : toute la logique est lexicale et infaillible côté
couche 1 (sauf l’octet
NULinterne deto_c_string⇒InvalidInput, couvert par test direct Rust + conformité C). Couverture 100 % atteignable sans exception. - Header régénéré (cbindgen) + committé ; diff vérifié par le hook barrière.
Tranche T5b — chaînes + locale (AirString, AirLocale)
Décisions de conception (autonomie BDFL 2026-06-27). Expose la section
chaînes d’air-base-lib::string (AirString UTF-8 garanti, AirLocale BCP
47, cf. crates/air-base-lib/src/string.rs) en handles opaques possédés
(calque T5a AirPath). Aucune modification d’air-base-lib : chaque
opération délègue à l’API publique (icu4x, couverte à 100 %). Sont reportées à
T5c : la segmentation itérative (graphemes/words/sentences). Les ponts
AirPath ↔ AirString (reportés de T5a) sont livrés ici.
Types
AirNormalizationForm— enum#[repr(C)](POD, par valeur), miroir d’air_base_lib::NormalizationForm:Nfc=0/Nfd=1/Nfkc=2/Nfkd=3. Forcé dans[export] includedecbindgen.toml. Mapping explicite (pas de castenum as u8).AirLocale— handle opaque (typedef struct AirLocale AirLocale;), enveloppe possédée d’air_base_lib::AirLocale(un identifiant BCP 47).AirString— handle opaque (typedef struct AirString AirString;), enveloppe possédée d’air_base_lib::AirString(invariant UTF-8 valide par construction). Comme unAirStringpeut excéder l’ASCII, toutes les sorties de contenu sont des octets (uint8_t *+*written), jamais deschar *(Principe 3, distinction stricte octets/UTF-8).
Surface extern "C"
typedef enum AirNormalizationForm { AIR_NORMALIZATION_FORM_NFC = 0, /* … */ } AirNormalizationForm;
typedef struct AirLocale AirLocale;
typedef struct AirString AirString;
// AirLocale (handle +1 : à libérer via air_locale_free).
AirStatus air_locale_parse(const uint8_t *tag, size_t len, AirLocale **out); // non-UTF-8 ⇒ INVALID_INPUT ; mal formé ⇒ INVALID_DATA
AirStatus air_locale_root(AirLocale **out);
AirStatus air_locale_to_bcp47(const AirLocale *loc, uint8_t *buf, size_t cap, size_t *written);
AirStatus air_locale_language(const AirLocale *loc, uint8_t *buf, size_t cap, size_t *written);
AirStatus air_locale_region(const AirLocale *loc, uint8_t *buf, size_t cap, size_t *written); // None ⇒ NOT_FOUND
void air_locale_free(AirLocale *loc); // NULL toléré (no-op)
// AirString (handle +1 : à libérer via air_string_free).
AirStatus air_string_new(AirString **out);
AirStatus air_string_from_utf8(const uint8_t *bytes, size_t len, AirString **out); // non-UTF-8 ⇒ INVALID_DATA
AirStatus air_string_from_utf8_lossy(const uint8_t *bytes, size_t len, AirString **out); // U+FFFD, jamais d'erreur
AirStatus air_string_as_bytes(const AirString *s, uint8_t *buf, size_t cap, size_t *written);
AirStatus air_string_len(const AirString *s, size_t *out); // longueur en octets
AirStatus air_string_normalize(const AirString *s, AirNormalizationForm form, AirString **out);
AirStatus air_string_is_normalized(const AirString *s, AirNormalizationForm form, bool *out);
AirStatus air_string_to_uppercase_in_locale(const AirString *s, const AirLocale *loc, AirString **out);
AirStatus air_string_to_lowercase_in_locale(const AirString *s, const AirLocale *loc, AirString **out);
AirStatus air_string_to_titlecase_in_locale(const AirString *s, const AirLocale *loc, AirString **out);
AirStatus air_string_compare_in_locale(const AirString *s, const AirString *other, const AirLocale *loc, int32_t *out); // -1/0/1
AirStatus air_string_eq_ignore_case_in_locale(const AirString *s, const AirString *other, const AirLocale *loc, bool *out);
AirStatus air_string_grapheme_count(const AirString *s, size_t *out); // clusters étendus (Annex #29)
void air_string_free(AirString *s); // NULL toléré (no-op)
// Ponts AirPath ↔ AirString (Principe 3).
AirStatus air_path_to_air_string(const AirPath *p, AirString **out); // non-UTF-8 ⇒ INVALID_DATA
AirStatus air_path_to_air_string_lossy(const AirPath *p, AirString **out); // U+FFFD, jamais d'erreur
AirStatus air_path_from_air_string(const AirString *s, AirPath **out);
- Sorties octets / UTF-8 (Principe 3) :
air_string_*etair_locale_*rendent du contenu UTF-8 en octets (uint8_t *+*written= longueur réelle TOUJOURS, même surBUFFER_TOO_SMALL; aucune troncature, ADR-032 ; pas deNUL). Réutilise le helper octetswrite_bytesde T5a. Ordering→int32:air_string_compare_in_localeprojette-1/0/1(Less/Equal/Greater) via un mappage explicite (pas d’aslossy, Principe 2).- Mapping
Option::None→AIR_STATUS_NOT_FOUND:air_locale_region(locale sans région).buf/*writtennon écrits dans ce cas. - UTF-8 amont vs aval :
air_locale_parsevalide l’UTF-8 en amont (octets →&str, sinonINVALID_INPUTavant tout parsing) puis parse (mal formé ⇒INVALID_DATA).air_string_from_utf8/air_path_to_air_stringmappent l’AirErrorde la couche 1 (octets non-UTF-8 ⇒INVALID_DATA). - Pas de variante
AirStatusajoutée :INVALID_INPUT/INVALID_DATA/NOT_FOUND/BUFFER_TOO_SMALL/NULL_ARGUMENTsuffisent. L’enumAirStatus(ADR-045) reste inchangé.
Propriété mémoire (clause ADR-027 §B — reportée dans chaque ///)
| Fonction | Retour | Arguments | Libération |
|---|---|---|---|
air_locale_parse / air_locale_root | *out handle +1 (sur OK) | tag emprunté +0 | air_locale_free |
air_locale_to_bcp47 / air_locale_language | *written ; buf rempli | loc emprunté +0 ; buf possédé appelant +0 | — |
air_locale_region | *written ; buf rempli (None ⇒ NOT_FOUND) | loc emprunté +0 ; buf possédé appelant +0 | — |
air_locale_free | — | consomme loc (+1 → libéré) ; NULL no-op | (c’est la libération) |
air_string_new / air_string_from_utf8{,_lossy} | *out handle +1 (sur OK) | bytes emprunté +0 | air_string_free |
air_string_normalize / casing | *out handle +1 (sur OK) | s/loc empruntés +0 | air_string_free |
air_string_as_bytes | *written ; buf rempli | s emprunté +0 ; buf possédé appelant +0 | — |
air_string_len / grapheme_count / compare / eq_* / is_normalized | *out POD +0 (sur OK) | s/other/loc empruntés +0 | — |
air_string_free | — | consomme s (+1 → libéré) ; NULL no-op | (c’est la libération) |
air_path_to_air_string{,_lossy} | *out handle AirString +1 (sur OK) | p emprunté +0 | air_string_free |
air_path_from_air_string | *out handle AirPath +1 (sur OK) | s emprunté +0 | air_path_free |
Thread-safety déclarée (ADR-027 §B.4)
AirString/AirLocalesont des handles possédés non partagés : prévoir un handle par thread. Les accesseurs n’empruntent qu’en lecture, maisair_string_free/air_locale_freeconsomment le handle et exigent l’absence d’usage concurrent — ne pas partager un même handle entre threads sans synchronisation externe. Les transformations rendent toujours un nouvel handle (jamais de mutation en place).
Notes d’implémentation
- Handles opaques (pas dans
[export] include) :typedef struct AirString AirString;/AirLocaleémis automatiquement par les signaturesextern "C". SeulAirNormalizationForm(POD) est ajouté à[export] include. - Aucun bras STRUCTURAL : le mapping
NormalizationFormest exhaustif (4 formes, pas de#[non_exhaustive]) ;ordering_to_i32couvreLess/Equal/Greater; les chemins d’erreur (INVALID_INPUT/INVALID_DATA/NOT_FOUND) sont tous couverts par tests Rust + conformité C. Couverture 100 % sans exception. - Header régénéré (cbindgen) + committé ; diff vérifié par le hook barrière.
Tranche T5c — segmentation (itérateur AirByteIter)
Décisions de conception (autonomie BDFL 2026-06-27). Expose la segmentation
d’air-base-lib — AirString::graphemes/words/sentences (sous-tranches
&str, UAX #29) et AirPath::components (segments &[u8], cf.
crates/air-base-lib/src/{string,path}.rs) — derrière un seul itérateur
opaque unifié à snapshot eager. Aucune modification d’air-base-lib :
chaque constructeur délègue à l’itérateur natif de la couche 1 (couvert à 100 %)
et en copie les segments.
Types
AirByteIter— handle opaque possédé (typedef struct AirByteIter AirByteIter;), non dans[export] include(émis automatiquement par les signaturesextern "C"). Contient un snapshot possédé des segments (Vec<Vec<u8>>) collecté à la création + un curseur. Sorties en octets (uint8_t *) : un graphème/mot/phrase est de l’UTF-8, mais un composant de chemin peut être non-UTF-8 (Principe 3) — d’où un itérateur d’octets unifié, jamaischar *.
Snapshot eager (indépendance de la source)
À la création, l’itérateur collecte immédiatement tous les segments dans
un buffer possédé (.as_bytes().to_vec() pour les itérateurs &str, .to_vec()
pour components). Il est donc indépendant de la durée de vie de la source :
muter ou libérer l’AirString/AirPath après la création n’affecte pas
l’itération (aucun emprunt, aucune auto-référence). Choix assumé : simplicité et
sûreté mémoire à la frontière C priment sur l’économie d’une copie (Principe 5 ;
la source vit typiquement le temps de la segmentation, mais le contrat ne l’exige
pas).
Surface extern "C"
typedef struct AirByteIter AirByteIter;
// Constructeurs (chacun : *out handle +1, à libérer via air_byte_iter_free).
AirStatus air_string_graphemes(const AirString *s, AirByteIter **out);
AirStatus air_string_words(const AirString *s, AirByteIter **out);
AirStatus air_string_sentences(const AirString *s, AirByteIter **out);
AirStatus air_path_components(const AirPath *p, AirByteIter **out);
// Avancement (idiome two-call : sonder la taille, puis copier).
AirStatus air_byte_iter_next(AirByteIter *it, uint8_t *buf, size_t cap,
size_t *written, bool *has_value);
void air_byte_iter_free(AirByteIter *it); // NULL toléré (no-op)
- Idiome two-call (
air_byte_iter_next), tous cas in-band :- épuisé ⇒
*has_value = false,*written = 0,AIR_STATUS_OK; - le segment tient (
cap >= longueur) ⇒ octets copiés,*written = longueur,*has_value = true, le curseur avance,AIR_STATUS_OK; - buffer trop petit (
cap < longueur) ⇒*written = longueur requise,*has_value = true, le curseur n’avance PAS (retry possible sur le même élément après agrandissement),AIR_STATUS_BUFFER_TOO_SMALL(ADR-032 : aucune troncature ni copie partielle).
- épuisé ⇒
- Sondage de taille :
cap == 0est autorisé (bufpeut alors êtreNULL) — rendBUFFER_TOO_SMALL+*writtensans rien copier ni avancer. Dès quecap != 0,bufdoit être non-NULL(sinonNULL_ARGUMENT). - Pas de variante
AirStatusajoutée :OK/BUFFER_TOO_SMALL/NULL_ARGUMENTsuffisent. L’enumAirStatus(ADR-045) reste inchangé.
Propriété mémoire (clause ADR-027 §B — reportée dans chaque ///)
| Fonction | Retour | Arguments | Libération |
|---|---|---|---|
air_string_graphemes / words / sentences | *out handle +1 (sur OK) | s emprunté +0 (snapshot ⇒ libérable après) | air_byte_iter_free |
air_path_components | *out handle +1 (sur OK) | p emprunté +0 (snapshot ⇒ libérable après) | air_byte_iter_free |
air_byte_iter_next | *written/*has_value ; buf rempli (sauf BUFFER_TOO_SMALL) | it emprunté en mutation +0 (curseur) ; buf possédé appelant +0 | — |
air_byte_iter_free | — | consomme it (+1 → libéré) ; NULL no-op | (c’est la libération) |
Thread-safety déclarée (ADR-027 §B.4)
AirByteIterest un handle possédé non partagé :air_byte_iter_nextmute le curseur ⇒ prévoir un itérateur par thread. Ne pas partager un mêmeAirByteIter *entre threads sans synchronisation externe ;air_byte_iter_freeconsomme le handle et exige l’absence d’usage concurrent. Grâce au snapshot, l’itérateur ne dépend pas de la source après création.
Notes d’implémentation
- Constructeurs :
s/pempruntés en lecture le temps de.collect(); chaque segment est copié enVec<u8>possédé — aucun emprunt de la source conservé. air_byte_iter_next:*written/*has_valueécrits avant le test de capacité (longueur réelle TOUJOURS rendue) ;saturating_addpour le curseur (lintarithmetic_side_effects) ; copie seulement silen != 0(défensif —copy_nonoverlappingexige des pointeurs valides,bufpeut êtreNULLsicap == 0). Aucunaslossy (usize⇒size_texact).- Aucun bras STRUCTURAL : tous les chemins (épuisement, BUFFER_TOO_SMALL sans avancement, copie+avance, gardes NULL, free(NULL), snapshot survivant au free de la source) sont couverts par tests Rust + conformité C. Couverture 100 % sans exception.
- Header régénéré (cbindgen) + committé ; diff vérifié par le hook barrière.