# SPEC-001 - Public API Conventions **Implementation and release scope:** [Project status](../STATUS.md) records the implemented subset; [roadmap](../architecture/07-roadmap.md) defines release gates. Acceptance of this design does not establish implementation, verification, or support. Reconciled 2026-10-14. **Status:** Accepted 2026-10-07 by the project owner, with the defaults of §14. Specification work item 1 of the roadmap (07 §3). **Requirements:** `docs/requirements/API.md` (API-001 to API-032). This document is the design; the requirement file is the normative, testable statement of it. **Decided by:** ADR-002 (namespaces, status enum, typed time), ADR-003 (single API with context classes), ADR-006 (generated storage types), ADR-014 (handles are capabilities), ADR-023 (C11 minimum, freestanding). **Toolchain constraints applied:** document 09 (EmbCC: one C17 dialect, int-sized enums, struct-by-value inlining limits, no `_Thread_local`). --- ## 1. Scope Everything a public header in `include/emb/` is allowed to contain, and how every public function is named, typed, documented, and annotated. Kernel internals (`embk_`) and the architecture-port contract (`emb_arch_`) follow the same conventions where they apply but are not public API. ## 2. Namespaces | Prefix | Meaning | Visible to | |---|---|---| | `emb_` | Public functions, types, variables | Applications, middleware, compatibility layers | | `EMB_` | Public macros, constants, enumerators | Same | | `emb_arch_` / `EMB_ARCH_` | Architecture-port contract | Kernel and ports only | | `embk_` / `EMBK_` | Kernel-private | Kernel only | | `emb_drv__` | Driver class APIs | Drivers, subsystems, applications | | `CONFIG_EMB_` | Configuration symbols (generated into ``) | Everyone | | `__emb_` / `_EMB_` | Reserved for generated code and compiler-portability internals; never written by hand in application or kernel source | Generated headers | Anything starting with `emb`, `EMB`, `Emb` is reserved for the project. No identifier may contain `embos` in any case (trademark proximity, ADR-002). ## 3. Identifier grammar ``` function := "emb_" subject "_" verb [ "_" qualifier ] subject := object | service e.g. thread, mutex, sem, event, notify, queue, pipe, pool, timer, work, workq, part, cap, mem, time, sched, irq, power, log, trace verb := see table one word, lower case qualifier := "until" | "all" | "any" | "front" | "n" | ... one word, documented per subject type := "emb_" subject [ "_" noun ] "_t" macro := "EMB_" SUBJECT "_" NAME upper case, underscores enumerator := "EMB_" SUBJECT "_" NAME same form as a macro ``` Standard verbs. A subject uses the verb from this table that matches the operation; a new verb needs a note in the subject's specification. | Verb | Meaning | Examples | |---|---|---| | `init` | Construct in caller-provided storage; returns a handle | `emb_mutex_init` | | `create` | Construct using a configured allocator (only when `CONFIG_EMB_DYNAMIC_OBJECTS`) | `emb_thread_create` | | `destroy` | End the object's life; storage returns to the caller | `emb_mutex_destroy` | | `start`, `stop`, `restart` | Begin, end, or re-arm an activity | `emb_thread_start`, `emb_timer_start` | | `suspend`, `resume` | Externally pause and continue a thread | `emb_thread_suspend` | | `join`, `detach`, `cancel`, `exit` | Thread lifecycle | `emb_thread_join` | | `lock`, `unlock` | Mutex ownership | `emb_mutex_lock` | | `take`, `give` | Semaphore count | `emb_sem_take` | | `set`, `clear`, `wait` | Event flags, notifications | `emb_event_set`, `emb_notify_wait` | | `send`, `recv` | Queues, ports | `emb_queue_send` | | `read`, `write` | Byte streams, devices | `emb_pipe_read` | | `alloc`, `free` | Pools and heaps | `emb_pool_alloc` | | `submit`, `cancel` | Work items, driver requests | `emb_work_submit` | | `get`, `set` | Attribute access | `emb_thread_get_priority`, `emb_thread_set_priority` | | `now`, `sleep` | Time | `emb_time_now`, `emb_thread_sleep` | Qualifiers: `_until` is the absolute-deadline form of a blocking verb (§7.4). `_all` and `_any` are event-wait modes. `_front` is send-to-front on a queue. There is no `_try` qualifier (pass `EMB_NO_WAIT`) and no `_from_isr` qualifier (ADR-003). The object handle is first, followed by inputs; outputs use `out_*` names. M1 functions with results place timeout/deadline before trailing outputs (`emb_thread_join`, `emb_notify_wait`); functions without outputs put it last. Preserve each public prototype; future tooling identifies the typed timeout and explicit argument metadata rather than assuming the last argument (A26). ## 4. Status codes ### 4.1 Type ```c typedef int emb_status_t; ``` `int` is the natural return register on every ABI (16 bits on AVR, 32 bits elsewhere). Zero is success. Negative values are errors from the fixed table below. Positive values are operation-specific counts where the function's documentation says so (bytes transferred, items received); such a function documents its maximum. ### 4.2 Error table Values are stable forever once released (API-010). The errno column is the mapping the POSIX compatibility layer uses; it is informative here. | Name | Value | Meaning | errno | |---|---|---|---| | `EMB_OK` | 0 | Success | 0 | | `EMB_EPERM` | -1 | Caller not permitted: wrong execution context, not the owner, insufficient capability rights | `EPERM` | | `EMB_EINVAL` | -2 | Invalid argument: null required pointer, out-of-range value, invalid handle, misaligned or undersized storage | `EINVAL` | | `EMB_EBUSY` | -3 | Object or resource is in use and the operation cannot proceed without waiting where waiting is not allowed (destroy with waiters in release builds, device busy) | `EBUSY` | | `EMB_ETIMEDOUT` | -4 | The awaited condition did not become true within the allowed wait, including a zero wait (`EMB_NO_WAIT`) | `ETIMEDOUT` | | `EMB_ECANCELED` | -5 | The wait ended because the calling thread was canceled | `ECANCELED` | | `EMB_EDESTROYED` | -6 | The wait ended because the object was destroyed with `ABORT_WAITERS` | `EIDRM` | | `EMB_EOWNERDEAD` | -7 | The mutex's previous owner terminated while holding it (configuration-dependent) | `EOWNERDEAD` | | `EMB_ENOMEM` | -8 | Pool, heap, table, or capacity exhausted | `ENOMEM` | | `EMB_ENOTSUP` | -9 | Not supported by this configuration, profile, target, or device | `ENOTSUP` | | `EMB_EIO` | -10 | Hardware or transport error | `EIO` | | `EMB_EFAULT` | -11 | Address outside the caller's accessible regions (isolated profiles) | `EFAULT` | | `EMB_ENOENT` | -12 | No such object, device, or entry | `ENOENT` | | `EMB_EEXIST` | -13 | Already exists, already initialized, already started | `EEXIST` | | `EMB_ESTATE` | -14 | The object or thread is in a state that does not permit the operation (resume a thread that is not suspended, start a running timer) | `EINVAL` | | `EMB_EOVERFLOW` | -15 | Value or size exceeds what can be represented or stored (time conversion saturated, message larger than the slot) | `EOVERFLOW` | | `EMB_ESTALE` | -16 | Handle refers to an object generation that no longer exists | `ESTALE` | | `EMB_EINTR` | -17 | Reserved for signal-like interruption of waits; not returned in 1.0 | `EINTR` | | `EMB_EDEADLK` | -32 | Self-join, non-recursive self-lock, or detected inheritance-chain cycle | `EDEADLK` | Ranges: -1 to -31 kernel core (above); -32 to -63 kernel extension range (`EMB_EDEADLK = -32`; the remaining values are reserved); -64 to -127 driver classes and subsystems, each defined in that class's specification with the prefix `EMB_E` and a class word (for example `EMB_ECRC`); -128 to -255 application-defined, from `EMB_EAPP_BASE`. Negative status codes down to -255 require at least `int16_t` for compact storage; `int8_t` cannot represent the application range. `emb_status_t` remains `int`, including operation-specific positive counts, which must be range-checked before narrowing. ### 4.3 Declaration form ```c /* */ typedef int emb_status_t; enum emb_status_code { EMB_OK = 0, EMB_EPERM = -1, /* ... as the table ... */ EMB_EINTR = -17, EMB_EAPP_BASE = -128 }; const char *emb_status_name(emb_status_t status); /* "EMB_EINVAL"; compiled out when CONFIG_EMB_STATUS_NAMES=n */ ``` The enum names the values; the function type stays `int` so that counts can share it. No `errno`, no thread-local error state (KRN-THR-014 forbids compiler TLS anyway). ### 4.4 Rules - A function returns `emb_status_t`, or a value type whose documentation defines every value, or `void` only when it cannot fail (pure accessors, `emb_time_now`). - A function that reports success and produces a value returns the status and writes the value through an `out_*` pointer. - A function never returns a positive count *and* a distinct success code; if it returns counts, `EMB_OK` means zero. - Error precedence when several apply: `EMB_EPERM` (context, rights) before `EMB_EINVAL` (arguments) before `EMB_ESTALE` before state and resource errors; the wait-result errors (`ETIMEDOUT`, `ECANCELED`, `EDESTROYED`) are exclusive of each other by the wait protocol (KRN-WAIT-004). ## 5. Execution contexts and misuse ### 5.1 Contexts | Context | Definition | `emb_context()` returns | |---|---|---| | **thread** | Running on a thread stack after the scheduler started, including work-queue and software-timer callbacks | `EMB_CONTEXT_THREAD` | | **isr** | Interrupt or exception handler at or below the kernel's masking level | `EMB_CONTEXT_ISR` | | **prekernel** | Before `emb_kernel_start()` returns control to the first thread | `EMB_CONTEXT_PREKERNEL` | Kernel-independent interrupts (above the kernel's masking level, 02 §6) may call nothing in the public API; `emb_context()` is not meaningful there. Two **sub-states** of thread context restrict blocking: *scheduler locked* (`emb_sched_lock()` depth > 0) and *critical section* (`emb_irq_lock()` held). A blocking call with a nonzero timeout in either sub-state is misuse. ### 5.2 Context classes of functions Every function belongs to exactly one class, stated in its `@ctx` annotation (§9): | Class | `@ctx` | Callable from | Rules | |---|---|---|---| | **ISR-safe** | `thread isr` or `thread isr prekernel` | thread, isr, (prekernel) | Never blocks; O(1) or a named bound; may make a thread READY and set `reschedule_pending` | | **Thread-only** | `thread` | thread | May block when the timeout is not `EMB_NO_WAIT`; with `EMB_NO_WAIT` it behaves as its non-blocking form and is still thread-only unless the annotation says `thread isr` | | **Pre-kernel** | `prekernel` or `prekernel thread` | before start, (thread) | Object initialization, configuration, registration | Convention: object `init` functions are `prekernel thread`; signal-type operations (`give`, `set`, `notify_set`, `work_submit`, `queue_send` with `EMB_NO_WAIT`) are `thread isr`; everything that can wait is `thread`. A blocking function called from an ISR is misuse even with `EMB_NO_WAIT` unless its annotation includes `isr`. Rationale: the ISR-safe variant of an operation is a property of its implementation path (no ownership tracking, no priority inheritance), not of the timeout value; the annotation is the contract. Where a blocking operation has a sound non-blocking path from ISR context, the annotation says so (`emb_queue_send` with `EMB_NO_WAIT` is the canonical case and is annotated `thread isr`). ### 5.3 Misuse versus runtime condition A **misuse** is a programming error in the caller that a correct program never makes. A **runtime condition** is an outcome a correct program must handle. The distinction decides the behavior in checked builds. | Situation | Kind | Checked build (`CONFIG_EMB_CHECKED=y`) | Release build | |---|---|---|---| | Wrong context for the function's class | misuse | kernel fault `EMB_FAULT_API_CONTEXT` | `EMB_EPERM` | | Blocking with nonzero timeout while scheduler locked or in a critical section | misuse | fault `EMB_FAULT_API_CONTEXT` | `EMB_EPERM` | | Null required pointer, out-of-range constant, bad enum | misuse | fault `EMB_FAULT_API_ARGUMENT` | `EMB_EINVAL` | | Invalid handle (null, wrong type tag) | misuse | fault `EMB_FAULT_API_HANDLE` | `EMB_EINVAL` | | Stale handle (generation mismatch) | runtime condition in profiles that check it | `EMB_ESTALE` | `EMB_ESTALE` | | Unlock by non-owner | misuse | fault `EMB_FAULT_API_OWNER` | `EMB_EPERM` | | Destroy with waiters, no `ABORT_WAITERS` | misuse | fault `EMB_FAULT_API_LIFECYCLE` | `EMB_EBUSY` | | Storage misaligned or too small | misuse | fault `EMB_FAULT_API_ARGUMENT` | `EMB_EINVAL` | | Operation not valid in current object state | runtime condition | `EMB_ESTATE` | `EMB_ESTATE` | | Timeout, cancel, destroyed-while-waiting, busy, no memory, unsupported | runtime condition | status | status | **Isolated profiles.** A call from an unprivileged partition crosses the syscall boundary, where every check above is performed and *always* returns a status to the caller; an unprivileged partition can never cause a kernel fault by misusing the API (KRN-CAP-002). Faults in checked builds apply to privileged callers only. Faults carry the API function identifier and the offending argument index into the crash record (03 §10.2). ## 6. Handles, storage, and attributes ### 6.1 Handles A handle is one machine word wrapped in a per-object struct type so that the compiler keeps object types apart: ```c #define EMB_DECLARE_HANDLE(name) typedef struct name { uintptr_t raw; } name EMB_DECLARE_HANDLE(emb_thread_t); EMB_DECLARE_HANDLE(emb_mutex_t); /* ... one per kernel object type ... */ #define EMB_HANDLE_NULL { 0 } #define EMB_HANDLE_IS_NULL(h) ((h).raw == 0) #define EMB_HANDLE_EQ(a, b) ((a).raw == (b).raw) ``` The word holds a pointer (tiny and base default), a registry index plus slot generation (planned INDEXED option), or a partition capability-table index plus slot generation (planned TABLE model); the API signature is the same in all three (KRN-CAP-001, ADR-014). Handles are passed by value, copied freely, and compared only with `EMB_HANDLE_EQ`. Nothing in application code dereferences a handle. All three ABIs pass a one-word struct in a single register. EmbCC does not inline a function that takes or returns a struct (09 §7, optimizer gaps); public API functions are real calls anyway, and the handle helpers above are macros, so there is no cost. ### 6.2 Storage Every object type has a generated storage type and size constant (ADR-006): ```c /* generated into from the configured kernel layout */ #define EMB_MUTEX_STORAGE_SIZE 16u #define EMB_MUTEX_STORAGE_ALIGN 4u typedef union emb_mutex_storage { emb_max_align_t align_; EMB_ALIGNED(EMB_MUTEX_STORAGE_ALIGN) unsigned char bytes_[EMB_MUTEX_STORAGE_SIZE]; } emb_mutex_storage_t; ``` The kernel verifies with `_Static_assert` that the real object fits the generated size and alignment. Storage belongs to the caller for the object's whole life and may be `static`, in a table, or on a long-lived stack frame; the kernel never frees it (KRN-OBJ-003). ### 6.3 Attributes Construction options travel in a plain attribute struct. `NULL` means defaults. ```c typedef struct emb_mutex_attr { const char *name; /* optional, kept by pointer: must outlive the object; NULL allowed */ uint8_t protocol; /* EMB_MUTEX_INHERIT (default) | EMB_MUTEX_CEILING | EMB_MUTEX_NONE */ uint8_t ceiling; /* for EMB_MUTEX_CEILING */ uint8_t flags; /* EMB_MUTEX_RECURSIVE, EMB_OBJ_ABORT_WAITERS, ... */ } emb_mutex_attr_t; void emb_mutex_attr_default(emb_mutex_attr_t *out_attr); /* C++17 has no designated initializers */ ``` Attribute fields that hold enumeration values are fixed-width integers (`uint8_t`), not enum types, because enums are `int`-sized on EmbCC (`-fshort-enums` is refused) and public struct layout must not depend on that. ### 6.4 Construction and destruction ```c emb_status_t emb_mutex_init(emb_mutex_storage_t *storage, const emb_mutex_attr_t *attr, /* NULL = defaults */ emb_mutex_t *out_mutex); emb_status_t emb_mutex_destroy(emb_mutex_t mutex); ``` `init` is `prekernel thread`. On failure `*out_mutex` is `EMB_HANDLE_NULL`. `destroy` follows KRN-OBJ-001. Compile-time definition helpers exist for the common static case: ```c EMB_MUTEX_DEFINE(name, attr_initializer...) /* storage + handle variable + registration in the init table */ ``` The `_DEFINE` form registers the object in a generated initialization table so it is constructed during `emb_kernel_init()`, before the scheduler starts without hand-written init code; the table is one of the bracketed sections `embld` and GNU ld both provide (09 §7). ## 7. Time ### 7.1 Types ```c typedef uint64_t emb_tick_t; /* uint32_t when CONFIG_EMB_TICK_32BIT=y (tiny profile) */ typedef struct emb_instant { emb_tick_t ticks; } emb_instant_t; /* a point on the kernel monotonic clock */ typedef struct emb_duration { emb_tick_t ticks; } emb_duration_t; /* a length of time */ typedef struct emb_timeout { emb_tick_t ticks; } emb_timeout_t; /* how long a call may wait */ ``` Three structs over the same scalar: the compiler rejects an instant where a duration is expected, which is the class of bug this exists to prevent (drifting periodic loops, deadlines compared with durations). The cost is one or two registers per argument on every ABI. EmbCC's inliner declines functions taking or returning structs (09 §7), so every helper on these types that must be free is a macro, and the kernel converts to raw `emb_tick_t` once at the API boundary. ### 7.2 Tick unit `CONFIG_EMB_TICK_NS` is the length of one tick in nanoseconds (default 1 000 000, one millisecond). In tickless configurations the kernel clock advances in the same unit from the hardware timer. All conversions are relative to this constant and are exact at compile time where the argument is a constant. ### 7.3 Constructors and conversions ```c #define EMB_TICKS(n) ((emb_duration_t){ .ticks = (emb_tick_t)(n) }) #define EMB_NS(n) ((emb_duration_t){ .ticks = EMB_NS_TO_TICKS(n) }) #define EMB_US(n) ((emb_duration_t){ .ticks = EMB_US_TO_TICKS(n) }) #define EMB_MS(n) ((emb_duration_t){ .ticks = EMB_MS_TO_TICKS(n) }) #define EMB_SEC(n) ((emb_duration_t){ .ticks = EMB_SEC_TO_TICKS(n) }) #define EMB_NO_WAIT ((emb_timeout_t){ .ticks = 0 }) #define EMB_WAIT_FOREVER ((emb_timeout_t){ .ticks = EMB_TICK_MAX }) #define EMB_TIMEOUT(d) emb_timeout_from_duration(d) /* saturates to EMB_TICK_MAX - 1 so a finite wait never reads as forever */ emb_instant_t emb_time_now(void); /* thread isr prekernel */ emb_instant_t emb_instant_add(emb_instant_t t, emb_duration_t d); /* 64-bit: saturating; 32-bit: wrapping */ emb_duration_t emb_instant_sub(emb_instant_t later, emb_instant_t earlier); /* 0 if later < earlier */ bool emb_instant_before(emb_instant_t a, emb_instant_t b); /* wrap-safe in the 32-bit profile */ uint64_t emb_duration_to_ns(emb_duration_t d); /* saturating */ ``` Rounding rule: a duration-to-ticks conversion rounds **up**, so that a wait of `EMB_MS(3)` on a 2 ms tick waits 2 ticks, never 1. A ticks-to-duration conversion is exact. Unit conversions saturate for both constants and runtime values; `emb_duration_is_saturated()` detects saturation. The generic C11/C++17 API does not promise compile-time overflow diagnostics or constant-expression evaluation of unit conversions. `EMB_TICKS(n)` is a raw cast constructor; callers must supply a representable value (A1). C++17 wrappers expose the same three types as `constexpr` strong types with `std::chrono` conversions. ### 7.4 Blocking semantics - An operation may complete before its timeout when satisfied, canceled, or destroyed. Expiry makes an unsatisfied waiter eligible to return `EMB_ETIMEDOUT`; actual return also depends on dispatch and, for a condition variable, mutex reacquisition. There is no universal finite return-time guarantee under arbitrary higher-priority load or suspension. A response-time bound requires the workload and resource assumptions of [Timing and evidence](../engineering/timing-and-evidence.md). - `EMB_NO_WAIT` never blocks and never yields. - `EMB_WAIT_FOREVER` has no deadline; the wait ends only by satisfaction, cancel, or destroy. - Every blocking function has an absolute form `..._until(..., emb_instant_t deadline)`; a past wait deadline behaves as `EMB_NO_WAIT`; sleep with a zero duration or past deadline yields with `EMB_OK`. Periodic work uses `emb_thread_sleep_until(next)` with `next = emb_instant_add(next, period)` so that drift does not accumulate (KRN-TIM-011). - 64-bit time is read with `emb_time_now()`, which uses a sequence lock or critical section internally; no 64-bit atomic is involved (KRN-TIM-016). ## 8. Headers ``` include/emb/ emb.h umbrella: includes every public header below config.h GENERATED from Kconfig: CONFIG_EMB_* as macros storage.h GENERATED: EMB_*_STORAGE_SIZE/ALIGN and storage types version.h EMB_VERSION_*, EMB_API_VERSION, emb_version() compiler.h the portability macros (§10); includes compiler/.h types.h emb_tick_t, emb_max_align_t, EMB_DECLARE_HANDLE, handle macros status.h emb_status_t, the error table, emb_status_name time.h §7 context.h emb_context(), EMB_CONTEXT_*, emb_in_isr() thread.h sched.h mutex.h sem.h event.h notify.h cond.h queue.h pipe.h pool.h timer.h work.h part.h cap.h mem.h irq.h power.h fault.h log.h trace.h kernel.h compiler/ gcc.h clang.h embcc.h (one per compiler, nothing else includes them directly) ``` This is the target header inventory; unimplemented headers are listed in [Project status](../STATUS.md). `kernel.h` provides init/start, the initialization table, and `EMB_CHECK`; M1 `trace.h` exposes the trace sink subset. `arch.h` is the port contract and is not public application API. Rules for every public header: - Include guard `EMB__H` (`EMB_MUTEX_H`); no `#pragma once` (EmbCC drops unknown pragmas silently but `once` is honored; the guard is the portable form and is kept as the single mechanism). - `#ifdef __cplusplus extern "C" {` around declarations. - Compiles as C11 (`-std=c11 -pedantic` on GCC and Clang) and as C++17, with the project's warning set clean, on EmbCC, GCC, and Clang (API-004). - Includes only ``, ``, ``, and other `` headers. No hosted libc headers. - No function definitions except `static inline` helpers that are trivial accessors; no macros that evaluate an argument twice; no variable-length arrays; no bit-fields in ABI structs; no anonymous structs (not C++17); anonymous unions only where every compiler accepts them. - Identifiers avoid C++ keywords (`new`, `delete`, `class`, `this`, `template`, `operator`, `export`, `namespace`, `typename`, `register`). - Every struct that crosses the API has fixed-width members and explicit padding, so its layout is identical on all compilers for a target. - `const` on every pointer to input data. `volatile` never appears in a public header. - Enumerations appear as `enum` types in prototypes only where the full value set is closed and documented; in structs they are fixed-width integers (§6.3). ## 9. Machine-readable contract annotations The target contract gives each function structured tags. Current `tools/docs/build.py` extracts them into `catalog/api-annotations.json` and renders an omission audit alongside the Doxygen/Breathe reference. It does not yet enforce complete header compliance or drive static analysis, conformance generation or syscalls. The richer `tools/apidoc/` contract/schema remains planned (API-003). ```c /** * emb_mutex_lock() - Acquire a mutex, waiting up to @timeout. * @mutex: The mutex. * @timeout: How long to wait; EMB_NO_WAIT or EMB_WAIT_FOREVER allowed. * * @ctx thread * @blocks timeout * @time O(1) acquire; O(waiters) enqueue in PRIORITY_FIFO wait order * @owns none * @config CONFIG_EMB_MUTEX * @req KRN-SYNC-001 KRN-SYNC-003 KRN-SYNC-009 * @since 1.0 * @stable yes * * Return: EMB_OK on acquisition; EMB_ETIMEDOUT; EMB_ECANCELED; EMB_EDESTROYED; * EMB_EOWNERDEAD when configured; EMB_EPERM from a disallowed context * (release builds); EMB_EINVAL or EMB_ESTALE for a bad handle. */ emb_status_t emb_mutex_lock(emb_mutex_t mutex, emb_timeout_t timeout); ``` Tag grammar (one tag per line, order free, every tag mandatory unless marked optional): | Tag | Values | Meaning | |---|---|---| | `@ctx` | subset of `thread isr prekernel` | Contexts the function may be called from (§5.2) | | `@blocks` | `no` or `timeout` | `timeout`: may block, bounded by the trailing timeout or deadline parameter; `no`: never blocks | | `@time` | Big-O expressions over `priorities`, `waiters`, `timeouts`, `items`, `len`, `regions`, `caps`, `owned`, `threads`, `stack`, `depth` or `PI work`; free text after the bound | Timing class per configuration; a function with more than one phase lists each; PI work includes scans/requeues along the visited chain as defined in the timing/evidence guide | | `@owns` | `none`, `takes:`, `gives:`, `borrows:` | Ownership transfer of a buffer or object across the call | | `@config` | zero or more `CONFIG_EMB_*` | Symbols that must be enabled for the function to exist | | `@req` | one or more requirement identifiers | Traceability (05 §6); never a capability-right name | | `@right` (required for future syscall generation) | explicit handle-parameter to object-right mapping, or `none` | Capability enforcement; separate from requirement IDs | | `@arg` (required for future syscall generation) | explicit argument kind, length relation, access, maximum, and lifetime | Marshaller validation; not inferred solely from C pointer types | | `@since` | version | First release with this signature | | `@stable` | `yes`, `experimental`, `deprecated:` | Stability promise (§11) | | `@isr-note` (optional) | free text | What differs when called from an ISR, for `thread isr` functions | The current `tools/docs/build.py` documentation gate rejects an extracted function lacking one of the eight current contract tags, a parameter description or a resolvable requirement ID. It preserves fields wrapped by the comment formatter. This is a presence/reference check, not a full grammar validator or proof that the values match implementation. The broader `tools/apidoc` schema and semantic checks remain planned. Future syscall generation requires explicit `@right` and `@arg` metadata; current headers do not supply it and are not ready for generated security checks. The current JSON audit is one record per extracted function with a `tags` map, missing-field lists and evidence kept at `not evaluated`; it is distinct from the future schema. ## 10. Compiler portability layer Generic code uses only these macros for compiler-specific behavior; they are defined per compiler in `include/emb/compiler/.h` and nowhere else (BLD-002, PORT-ABI-002): ``` EMB_INLINE EMB_ALWAYS_INLINE EMB_NOINLINE EMB_NORETURN EMB_UNUSED EMB_USED EMB_WEAK EMB_ALIAS(sym) EMB_SECTION(name) EMB_ALIGNED(n) EMB_PACKED EMB_NAKED EMB_DEPRECATED(msg) EMB_PRINTF_LIKE(fmt_idx, args_idx) EMB_LIKELY(x) EMB_UNLIKELY(x) EMB_UNREACHABLE() EMB_COMPILER_BARRIER() EMB_STATIC_ASSERT(cond, msg) EMB_CONTAINER_OF(ptr, type, member) EMB_CLZ32(x) EMB_CTZ32(x) EMB_POPCOUNT32(x) (portable fallbacks where no builtin exists) EMB_COMPILER_NAME EMB_COMPILER_VERSION ``` Each macro has a defined fallback (possibly empty) so a new compiler can start with the generic header. Attributes refused by a compiler map to nothing with a documented consequence (for example EmbCC refuses `aligned` on typedefs, so `EMB_ALIGNED` is applied to objects only, which the header rules in §8 already require). ## 11. Versioning and stability ```c #define EMB_VERSION_MAJOR 0 #define EMB_VERSION_MINOR 2 #define EMB_VERSION_PATCH 0 #define EMB_VERSION ((EMB_VERSION_MAJOR << 16) | (EMB_VERSION_MINOR << 8) | EMB_VERSION_PATCH) #define EMB_API_VERSION 1 /* increments on any compatible addition to the public API */ const char *emb_version_string(void); ``` - From 1.0, every `@stable yes` function keeps its signature and documented semantics for all `1.x` releases (05 §7.1). Additions bump `EMB_API_VERSION`. - Deprecation: the declaration gains `EMB_DEPRECATED("use emb_x_y")`, the annotation becomes `@stable deprecated:`, and the function stays for at least one minor release. - `experimental` functions live in headers under `include/emb/experimental/` and carry no promise. - Binary ABI stability is promised only at the syscall boundary of isolated profiles and for the versioned data formats (trace, crash record, debug descriptor). Source compatibility is the public promise. ## 12. Callbacks and entry points ```c typedef void (*emb_thread_entry_t)(void *arg); /* returning terminates the thread, KRN-THR-005 */ typedef void (*emb_work_fn_t)(emb_work_t work); /* thread context on the owning work queue */ typedef void (*emb_timer_fn_t)(emb_timer_t timer, void *arg); /* work queue by default; ISR only with EMB_TIMER_ISR_CONTEXT and an ISR-safe body */ typedef void (*emb_fault_hook_t)(const emb_fault_info_t *info); /* may run in any context; must be ISR-safe */ ``` A callback's documentation states its context exactly as a function's `@ctx` does. No public API takes a variadic function or is variadic itself; the logging macros are the only variadic surface and they expand to deferred-format calls (ADR-009). ## 13. Worked example ```c #include static emb_mutex_storage_t g_lock_storage; static emb_mutex_t g_lock; static emb_thread_storage_t g_worker_storage; EMB_THREAD_STACK(g_worker_stack, 256); static void worker(void *arg) { (void)arg; emb_instant_t next = emb_time_now(); for (;;) { next = emb_instant_add(next, EMB_MS(10)); if (emb_mutex_lock(g_lock, EMB_TIMEOUT(EMB_MS(2))) == EMB_OK) { /* ... */ (void)emb_mutex_unlock(g_lock); } (void)emb_thread_sleep_until(next); /* no drift */ } } int main(void) { emb_thread_t t; emb_thread_attr_t ta; emb_kernel_init(); EMB_CHECK(emb_mutex_init(&g_lock_storage, NULL, &g_lock)); emb_thread_attr_default(&ta); ta.name = "worker"; ta.priority = 5; ta.stack = g_worker_stack; ta.stack_size = sizeof g_worker_stack; EMB_CHECK(emb_thread_init(&g_worker_storage, &ta, worker, NULL, &t)); EMB_CHECK(emb_thread_start(t)); emb_kernel_start(); /* does not return */ } ``` `EMB_CHECK(expr)` is a checked-build helper that faults on a non-`EMB_OK` status and is the expression's value in release builds. ## 14. Decisions taken at acceptance (2026-10-07) 1. `emb_status_t` is `int`: the natural return register on every ABI and 16 bits on AVR. Not `int32_t`. 2. The three time types are structs over `emb_tick_t`. Type safety wins over EmbCC's inliner limit; all constructors and free helpers are macros, and the kernel converts to `emb_tick_t` once at the API boundary. 3. `EMB_ETIMEDOUT` is the single code for an unsatisfied wait, including `EMB_NO_WAIT`. No would-block code exists. 4. `emb_status_name()` is enabled by default (`CONFIG_EMB_STATUS_NAMES=y`) in the base, isolated, multicore, and native profiles, and disabled by default in the tiny profile.