Memory ownership & lifecycle
The critical point of the C ABI. In C, nothing reminds you to free — the compiler does not track ownership. This page states, unambiguously and for every symbol, who allocates, who frees, and with which function. A C developer must always know whether to free a type, and how.
The ownership doctrine is not a separate guide that could drift: it is born from
the /// comment on each symbol (carried into the header, rendered by Doxygen)
and synthesized here (ADR-027 §B).
The uniform rule: +1 on RETURN, +0 on ARGUMENT
A single rule governs the whole Air object model:
- A complex type RETURNED is owned by the caller (+1). You are responsible for it → you must free it with its dedicated release function.
- A complex type passed as an ARGUMENT is borrowed (+0). You keep ownership;
the callee does not free it and retains no reference beyond the call (unless an
explicit
///clause says otherwise — there is none in T1/T3).
Everything else follows from this rule. The three categories below show how it applies concretely.
Category 1 — POD passed by value (nothing to free)
AirUuid, AirId128, AirInstant, AirDuration, AirLogLevel, AirStatus are
POD (Plain Old Data, #[repr(C)]), trivially copyable. They live where you
declare them (stack, struct, array): no Air allocation, no release function.
AirUuid u; /* on the caller's stack */
air_uuid_new_v7(&u); /* `&u` borrowed (+0), written on OK */
/* `u` vanishes at end of scope — nothing to do. */
When a POD is passed by pointer (const AirUuid *uuid, AirDuration *out), it is
only to avoid a copy or to write an output: the memory stays yours (+0).
Category 2 — String outputs via caller buffer (nothing to free)
The formatting functions (air_uuid_to_hyphenated, air_id128_to_hex) allocate
nothing. You provide the buffer; Air writes into it:
char buf[AIR_UUID_HYPHENATED_LEN]; /* 37, NUL included */
AirStatus st = air_uuid_to_hyphenated(&u, buf, sizeof buf);
/* `buf` is yours (+0). Too small ⇒ AIR_STATUS_BUFFER_TOO_SMALL, nothing written. */
- Size with the header constants, never a literal:
AIR_UUID_HYPHENATED_LEN(37),AIR_ID128_HEX_LEN(33). The trailingNULis included. - A short buffer ⇒
AIR_STATUS_BUFFER_TOO_SMALL, no partial write (ADR-032). - No
air_*_free: the memory is yours, its lifetime is yours.
Special case — air_status_message returns a const char * to a static
string: it is never freed, never NULL, valid for the whole program lifetime. Do
not free it (that would be a fault).
Category 3 — Opaque handles (+1 on return → you MUST free)
AirLog and AirLogFields are opaque handles: typedef struct … never
dereferenced on the C side. They are allocated by Air and returned owned (+1);
each has one dedicated release function:
| Type | Created by (+1) | Freed by |
|---|---|---|
AirLog | air_log_open | air_log_close |
AirLogFields | air_log_fields_new | air_log_fields_free |
AirLog *log = NULL;
if (air_log_open(NULL, &log) == AIR_STATUS_OK) { /* +1: log owned */
/* … use … */
air_log_close(log); /* mandatory release */
}
Handle lifecycle rules:
- Written on
AIR_STATUS_OKonly. On error,*outis unchanged: there is nothing to free (don’t callclose/freeon a handle you didn’t obtain). close/freeconsume the handle (+1 → freed). After the call the pointer is invalid: don’t reuse it, and don’t free it twice (double-free forbidden).NULLis a no-op forair_log_close/air_log_fields_free: freeing aNULLis safe (handy for error paths).- The handle passed to
air_log_emit(const AirLog *,const AirLogFields *) is borrowed (+0):emitdoes not free it and keeps no reference. It is always up to you to callclose/free.
Per-symbol summary table
| Symbol | Return | Argument(s) | Release function |
|---|---|---|---|
air_uuid_new_v7 / _v4 | AirStatus | out borrowed +0 (POD, written on OK) | — |
air_uuid_to_hyphenated | AirStatus | uuid +0 ; buf caller +0 | — |
air_uuid_parse | AirStatus | s +0 (C-string) ; out +0 | — |
air_id128_machine_id | AirStatus | out +0 (POD) | — |
air_id128_to_hex | AirStatus | id +0 ; buf caller +0 | — |
air_monotonic_id_next | AirStatus | out +0 (uint64_t) | — |
air_status_message | const char * static | status (by value) | do not free |
air_log_open | AirStatus | namespace_ +0 ; *out owned +1 | air_log_close |
air_log_close | void | consumes log (NULL = no-op) | (this is the release) |
air_log_emit | AirStatus | log / message / fields borrowed +0 | — |
air_log_lost_count | AirStatus | log +0 ; out +0 | — |
air_log_fields_new | AirStatus | *out owned +1 | air_log_fields_free |
air_log_fields_free | void | consumes fields (NULL = no-op) | (this is the release) |
air_log_fields_add | AirStatus | fields borrowed-mut +0 ; key/value +0 | — |
air_log_fields_add_bytes | AirStatus | fields +0 ; key +0 ; value +0 (len bytes) | — |
air_instant_now | AirStatus | out +0 (POD) | — |
air_instant_elapsed | AirStatus | since +0 ; out +0 | — |
air_instant_duration_since | AirStatus | later/earlier +0 ; out +0 | — |
air_duration_from_secs/millis/nanos | AirStatus | out +0 (POD) | — |
air_duration_as_secs_f64 | AirStatus | d +0 ; out +0 (double) | — |
Reading the table: a row with a non-empty release function is the only kind where you have something to free. All others: nothing.
String & buffer encoding
- Input
const char *are NUL-terminated C strings expected as UTF-8; non-UTF-8 content returnsAIR_STATUS_INVALID_DATA(never aNULLdereference: passingNULLreturnsAIR_STATUS_NULL_ARGUMENT). air_log_fields_add_bytesaccepts a binary value (lenbytes, newlines andNULallowed): the only “raw bytes” case, borrowed +0 for the duration of the call.- Output buffers are always provided and owned by you (category 2).
Secrets & reference counting
In T1/T3, no type carries a secret and none exposes retain/release: the
“+1 return / +0 argument” rule suffices. When the Air libc introduces reference-counted
or secret-bearing objects, their dedicated /// clause will state it explicitly
(paired retain/release, whose responsibility the zeroize wipe is) — always under
the same principle: ownership is carved into each symbol’s comment.
Version française : Propriété mémoire & cycle de vie.