Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 : AirStatus in-band, sans errno, 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 → AirStatus mappe le kind (message dynamique non transporté en T1).
  • air_status_message(AirStatus) -> *const c_char : chaîne statique (+0, jamais libérée).
  • panic = "abort" : aucun catch_unwind, aucun AIR_PANIC (panic = bug Air → abort fail-fast ; tests en unwind via 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/journald 0..=7 (AIR_LOG_LEVEL_EMERGENCY=0AIR_LOG_LEVEL_DEBUG=7). POD.
  • AirLoghandle opaque (typedef struct AirLog AirLog;), jamais déréférencé par l’appelant C. Côté Rust : Box<air_base_lib::AirLog> (cœur Arc<JournalSink>Send+Sync).
  • AirLogFieldshandle 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 ///)

FonctionRetourArgumentsLibération
air_log_open*out possédé +1namespace_ emprunté +0air_log_close
air_log_emit(statut)log/message/fields empruntés +0
air_log_closeconsomme log(c’est la libération)
air_log_fields_new*out possédé +1air_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œur Arc<JournalSink> + envoi datagramme atomique ; air_log_emit/air_log_lost_count sont sûrs depuis plusieurs threads sur le même handle. air_log_close exige l’absence d’usage concurrent (consomme).
  • AirLogFields = non thread-safe : muté par add ; le construire sur un thread, puis le passer (emprunté en lecture) à air_log_emit.

Notes d’implémentation

  • panic = "abort" (déjà workspace) ; pas de catch_unwind. Validation null amont.
  • Pas de troncature/perte silencieuse (ADR-032) : add reporte 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’i128 interne d’air-base-lib (non portable en ABI C) : un i64 de 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çon struct timespec (nanoseconds ∈ [0, 1e9)). Non négative (cas d’usage : mesure d’écoulement). La durée signée i128 interne 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érent to_std_saturating) et secondes excédant i64borné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-lib refuse à dessein d’exposer la valeur brute d’un AirInstant. 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 un i64 de 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’as non justifié).

Propriété mémoire (clause ADR-027 §B — reportée dans chaque ///)

FonctionRetourArgumentsLibé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] include de cbindgen.toml.
  • air_instant_now/air_instant_elapsed peuvent 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 son AirCalendar #[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 un switch). Mapping explicite C → lib (pas de cast enum 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çon struct 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étail icu4x).

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’AirDateTime interne via from_components depuis les parts (qui valide la date), opère (to_calendar/add_days), puis relit les composantes via les accesseurs (year/month/…) vers les parts. Le rata_die interne n’est jamais exposé.
  • Pas de variante AirStatus ajoutée : la couche 1 rend AirErrorKind::InvalidInput pour toute date/heure invalide et tout débordement de jour absolu — déjà miroité par AIR_STATUS_INVALID_INPUT via le helper T1 (From<AirErrorKind>). L’enum AirStatus (donc ADR-045) reste inchangé (aucune churn ABI).

Propriété mémoire (clause ADR-027 §B — reportée dans chaque ///)

FonctionRetourArgumentsLibé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_utc lit l’horloge kernel ; les autres sont du pur calcul calendaire icu4x). Sûres d’appel concurrent depuis plusieurs threads, chacun avec ses propres out.

