M0 foundation contract
SDK 1.0 retains the ABI 1.1 foundation frozen in M0. The original ABI 1.0 prefixes remain binary compatible. These definitions are frozen for the initial Windows x64, Linux x64 and Linux ARM64 targets. Breaking layouts/semantics require an ABI or named feature major change. macOS is a later certification target. This contract does not advertise later services as implemented merely because their identifiers are reserved.
Query, tables and caller-owned storage
et_plugin_query takes the largest ABI minor the host supports and the granted
capability mask. Return the lower supported minor within the same major. Reject a
different major, missing required capability, or insufficient mandatory prefix.
An error leaves the output table unchanged. A success writes only the negotiated
prefix and reports its actual size. Extra caller capacity remains untouched.
et_plugin_api stays 56 bytes. et_host_api retains its original 32-byte prefix;
ABI 1.1 appends get_service at offset 32, total 40 bytes. Check struct_size before
reading that pointer. An ABI 1.0 host has no service discovery and the C++ wrapper
returns ET_ERROR_UNSUPPORTED without reading past its prefix. A newer host can
run an older plugin. Append-only service tables negotiate versions independently.
get_service(context, id, requestedVersion, table, capacity, error) copies a table
into caller-owned storage; it never returns heap ownership. The context table v1
is 32 bytes and provides limit and named-feature queries. Unknown/unimplemented
services return UNSUPPORTED; incompatible service majors return ABI; insufficient
output capacity returns ARGUMENT. Optional et_error must have a valid size prefix.
Its fixed 512-byte detail buffer belongs to the caller, is NUL terminated, and
reports byte length excluding the terminator. Immediate calls use operation ID 0.
Too-short error/output prefixes are never overwritten.
Current features: et.context.v1, et.log.v1, et.events.v1. Feature availability
and capability grants are separate: metadata can describe an implemented feature
whose capability was not granted. An unavailable future service is not returned as
an empty success table. See contracts.h for stable service IDs 1–11 and capability
bits 0–14. Reserved capability names are catalogued in the package schema. Only
log/events are granted by this development runtime; context metadata needs no grant.
Values and handle lifetime
et_value is a 40-byte tagged union: null, bool (0 or 1), signed/unsigned 64-bit
integer, finite binary64, UTF-8 string, bytes, or handle. Reserved fields are zero.
Inactive union storage is ignored and never serialized. All pointed-to storage is
borrowed only during the call/callback. Text rejects NUL, malformed/overlong UTF-8,
surrogates and out-of-range scalars. Bytes may contain arbitrary data. Use the SDK
validators before enqueueing. Copy inputs before returning from any submit call;
copy completion data before returning from its callback. No module frees another
module's allocation, including error messages and output tables.
et_handle is 32 bytes: opaque session tag, project generation (0 means global),
slot, slot generation, kind and reserved. Slot/session/generation 0 is invalid.
Handles confer no authority unless the host registry resolves the full tuple in
the owning session and required scope/kind. Closing a panel invalidates its nodes
and panel-scoped resources; closing a project invalidates its scoped handles.
Global handles survive project changes, but no handle survives worker replacement.
Reusing a slot increments generation; exhausted generations retire the slot rather
than wrap. Stale/project/kind/session mismatches return STALE without dereferencing
any plugin-supplied pointer. The host reference registry and tests exercise these
rules; future service adapters must use equivalent checks at dispatch.
UI placement flags are workspace=1, floating=2, modal=4. Every panel instance has
independent handles; single-instance reopening resolves/focuses the existing valid
instance. Node/property schemas and concrete prefab service tables belong to named
et.ui.v1 feature work in M2/M4; raw QML injection is never a fallback. This freezes
the common identifiers/lifetimes without claiming a working renderer.
Async syntax, ordering and cancellation
Future asynchronous service submissions use a caller-owned et_async_options:
et_async_options options{};
options.struct_size = sizeof(options);
options.timeout_ms = 1000;
options.context = myPluginState;
options.complete = [](void* state, const et_completion* result) {
// Inspect result->code/operation_id; copy any needed value/detail now.
// Callback and its data belong to the worker dispatcher for this call only.
};
// A service's submit(..., &options, &operationId) returns a synchronous et_result.
// Concrete submit functions are supplied only by implemented, negotiated services.
Accepted submission returns OK and a nonzero operation ID; rejected submission returns an error, operation ID 0, and schedules no callback. IDs monotonically increase without reuse in a session. Local callback pointers/context never cross IPC: the worker stores them and routes wire completions by session/operation ID. Successful submission has one terminal completion while the worker remains alive; stop revokes its callback registrations after draining/cancelling unsent work. Worker death cannot deliver callbacks into dead code; the host still retains the operation outcome for diagnostics/reconciliation and never replays side effects.
Operation phases are queued -> running -> committed -> complete. “Committed” means an irreversible host operation has been dispatched, not that an ECU flash burn completed. Cancellation before that boundary yields CANCELLED exactly once. After it, cancellation returns BUSY and the host preserves the real outcome. Deadlines use monotonic host time, 1..3000 ms, and cancel undispatched/ordinary work with TIMEOUT. Project change yields STALE for unfinished pre-commit work. Already-sent host writes retain their outcome through cancellation, expiry and worker loss; M4 implements the ECU readback/partial-write adapter. Completion acknowledgement releases ledger capacity. Unknown, duplicate or replayed IDs never repeat a write.
et_completion is 80 bytes with size, code, operation ID, project generation,
tagged value and borrowed detail. Failed completions use a null value. Numeric
64-bit IDs/values use canonical decimal strings on future JSON service bodies to
avoid precision loss; existing worker wire correlation IDs remain uint32. Never
serialize these native structs, padding, pointers or callbacks verbatim.
Limits and execution policy
contracts.h is the numeric authority and the context service reports it. These
are contract ceilings, not grants: a service unavailable today still cannot be
used merely because a future limit is nonzero.
| Resource | Per-worker ceiling |
|---|---|
| Wire frame / outgoing queue | 64 KiB / 256 KiB |
| Pending operations | 32, including unacknowledged terminal outcomes |
| Text / structured error detail | 4096 / 511 UTF-8 bytes plus error terminator |
| UI nodes / nesting | 4096 / 32 |
| Panel definitions / instances | 32 / 64 |
| Subscriptions / background jobs | 64 / 16 |
| UI updates / data source | 30 Hz / 100 Hz |
| Logs / wire messages | 100 / 200 per fixed second |
| Startup / ordinary operation | 3000 ms / 3000 ms |
| Heartbeat interval / response | 500 ms / 3000 ms |
| Shutdown / supervisor resolution | 1000 ms / 100 ms |
Future display streams coalesce at 30 Hz and report losses; acquisition batches preserve source timestamps/gap counters. M0 validates transport/state contracts; M3 validates real channel/recording adapters against a matched ECU-poll baseline. No callback runs on or synchronously blocks the application GUI/ECU thread. Service calls require the dispatcher. M1 background tasks publish through bounded thread-safe result mailboxes; submission rejects at capacity. See JOBS.md.
Performance gate: five isolated test workers, 100 Hz timestamped synthetic source, 30 seconds, one crash/manual restart, no completion replay, queue occupancy <=32, explicit cancellation/gap counts, p95 <50 ms, p99 <100 ms and maximum <3000 ms. Startup/shutdown must retain their stated deadlines. This is a transport benchmark, not endurance, UI rendering, ECU polling or physical Raspberry Pi proof.
Full lifecycle contract
| State | Allowed next state and condition |
|---|---|
| Discovered | Verified after package authenticity policy; Failed/Disabled on rejection |
| Verified | Compatible after target/host/ABI/required-feature checks |
| Compatible | Enabled after user grant/license policy; Disabled otherwise |
| Enabled | Starting on explicit activation, or Disabled |
| Starting | Ready after authenticated handshake + successful start; Failed on deadline/error |
| Ready | Stopping on disable/update/close; Failed on crash/protocol/heartbeat error |
| Stopping | Stopped after both stop acknowledgement and normal process exit; Failed on deadline/error |
| Stopped | Starting only after renewed compatibility/grant checks, or Disabled |
| Failed | Disabled or explicit revalidation/restart; never automatic write replay |
| Disabled | Verified/Compatible/Enabled only after explicit re-enable and renewed checks |
The development checker explicitly substitutes unsigned development opt-in for production verification and does not claim a verified signature. Safe mode prevents all starts. The GUI manager and production installer implement the preceding policy states in M1/M5; the runtime skeleton enforces starting through stopped/failed.