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

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 trailing NUL is 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:

TypeCreated by (+1)Freed by
AirLogair_log_openair_log_close
AirLogFieldsair_log_fields_newair_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_OK only. On error, *out is unchanged: there is nothing to free (don’t call close/free on a handle you didn’t obtain).
  • close/free consume the handle (+1 → freed). After the call the pointer is invalid: don’t reuse it, and don’t free it twice (double-free forbidden).
  • NULL is a no-op for air_log_close/air_log_fields_free: freeing a NULL is safe (handy for error paths).
  • The handle passed to air_log_emit (const AirLog *, const AirLogFields *) is borrowed (+0): emit does not free it and keeps no reference. It is always up to you to call close/free.

Per-symbol summary table

SymbolReturnArgument(s)Release function
air_uuid_new_v7 / _v4AirStatusout borrowed +0 (POD, written on OK)
air_uuid_to_hyphenatedAirStatusuuid +0 ; buf caller +0
air_uuid_parseAirStatuss +0 (C-string) ; out +0
air_id128_machine_idAirStatusout +0 (POD)
air_id128_to_hexAirStatusid +0 ; buf caller +0
air_monotonic_id_nextAirStatusout +0 (uint64_t)
air_status_messageconst char * staticstatus (by value)do not free
air_log_openAirStatusnamespace_ +0 ; *out owned +1air_log_close
air_log_closevoidconsumes log (NULL = no-op)(this is the release)
air_log_emitAirStatuslog / message / fields borrowed +0
air_log_lost_countAirStatuslog +0 ; out +0
air_log_fields_newAirStatus*out owned +1air_log_fields_free
air_log_fields_freevoidconsumes fields (NULL = no-op)(this is the release)
air_log_fields_addAirStatusfields borrowed-mut +0 ; key/value +0
air_log_fields_add_bytesAirStatusfields +0 ; key +0 ; value +0 (len bytes)
air_instant_nowAirStatusout +0 (POD)
air_instant_elapsedAirStatussince +0 ; out +0
air_instant_duration_sinceAirStatuslater/earlier +0 ; out +0
air_duration_from_secs/millis/nanosAirStatusout +0 (POD)
air_duration_as_secs_f64AirStatusd +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 returns AIR_STATUS_INVALID_DATA (never a NULL dereference: passing NULL returns AIR_STATUS_NULL_ARGUMENT).
  • air_log_fields_add_bytes accepts a binary value (len bytes, newlines and NUL allowed): 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.