Notes d’implémentation

  • POD purs (AirCalendar/AirDateTimeParts) ajoutés à [export] include de cbindgen.toml.
  • air_datetime_now_utc peut 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ébordement add_days, in_parts malformé) 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 AirPathAirString 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

  • AirPathhandle opaque ABI C (typedef struct AirPath AirPath;), enveloppe possédée d’air_base_lib::AirPath (un Vec<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 des char *. 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/extension rendent des octets bruts (potentiellement non-UTF-8) dans un buffer fourni par l’appelant. *written reçoit la longueur réelleTOUJOURS, y compris sur AIR_STATUS_BUFFER_TOO_SMALL (l’appelant redimensionne puis réessaie ; aucune troncature, ADR-032). Aucun NUL n’est ajouté.
  • to_c_string est le pont couche 0 (frontière syscall) : chaîne C NUL-terminée, *written inclut le NUL. Un octet NUL interneAIR_STATUS_INVALID_INPUT (incompatible avec une chaîne C). char * = c_char (i8/x86_64, u8/aarch64) : la copie passe par buf.cast::<u8>(), identique sur les deux arches (leçon #132).
  • Mapping Option::NoneAIR_STATUS_NOT_FOUND : parent (racine / chemin vide), file_name (racine, vide, ou fin ./..), extension (pas de ., ou . en tête type .bashrc). *out/buf/*written non écrits dans ce cas.
  • Pas de variante AirStatus ajoutée : INVALID_INPUT (octet NUL interne, via le miroir T1 From<AirErrorKind>), NOT_FOUND, BUFFER_TOO_SMALL, NULL_ARGUMENT suffisent. L’enum AirStatus (ADR-045) reste inchangé.

Propriété mémoire (clause ADR-027 §B — reportée dans chaque ///)

FonctionRetourArgumentsLibération
air_path_from_bytes*out handle +1 (sur OK)bytes emprunté +0air_path_free
air_path_join*out handle +1 (sur OK)path/component empruntés +0air_path_free
air_path_parent*out handle +1 (sur OK ; None ⇒ NOT_FOUND)path emprunté +0air_path_free
air_path_normalize_lexically*out handle +1 (sur OK)path emprunté +0air_path_free
air_path_as_bytes*written longueur ; buf remplipath emprunté +0 ; buf possédé appelant +0
air_path_to_c_string*written (incl. NUL) ; buf remplipath 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_freeconsomme path (+1 → libéré) ; NULL no-op(c’est la libération)

Thread-safety déclarée (ADR-027 §B.4)

  • AirPath est 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), mais air_path_free consomme le handle et exige l’absence d’usage concurrent — ne pas partager un même AirPath * entre threads sans synchronisation externe.

Notes d’implémentation

  • Handle opaque (pas dans [export] include de cbindgen.toml) : typedef struct AirPath AirPath; émis automatiquement par les signatures extern "C".
  • Helper interne write_bytes (octets bruts + *written toujours écrit), pendant octets de write_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 NUL interne de to_c_stringInvalidInput, 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] include de cbindgen.toml. Mapping explicite (pas de cast enum as u8).
  • AirLocalehandle opaque (typedef struct AirLocale AirLocale;), enveloppe possédée d’air_base_lib::AirLocale (un identifiant BCP 47).
  • AirStringhandle opaque (typedef struct AirString AirString;), enveloppe possédée d’air_base_lib::AirString (invariant UTF-8 valide par construction). Comme un AirString peut excéder l’ASCII, toutes les sorties de contenu sont des octets (uint8_t * + *written), jamais des char * (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_* et air_locale_* rendent du contenu UTF-8 en octets (uint8_t * + *written = longueur réelle TOUJOURS, même sur BUFFER_TOO_SMALL ; aucune troncature, ADR-032 ; pas de NUL). Réutilise le helper octets write_bytes de T5a.
  • Orderingint32 : air_string_compare_in_locale projette -1/0/1 (Less/Equal/Greater) via un mappage explicite (pas d’as lossy, Principe 2).
  • Mapping Option::NoneAIR_STATUS_NOT_FOUND : air_locale_region (locale sans région). buf/*written non écrits dans ce cas.
  • UTF-8 amont vs aval : air_locale_parse valide l’UTF-8 en amont (octets → &str, sinon INVALID_INPUT avant tout parsing) puis parse (mal formé ⇒ INVALID_DATA). air_string_from_utf8 / air_path_to_air_string mappent l’AirError de la couche 1 (octets non-UTF-8 ⇒ INVALID_DATA).
  • Pas de variante AirStatus ajoutée : INVALID_INPUT/INVALID_DATA/ NOT_FOUND/BUFFER_TOO_SMALL/NULL_ARGUMENT suffisent. L’enum AirStatus (ADR-045) reste inchangé.

Propriété mémoire (clause ADR-027 §B — reportée dans chaque ///)

FonctionRetourArgumentsLibération
air_locale_parse / air_locale_root*out handle +1 (sur OK)tag emprunté +0air_locale_free
air_locale_to_bcp47 / air_locale_language*written ; buf rempliloc 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_freeconsomme 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é +0air_string_free
air_string_normalize / casing*out handle +1 (sur OK)s/loc empruntés +0air_string_free
air_string_as_bytes*written ; buf remplis 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_freeconsomme 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é +0air_string_free
air_path_from_air_string*out handle AirPath +1 (sur OK)s emprunté +0air_path_free

Thread-safety déclarée (ADR-027 §B.4)

  • AirString / AirLocale sont des handles possédés non partagés : prévoir un handle par thread. Les accesseurs n’empruntent qu’en lecture, mais air_string_free / air_locale_free consomment 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 signatures extern "C". Seul AirNormalizationForm (POD) est ajouté à [export] include.
  • Aucun bras STRUCTURAL : le mapping NormalizationForm est exhaustif (4 formes, pas de #[non_exhaustive]) ; ordering_to_i32 couvre Less/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-libAirString::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

  • AirByteIterhandle opaque possédé (typedef struct AirByteIter AirByteIter;), non dans [export] include (émis automatiquement par les signatures extern "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é, jamais char *.

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).
  • Sondage de taille : cap == 0 est autorisé (buf peut alors être NULL) — rend BUFFER_TOO_SMALL + *written sans rien copier ni avancer. Dès que cap != 0, buf doit être non-NULL (sinon NULL_ARGUMENT).
  • Pas de variante AirStatus ajoutée : OK/BUFFER_TOO_SMALL/NULL_ARGUMENT suffisent. L’enum AirStatus (ADR-045) reste inchangé.

Propriété mémoire (clause ADR-027 §B — reportée dans chaque ///)

FonctionRetourArgumentsLibé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_freeconsomme it (+1 → libéré) ; NULL no-op(c’est la libération)

Thread-safety déclarée (ADR-027 §B.4)

  • AirByteIter est un handle possédé non partagé : air_byte_iter_next mute le curseur ⇒ prévoir un itérateur par thread. Ne pas partager un même AirByteIter * entre threads sans synchronisation externe ; air_byte_iter_free consomme 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/p empruntés en lecture le temps de .collect() ; chaque segment est copié en Vec<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_add pour le curseur (lint arithmetic_side_effects) ; copie seulement si len != 0 (défensif — copy_nonoverlapping exige des pointeurs valides, buf peut être NULL si cap == 0). Aucun as lossy (usizesize_t exact).
  • 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.