Runtime contract: ABI 1.1 / wire 1 / development manifest 1
SDK 1.0 retains the frozen M0 foundation. See common types and lifetime rules, the package contract, and assigned decisions. New services need their own negotiated tables and tests. The broader product contract is PLAN-plugin-sdk.md in the private repository; this document stands alone in the exported SDK.
ABI and ownership
Only fixed-width integers, pointers local to the worker, explicit byte spans and
function pointers cross the C ABI. Native 64-bit pointers, maximum packing of 8,
Windows __cdecl and the platform C calling convention on Linux are required.
No Qt/STL objects, C++ exceptions, RTTI, compiler enums, bools or heap ownership
cross the boundary. Headers assert layouts in both C and C++:
| Structure | Size | Selected offsets |
|---|---|---|
| et_string | 16 | data 0, size 8, reserved 12 |
| et_query | 16 | size 0, ABI 4, capabilities 8 |
| et_host_api | 40 | context 16, log 24, optional get_service 32; ABI 1.0 prefix is 32 bytes |
| et_event | 40 | operation 8, generation 16, text 24 |
| et_plugin_api | 56 | ID 16, start 32, stop 40, event 48 |
Strings are valid UTF-8, without embedded NUL, with explicit byte lengths. Empty
strings may use a null pointer. All reserved fields are zero. Inputs are borrowed
only during a call; the service copies before returning. Plugin ID is immutable
plugin-owned storage valid until worker exit. Host table is worker-owned and valid
through stop; copy Context to retain a reference to it, not its temporary wrapper.
No host call is legal after stop returns. Instance is plugin-owned; its stop handler
destroys it using that module's allocator. A failed start returns a null instance
and releases its own resources. The C++ adapter enforces this even on exceptions.
et_plugin_query receives the host's maximum ABI minor and available capabilities.
The caller supplies output capacity. A plugin rejects short mandatory prefixes or
different major versions, writes only its negotiated prefix, and reports that size.
Future fields are append-only within a major. ABI 1.0 and 1.1 negotiate to the
lower supported minor; 1.1 adds optional service discovery. Incompatible required features fail
before loading the library. Runtime identity must equal the manifest identity and
required capabilities must be a subset of grants. Context metadata is exposed by
a separately versioned table; reserved future services return UNSUPPORTED.
Lifecycle/event/completion callbacks and service calls run on the worker dispatcher.
Only background job work and its cancellation probe run on other threads.
The log proxy returns ET_ERROR_STATE if called from another thread. Background
task queues are implemented in M1; see JOBS.md. Callbacks must return promptly; start and
normal callbacks have a 3-second supervision deadline and stop has 1 second.
Nothing executes or waits on the GUI/ECU thread. The broker destructor only kills
and reaps its own child at application teardown (bounded to 1 second).
Capabilities, features and errors
| Capability | Bit | Feature | Implemented behavior |
|---|---|---|---|
| log | 1 | et.log.v1 | bounded UTF-8 info/warning/error diagnostics |
| events | 2 | et.events.v1 | ordered developer message callback with operation ID |
| none | 0 | et.context.v1 | service/feature metadata and frozen limits |
Events are a foundation for later UI/project events; no project/tune access exists yet. Unknown required features and capabilities are rejected. Unknown optional features are ignored. Recognized features require their capability in the manifest.
| Code | Meaning |
|---|---|
| 0 | OK (log means queued, not a durable disk write) |
| 1 | invalid argument or mandatory structure prefix |
| 2 | incompatible ABI/table/identity |
| 3 | missing capability |
| 4 | message, string, log or queue limit |
| 5 | invalid lifecycle/thread state |
| 6 | caught C++ callback exception |
| 7 | unsupported operation |
| 8 | stale handle/generation |
| 9 | cancelled on worker loss; not replayed |
| 10 | deadline exceeded |
| 11 | protocol violation |
| 12 | revision/expected-value conflict |
| 13 | not found |
| 14 | I/O failure |
| 15 | busy or already committed |
Async event replies carry the operation ID and code. Broker failures retain a human-readable reason and cancel outstanding events. Immediate C calls return a code; optional service discovery also fills caller-owned et_error. The common et_completion/et_async_options contracts are frozen for later asynchronous services.
Transport and wire encoding
Qt local sockets provide Windows named pipes and Linux Unix-domain sockets. No TCP listener is used. See Qt local server permissions. Each launch uses a fresh random endpoint/session, a 256-bit system-random token, same-user endpoint permissions, and native peer PID validation in both directions (Windows pipe PID APIs; Linux SO_PEERCRED including UID). The token and endpoint arrive over the child's inherited stdin pipe, never command-line arguments or an environment variable. No plugin library is loaded before the welcome handshake. This prevents accidental session crossing, not hostile code under the same account.
Frame: four-byte unsigned little-endian payload length, followed by a UTF-8 JSON object. C structs are never copied over the wire. A frame's payload is 1..65536 bytes. Numeric envelope fields are exact uint32 integers. The session is a UUID string. Sequences start at 1 independently in each direction and must increase by exactly one. Request IDs are globally increasing for host requests; replies echo them. Generation must be zero in this worker-global preview. Nonzero/stale generations, session mismatches, unknown envelope/body fields and messages in the wrong state fail the connection. Unknown optional manifest features are the only ignored fields. JSON objects use Qt's parser (duplicate member names resolve to the last value); signed package manifests instead forbid duplicates and use the frozen JCS subset defined in PACKAGE.md. Production verification is M5.
Envelope keys: wire, session, seq, request, generation, type, body.
request=0 means unsolicited/lifecycle; events and ping use nonzero request IDs.
| Message | Direction | Exact body |
|---|---|---|
| hello | worker -> broker | token:string, id:string |
| welcome | broker -> worker | capabilities:uint32 |
| ready | worker -> broker | empty |
| log | worker -> broker | level:1..3, text:string |
| ping / pong | broker -> worker / reply | empty |
| event | broker -> worker | type:1 (message), text:string |
| result | worker -> broker | code:0..15 |
| stop / stopped | broker -> worker / reply | empty |
| error | worker -> broker | code:1..15, detail:string (max 512 characters) |
Limits and lifecycle
| Resource | Preview limit |
|---|---|
| Manifest / frame payload | 64 KiB |
| ABI text / log / event | 4096 UTF-8 bytes |
| Outbound socket queue | 256 KiB per direction |
| Decoded message rate | 200 per fixed 1-second window, per direction |
| Logs | 100 per fixed 1-second window (caller sees LIMIT) |
| Pending event requests | 32 per worker |
| Startup / event / heartbeat response | 3000 ms |
| Heartbeat interval | 500 ms |
| Shutdown | 1000 ms |
| Feature lists | 32 entries each |
| Future service quotas | Nonzero frozen ceilings in FOUNDATION.md; services are unavailable until negotiated |
The decoder buffers at most two bounded frames while parsing; socket reads yield between bounded batches. Pending events, outbound buffers and diagnostic messages are bounded; arbitrary native stdout/stderr goes to the OS null device. No growing native-output capture is retained. Public SDK thread queues are not implemented. Supervision runs every 100 ms, so deadlines allow that scheduling granularity. The five-worker 100 Hz/30-second transport benchmark records p50/p95/p99/max, queue occupancy and explicit fault gaps. Budgets: p95 <50 ms, p99 <100 ms, max <3000 ms. Actual telemetry/ECU-poll baseline verification remains M3.
Development lifecycle: manifest validated/compatible -> Starting -> Ready -> Stopping -> Stopped. Any failure transitions to Failed, cancels pending operations, and terminates only that session's worker. Manual restart waits until it has exited, creates a new session and never replays events. Safe mode refuses all starts. Signed verification, enabled/discovered management, a GUI recovery state, and application startup integration are not part of this checker yet.
Platform and dependency rules
Initial API targets: windows-x64, linux-x64 and linux-arm64. macOS is deferred.
Native modules must match the running worker's OS and architecture. There is no
Windows-to-Linux binary portability promise. Use CMake MODULE output (.dll on
Windows, .so on Linux) beside the development manifest, no path traversal or
symlink outside its directory. The worker uses restricted LoadLibraryEx search on
Windows and absolute-path RTLD_NOW/RTLD_LOCAL on Linux. Linux packaged dependencies
should use an $ORIGIN RPATH; native OS loaders still resolve system dependencies.
LD_PRELOAD/LD_LIBRARY_PATH/DYLD injection variables are removed from worker launch.
The standalone runtime needs Qt Core/Network/Test 6.4+ to build its own test suite; the main EpicTuner build continues to use its existing Qt 6.11 kit. SDK author code never links Qt. Compiler runtime dependencies remain the plugin author's packaging responsibility. Production packaging/licensing, task dispatch, UI rendering and real data/tune services retain their assigned later gates in DECISIONS.md; their common contracts and identifiers are defined now.