2026-09-16 21:43:01 +02:00
|
|
|
/* GameCult.Mesh.Quic.Native — C ABI v2, the runtime-neutral CultMesh QUIC bridge.
|
|
|
|
|
*
|
|
|
|
|
* This header is the contract. It is the single place the event layout, the
|
|
|
|
|
* exports, the id and return-code conventions, the object lifetime rules and the
|
|
|
|
|
* threading rules are stated; `cultmesh_quic_native.cpp` includes it, the README
|
|
|
|
|
* points at it, and a host that cannot include it (koffi, P/Invoke) declares the
|
|
|
|
|
* same thing in its own FFI and is checked against this file by a test.
|
|
|
|
|
*
|
|
|
|
|
* It is C. It includes nothing of MsQuic, nothing of Node, nothing of Unity, and
|
|
|
|
|
* nothing platform-specific: a host binds these twelve queue exports plus
|
|
|
|
|
* `cultmesh_quic_last_status` and needs no other build input.
|
|
|
|
|
*
|
|
|
|
|
* The v1 Unity exports (`cultmesh_quic_open`, `_state`, `_poll`, `_error`,
|
|
|
|
|
* `_close`) are deliberately not here. They are a Windows-only shim for a
|
|
|
|
|
* shipped managed connector, not a contract offered to new hosts.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#ifndef CULTMESH_QUIC_NATIVE_H
|
|
|
|
|
#define CULTMESH_QUIC_NATIVE_H
|
|
|
|
|
|
|
|
|
|
#include <stddef.h>
|
|
|
|
|
#include <stdint.h>
|
|
|
|
|
|
|
|
|
|
#if defined(__cplusplus)
|
|
|
|
|
#define CULTMESH_QUIC_STATIC_ASSERT(condition, message) static_assert(condition, message)
|
|
|
|
|
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
|
|
|
|
|
#define CULTMESH_QUIC_STATIC_ASSERT(condition, message) _Static_assert(condition, message)
|
|
|
|
|
#else
|
|
|
|
|
#define CULTMESH_QUIC_STATIC_ASSERT(condition, message)
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
/* `CULTMESH_QUIC_BUILD` is defined by the bridge's own translation unit and by
|
|
|
|
|
* nothing else. A consumer gets plain declarations and links by name, which is
|
|
|
|
|
* what every host here does: all three load the library at runtime. */
|
|
|
|
|
#if defined(CULTMESH_QUIC_BUILD)
|
|
|
|
|
# if defined(_WIN32)
|
|
|
|
|
# define CULTMESH_QUIC_API __declspec(dllexport)
|
|
|
|
|
# else
|
|
|
|
|
# define CULTMESH_QUIC_API __attribute__((visibility("default")))
|
|
|
|
|
# endif
|
|
|
|
|
#else
|
|
|
|
|
# define CULTMESH_QUIC_API
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#if defined(__cplusplus)
|
|
|
|
|
extern "C" {
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* 1. The event
|
|
|
|
|
*
|
|
|
|
|
* Exactly 64 bytes, no implicit padding, field order below is the ABI. A host
|
|
|
|
|
* that reorders a field reads a different struct; the offsets are asserted here
|
|
|
|
|
* so a compiler that pads differently fails the build rather than the wire.
|
|
|
|
|
* --------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
struct cultmesh_quic_event {
|
|
|
|
|
uint32_t type; /* offset 0 — one of the cultmesh_quic_event_type values */
|
|
|
|
|
uint32_t stream_kind; /* offset 4 — 1 reliable, 2 latest-only; 0 when not a stream event */
|
|
|
|
|
uint64_t listener_id; /* offset 8 — 0 for an outbound connection */
|
|
|
|
|
uint64_t connection_id; /* offset 16 */
|
|
|
|
|
uint64_t stream_id; /* offset 24 */
|
|
|
|
|
uint64_t code; /* offset 32 — meaning depends on `type`; see below */
|
|
|
|
|
int32_t status; /* offset 40 — the raw platform QUIC status, diagnostics only */
|
|
|
|
|
int32_t payload_length; /* offset 44 — bytes written into the caller's payload buffer */
|
|
|
|
|
uint8_t reserved[16]; /* offset 48 — zero today; do not read meaning into it */
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
CULTMESH_QUIC_STATIC_ASSERT(sizeof(struct cultmesh_quic_event) == 64,
|
|
|
|
|
"the event struct is a 64-byte ABI contract");
|
|
|
|
|
CULTMESH_QUIC_STATIC_ASSERT(offsetof(struct cultmesh_quic_event, type) == 0,
|
|
|
|
|
"event field offsets are an ABI contract");
|
|
|
|
|
CULTMESH_QUIC_STATIC_ASSERT(offsetof(struct cultmesh_quic_event, stream_kind) == 4,
|
|
|
|
|
"event field offsets are an ABI contract");
|
|
|
|
|
CULTMESH_QUIC_STATIC_ASSERT(offsetof(struct cultmesh_quic_event, listener_id) == 8,
|
|
|
|
|
"event field offsets are an ABI contract");
|
|
|
|
|
CULTMESH_QUIC_STATIC_ASSERT(offsetof(struct cultmesh_quic_event, connection_id) == 16,
|
|
|
|
|
"event field offsets are an ABI contract");
|
|
|
|
|
CULTMESH_QUIC_STATIC_ASSERT(offsetof(struct cultmesh_quic_event, stream_id) == 24,
|
|
|
|
|
"event field offsets are an ABI contract");
|
|
|
|
|
CULTMESH_QUIC_STATIC_ASSERT(offsetof(struct cultmesh_quic_event, code) == 32,
|
|
|
|
|
"event field offsets are an ABI contract");
|
|
|
|
|
CULTMESH_QUIC_STATIC_ASSERT(offsetof(struct cultmesh_quic_event, status) == 40,
|
|
|
|
|
"event field offsets are an ABI contract");
|
|
|
|
|
CULTMESH_QUIC_STATIC_ASSERT(offsetof(struct cultmesh_quic_event, payload_length) == 44,
|
|
|
|
|
"event field offsets are an ABI contract");
|
|
|
|
|
CULTMESH_QUIC_STATIC_ASSERT(offsetof(struct cultmesh_quic_event, reserved) == 48,
|
|
|
|
|
"event field offsets are an ABI contract");
|
|
|
|
|
|
|
|
|
|
enum cultmesh_quic_event_type {
|
|
|
|
|
/* A listener accepted an inbound connection. `listener_id`, `connection_id`. */
|
|
|
|
|
CULTMESH_QUIC_EVENT_LISTENER_NEW_CONNECTION = 1,
|
|
|
|
|
/* The handshake completed. `connection_id`. */
|
|
|
|
|
CULTMESH_QUIC_EVENT_CONNECTION_CONNECTED = 2,
|
|
|
|
|
/* Payload: the peer's DER certificate. The handshake is blocked until the
|
|
|
|
|
* host answers with `cultmesh_quic_connection_certificate_complete`. */
|
|
|
|
|
CULTMESH_QUIC_EVENT_CONNECTION_CERTIFICATE_RECEIVED = 3,
|
Close the three ABI gaps the bridge left open
Event 7 was not one per frame. The bridge writes the stream's kind byte
itself, that send completed as an anonymous event 7, and a host counting
completions saw six for five frames with nothing to tell them apart. The
send request now says which send it was; the kind byte's completion is the
bridge's own business and is dropped. A send MsQuic released without
putting on the wire reports it: event 7 carries a send result in `code`,
completed or canceled, so an aborted send is accounted for rather than
silently missing. Measured: 32 sends queued under an abort, 32 event 7, 28
canceled and 4 completed.
The initiator of a shutdown learned nothing. MsQuic gives it only
shutdown-complete, and the bridge swallowed that, so a host could end its
own connection and never see the id end. Event 4 now has one emitter —
shutdown-complete — and the two initiated-by callbacks record why rather
than announcing it. Each connection id gets exactly one event 4, whoever
ended it, with the code its side passed, and it is emitted after the id
has stopped resolving, so a host told a connection is gone cannot still
resolve it on the next line.
Return codes inverted by platform. QUIC_STATUS is a negative HRESULT on
Windows and a positive errno on POSIX, so a Linux host checking for a
negative code saw every failure as success, against a README that promised
negative. A MsQuic refusal is now -(1000 + (status & 0xffff)) on both, and
cultmesh_quic_last_status hands over the raw bits for diagnostics. On
win32-x64 a garbage PKCS12 returns -12032 with last_status 0x80072B18.
The v1 exports move with it; the managed connector tests the value against
zero and formats it, so the contract it was shipped with holds.
Smaller ones, from the same pass: AppendTrace is v1-only and bounded,
because a long-lived v2 connection grew that string once per MsQuic event
for nothing to read; a listener configuration failure takes the connection
out of the map instead of leaving it naming a handle MsQuic is about to
close; and next_event refuses a null payload with a non-zero capacity
rather than treating the lie as a discard.
Soul's C consumer now compiles against the header and asserts its own
struct and its eleven bound prototypes against it, and the koffi smoke
checks its declarations and the thirteen exports the same way: 20 of 20,
seq clean, cancel clean, the v1 gate 3 passed 1 skipped as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:00:06 +02:00
|
|
|
/* The connection is gone, whoever ended it, and its id no longer resolves.
|
|
|
|
|
* Payload: a UTF-8 reason. `code` is the QUIC application error code — the
|
|
|
|
|
* peer's when the peer ended it, and the code the host passed to
|
|
|
|
|
* `cultmesh_quic_connection_shutdown` when the host did. Emitted exactly
|
|
|
|
|
* once per connection id, including for the initiator of a local shutdown:
|
|
|
|
|
* every connection id's end is observable from the host that owns it. */
|
2026-09-16 21:43:01 +02:00
|
|
|
CULTMESH_QUIC_EVENT_CONNECTION_SHUTDOWN = 4,
|
|
|
|
|
/* An inbound stream's first byte named its kind. `stream_id`, `stream_kind`. */
|
|
|
|
|
CULTMESH_QUIC_EVENT_STREAM_STARTED = 5,
|
|
|
|
|
/* Payload: one whole encoded frame, the 4-byte length prefix stripped. */
|
|
|
|
|
CULTMESH_QUIC_EVENT_STREAM_FRAME = 6,
|
|
|
|
|
/* One per `cultmesh_quic_stream_send_frame` call that returned 0, and never
|
|
|
|
|
* for the bridge's own kind byte. `code` is a cultmesh_quic_send_result. */
|
|
|
|
|
CULTMESH_QUIC_EVENT_STREAM_SEND_COMPLETE = 7,
|
|
|
|
|
/* The stream is gone. `stream_id`, `stream_kind`. */
|
|
|
|
|
CULTMESH_QUIC_EVENT_STREAM_SHUTDOWN = 8,
|
|
|
|
|
/* The listener stopped. `listener_id`. */
|
|
|
|
|
CULTMESH_QUIC_EVENT_LISTENER_STOPPED = 9
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* The `code` of event 7. A canceled send is a send MsQuic released without
|
|
|
|
|
* putting on the wire — the stream was aborted or the connection ended under it.
|
|
|
|
|
* It is reported, not silently dropped. */
|
|
|
|
|
enum cultmesh_quic_send_result {
|
|
|
|
|
CULTMESH_QUIC_SEND_COMPLETED = 0,
|
|
|
|
|
CULTMESH_QUIC_SEND_CANCELED = 1
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* The first byte of every CultMesh QUIC stream. */
|
|
|
|
|
enum cultmesh_quic_stream_kind {
|
|
|
|
|
CULTMESH_QUIC_STREAM_RELIABLE = 1,
|
|
|
|
|
CULTMESH_QUIC_STREAM_LATEST_ONLY = 2
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* 2. Ids
|
|
|
|
|
*
|
|
|
|
|
* Listeners, connections and streams are named by `uint64_t` ids drawn from one
|
|
|
|
|
* per-runtime counter that starts at 1 and never repeats. 0 is never a valid id
|
|
|
|
|
* and is what an out parameter holds when a call fails. An id is the only thing
|
|
|
|
|
* a host ever holds: there is no host-visible pointer to a connection or stream,
|
|
|
|
|
* so a host cannot outlive an object it is naming. An id whose object is gone is
|
|
|
|
|
* a bad call (-1), not undefined behaviour.
|
|
|
|
|
* --------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* 3. Return codes — negative on a bad call, on every platform
|
|
|
|
|
*
|
|
|
|
|
* Every int32_t-returning export returns:
|
|
|
|
|
*
|
|
|
|
|
* 0 the call succeeded
|
|
|
|
|
* 1, 2 `cultmesh_quic_next_event` only; see its comment
|
|
|
|
|
* -1 a bad call: a null handle, a null out parameter,
|
|
|
|
|
* or an id whose object is gone
|
|
|
|
|
* -2 an argument the bridge rejects on its own terms:
|
|
|
|
|
* an unknown stream kind, an oversized frame, a
|
|
|
|
|
* listener host that is not an IP literal
|
|
|
|
|
* <= -1000 MsQuic refused. The bridge code is
|
|
|
|
|
* -(1000 + (raw_status & 0xffff)); the raw
|
|
|
|
|
* platform status is available from
|
|
|
|
|
* `cultmesh_quic_last_status` and a sentence about
|
|
|
|
|
* it from `cultmesh_quic_last_error`.
|
|
|
|
|
*
|
|
|
|
|
* This mapping exists because `QUIC_STATUS` itself is not portable: it is a
|
|
|
|
|
* negative HRESULT on Windows and a positive errno on POSIX, so a host that
|
2026-09-16 23:21:50 +02:00
|
|
|
* checked for a negative value saw every Linux failure as success.
|
|
|
|
|
*
|
|
|
|
|
* What the mapping makes portable is the sign, and only the sign. A failure is
|
|
|
|
|
* negative everywhere, and that is what a host may branch on. The magnitude of a
|
|
|
|
|
* <= -1000 code is diagnostic and platform-specific: the same MsQuic failure
|
|
|
|
|
* carries different numbers on different platforms (a TLS error is -12032 on
|
|
|
|
|
* win32-x64 and -1126 on linux-x64; an ALPN already in use is -1009 against
|
|
|
|
|
* -1091), and the 16-bit mask can give two distinct statuses the same code
|
|
|
|
|
* (`QUIC_STATUS_USER_CANCELED` and `QUIC_STATUS_FILE_NOT_FOUND` are both -1002
|
|
|
|
|
* on Windows). Switching on a particular code is therefore not portable and not
|
|
|
|
|
* a reliable identification of the failure. Report it, log it, and read
|
|
|
|
|
* `cultmesh_quic_last_status` and `cultmesh_quic_last_error` beside it.
|
2026-09-16 21:43:01 +02:00
|
|
|
* --------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
#define CULTMESH_QUIC_RESULT_BAD_CALL (-1)
|
|
|
|
|
#define CULTMESH_QUIC_RESULT_BAD_ARGUMENT (-2)
|
|
|
|
|
#define CULTMESH_QUIC_RESULT_MSQUIC_BASE (-1000)
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* 4. Object lifetime — resolved from an id, kept alive across the call
|
|
|
|
|
*
|
|
|
|
|
* The bridge's objects are shared-owned. The runtime's maps hold one reference
|
|
|
|
|
* each; an export that names an id takes its own reference under the lock and
|
|
|
|
|
* holds it for the whole MsQuic call, so an object cannot be freed under a host
|
|
|
|
|
* thread that is using it. MsQuic's worker, on shutdown-complete, removes the
|
|
|
|
|
* map entry and drops the map's reference only — it never frees an object a host
|
|
|
|
|
* call is inside.
|
|
|
|
|
*
|
|
|
|
|
* The MsQuic handle follows the object: it is closed exactly once, by whichever
|
|
|
|
|
* holder drops the last reference (or by `cultmesh_quic_runtime_close`), under a
|
|
|
|
|
* one-shot exchange guard. A handle is therefore valid for as long as any holder
|
|
|
|
|
* has a reference to its object, which is the property the host thread needs. A
|
|
|
|
|
* stream holds a reference to its connection, so stream handles are always
|
|
|
|
|
* closed before the connection handle they belong to.
|
|
|
|
|
*
|
|
|
|
|
* `cultmesh_quic_runtime_close` quiesces before it tears anything down. It marks
|
|
|
|
|
* the runtime closing, wakes every blocked `cultmesh_quic_next_event`, and waits
|
|
|
|
|
* for every in-flight export to return before it closes a handle or frees a
|
2026-09-17 00:25:59 +02:00
|
|
|
* byte. No host call may begin after `cultmesh_quic_runtime_close` starts. That
|
|
|
|
|
* ordering is the host's, and it is a prohibition rather than a race the bridge
|
|
|
|
|
* arbitrates: once the close returns, the handle names freed memory, and a call
|
|
|
|
|
* made with it has nothing left to refuse it. A call that races the start of the
|
|
|
|
|
* close, while it is still running, is refused with -1 and touches nothing; that
|
|
|
|
|
* narrows one mis-ordering and does not make the prohibition optional.
|
2026-09-16 21:43:01 +02:00
|
|
|
* --------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* 5. Threading
|
|
|
|
|
*
|
|
|
|
|
* There are no host callbacks. Every crossing is the host calling in.
|
|
|
|
|
*
|
|
|
|
|
* - MsQuic's worker threads run inside this library only. They push events onto
|
|
|
|
|
* the runtime's queue and wake the one blocking wait. No host code is ever
|
|
|
|
|
* called from a thread the host does not own.
|
|
|
|
|
* - `cultmesh_quic_next_event` is called from host threads, one or many. It is
|
|
|
|
|
* the only blocking export. Concurrent callers each get a distinct event.
|
|
|
|
|
* - Every other export may be called from any host thread at any time, except
|
|
|
|
|
* during or after `cultmesh_quic_runtime_close`.
|
|
|
|
|
* - `cultmesh_quic_runtime_close` is called once, from one thread, and
|
|
|
|
|
* quiesces as described above.
|
|
|
|
|
* --------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* 6. The exports
|
|
|
|
|
* --------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
/* Opens a runtime: one MsQuic registration, one event queue. `app_name` may be
|
|
|
|
|
* null. `*out_runtime` is the opaque handle every other export takes. */
|
|
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_runtime_open(
|
|
|
|
|
const char* app_name, void** out_runtime);
|
|
|
|
|
|
|
|
|
|
/* Quiesces, tears down and frees the runtime. Safe to call with null. After it
|
|
|
|
|
* returns, the handle is dead; see section 4 for the ordering rule. */
|
|
|
|
|
CULTMESH_QUIC_API void cultmesh_quic_runtime_close(void* runtime);
|
|
|
|
|
|
|
|
|
|
/* Opens a provider listener on `host`:`port` with a PKCS12 credential. `host`
|
|
|
|
|
* may be null or empty for any address, otherwise it must be an IP literal.
|
|
|
|
|
* `port` 0 binds an ephemeral port, which `*out_bound_port` reports. A failure
|
|
|
|
|
* leaves no listener behind and no id allocated to the host. */
|
|
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_listener_open(
|
|
|
|
|
void* runtime, const char* host, uint16_t port,
|
|
|
|
|
const uint8_t* pkcs12, int32_t pkcs12_length, const char* password,
|
|
|
|
|
uint64_t* out_listener_id, uint16_t* out_bound_port);
|
|
|
|
|
|
|
|
|
|
/* Stops and closes a listener. Its accepted connections are not affected.
|
|
|
|
|
* Emits event 9. Unknown ids are ignored. */
|
|
|
|
|
CULTMESH_QUIC_API void cultmesh_quic_listener_close(
|
|
|
|
|
void* runtime, uint64_t listener_id);
|
|
|
|
|
|
|
|
|
|
/* Starts an outbound connection. Certificate validation is deferred: the host
|
|
|
|
|
* gets event 3 and must answer. */
|
|
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_connection_open(
|
|
|
|
|
void* runtime, const char* host, uint16_t port, uint64_t* out_connection_id);
|
|
|
|
|
|
|
|
|
|
/* Answers event 3. `accept` non-zero continues the handshake; zero fails it with
|
|
|
|
|
* a bad-certificate alert. */
|
|
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_connection_certificate_complete(
|
|
|
|
|
void* runtime, uint64_t connection_id, int32_t accept);
|
|
|
|
|
|
|
|
|
|
/* Ends a connection with a QUIC application error code. The initiator gets its
|
|
|
|
|
* own event 4 carrying `code`, as the peer does. Unknown ids are ignored. */
|
|
|
|
|
CULTMESH_QUIC_API void cultmesh_quic_connection_shutdown(
|
|
|
|
|
void* runtime, uint64_t connection_id, uint64_t code);
|
|
|
|
|
|
|
|
|
|
/* Opens an outbound unidirectional stream and puts `kind` on the wire as its
|
|
|
|
|
* first byte. That byte is the bridge's own and never produces an event 7. */
|
|
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_stream_open(
|
|
|
|
|
void* runtime, uint64_t connection_id, uint8_t kind, uint64_t* out_stream_id);
|
|
|
|
|
|
|
|
|
|
/* Queues one whole encoded frame, length-prefixed by the bridge. A call that
|
|
|
|
|
* returns 0 produces exactly one event 7 later, completed or canceled. */
|
|
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_stream_send_frame(
|
|
|
|
|
void* runtime, uint64_t stream_id,
|
|
|
|
|
const uint8_t* encoded_frame, int32_t length, int32_t fin);
|
|
|
|
|
|
|
|
|
|
/* Aborts a stream with a QUIC application error code. Unknown ids are ignored. */
|
|
|
|
|
CULTMESH_QUIC_API void cultmesh_quic_stream_shutdown(
|
|
|
|
|
void* runtime, uint64_t stream_id, uint64_t code);
|
|
|
|
|
|
|
|
|
|
/* The one blocking crossing.
|
|
|
|
|
*
|
Close the three ABI gaps the bridge left open
Event 7 was not one per frame. The bridge writes the stream's kind byte
itself, that send completed as an anonymous event 7, and a host counting
completions saw six for five frames with nothing to tell them apart. The
send request now says which send it was; the kind byte's completion is the
bridge's own business and is dropped. A send MsQuic released without
putting on the wire reports it: event 7 carries a send result in `code`,
completed or canceled, so an aborted send is accounted for rather than
silently missing. Measured: 32 sends queued under an abort, 32 event 7, 28
canceled and 4 completed.
The initiator of a shutdown learned nothing. MsQuic gives it only
shutdown-complete, and the bridge swallowed that, so a host could end its
own connection and never see the id end. Event 4 now has one emitter —
shutdown-complete — and the two initiated-by callbacks record why rather
than announcing it. Each connection id gets exactly one event 4, whoever
ended it, with the code its side passed, and it is emitted after the id
has stopped resolving, so a host told a connection is gone cannot still
resolve it on the next line.
Return codes inverted by platform. QUIC_STATUS is a negative HRESULT on
Windows and a positive errno on POSIX, so a Linux host checking for a
negative code saw every failure as success, against a README that promised
negative. A MsQuic refusal is now -(1000 + (status & 0xffff)) on both, and
cultmesh_quic_last_status hands over the raw bits for diagnostics. On
win32-x64 a garbage PKCS12 returns -12032 with last_status 0x80072B18.
The v1 exports move with it; the managed connector tests the value against
zero and formats it, so the contract it was shipped with holds.
Smaller ones, from the same pass: AppendTrace is v1-only and bounded,
because a long-lived v2 connection grew that string once per MsQuic event
for nothing to read; a listener configuration failure takes the connection
out of the map instead of leaving it naming a handle MsQuic is about to
close; and next_event refuses a null payload with a non-zero capacity
rather than treating the lie as a discard.
Soul's C consumer now compiles against the header and asserts its own
struct and its eleven bound prototypes against it, and the koffi smoke
checks its declarations and the thirteen exports the same way: 20 of 20,
seq clean, cancel clean, the v1 gate 3 passed 1 skipped as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:00:06 +02:00
|
|
|
* 0 nothing within `timeout_ms`. A poller already blocked here when
|
2026-09-17 00:25:59 +02:00
|
|
|
* `cultmesh_quic_runtime_close` starts is woken and returns 0. A poll that
|
|
|
|
|
* begins after the close starts is forbidden by section 4, and is not a
|
|
|
|
|
* case this return code covers.
|
2026-09-16 21:43:01 +02:00
|
|
|
* 1 `*out_event` filled and up to `payload_capacity` payload bytes copied;
|
|
|
|
|
* `*out_required` is the payload size
|
|
|
|
|
* 2 `payload_capacity` is too small. Nothing is consumed and nothing is
|
|
|
|
|
* written to `*out_event`; `*out_required` is the size to allocate. Ask
|
|
|
|
|
* again with a big enough buffer and the same event is delivered.
|
|
|
|
|
* -1 a bad call, including a null `payload` with a non-zero
|
|
|
|
|
* `payload_capacity`, which is a lie about the buffer and is refused
|
|
|
|
|
* rather than treated as a discard.
|
|
|
|
|
*
|
|
|
|
|
* `payload` may be null when `payload_capacity` is 0: that is the two-phase
|
|
|
|
|
* poll's first ask. */
|
|
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_next_event(
|
|
|
|
|
void* runtime, int32_t timeout_ms, struct cultmesh_quic_event* out_event,
|
|
|
|
|
uint8_t* payload, int32_t payload_capacity, int32_t* out_required);
|
|
|
|
|
|
|
|
|
|
/* The last error sentence, NUL-terminated, truncated to `capacity`. Returns the
|
|
|
|
|
* number of bytes written, or -1 on a bad call. */
|
|
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_last_error(
|
|
|
|
|
void* runtime, char* destination, int32_t capacity);
|
|
|
|
|
|
|
|
|
|
/* The raw platform `QUIC_STATUS` behind the last <= -1000 return, as its own
|
|
|
|
|
* bits: a negative HRESULT on Windows, a positive errno on POSIX. Diagnostics
|
2026-09-16 23:21:50 +02:00
|
|
|
* only; what is portable about the return code is its sign, not its value. 0
|
|
|
|
|
* when nothing has failed. */
|
2026-09-16 21:43:01 +02:00
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_last_status(void* runtime);
|
|
|
|
|
|
Hold host calls inside the bridge so the quiesce is tested, not assumed
The closerace scenario pinned the order of the count and the notify but never
that a host call is counted at all. Deleting the call scope from
`cultmesh_quic_next_event` outright leaves every configuration green — the
pollers finish before the teardown reaches them — and so does inverting the
comparison that makes a positive timeout block, which empties the library before
the close even begins. A scenario that only sleeps and hopes cannot tell any of
that apart from the real thing.
So the bridge says it itself, in development builds only (section 7 of the
header, behind CULTMESH_QUIC_DEBUG_ASSERTS, absent from every shipped binary):
the peak number of host calls counted inside the library, the number counted
when the close began its wait, and a hold that parks a call inside the library
until the scenario releases it. A held call returns the 0 an empty queue would
have and touches the runtime no further, so a bridge that frees the runtime
under it is reported by the scenario rather than crashing inside it.
`holdclose` drives the quiesce through that seam: the calls are counted, they
were still counted when the close began, and the close did not return while they
were held. `closerace` keeps hunting the race and now also refuses to pass when
a poller left before the close began, which is a scenario that raced nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:23:04 +02:00
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* 7. The development seam
|
|
|
|
|
*
|
|
|
|
|
* Present only in a library configured with `CULTMESH_QUIC_DEBUG_ASSERTS`, which
|
|
|
|
|
* a shipped build never is. It exists for the runtime-lifetime scenarios: the
|
Land the QUIC Cut 3 eighth fix batch: release config, whole-duration seam, pollhammer live objects, zero-timeout bound, closerace starvation fix
Self's ruling of 2026-09-22 named six findings against Soul's eighth pass.
This lands the code side of all six:
- The wait-record seam (CULTMESH_QUIC_DEBUG_RECORD_WAIT) now wraps the whole
duration handed to `wait_for`, not the raw `timeout_ms` before it is
converted to milliseconds, so arithmetic applied after that conversion can
no longer live outside what the seam records. Its "unset" sentinel moves to
a separate `debug_wait_recorded` flag so a recorded -1 could never be
confused with nothing recorded.
- `pollhammer` also runs against a runtime holding a live connection to an
unroutable address (RFC 5737 TEST-NET-1), a best-effort stream on it, and a
real (never-connected) listener, closing the gap where the wait's predicate
was only ever exercised on an empty runtime.
- A new `zerotimeout` scenario pins that `timeout_ms <= 0` returns within
50 ms, probing 0, -1 and INT32_MIN.
- `closerace`'s settle no longer sleeps a guessed duration: where the
development seam exists it waits for `debug_peak_calls` to reach the poller
count (reset before any poller thread exists, so nothing can zero a count a
poller already raised) before a short fixed margin closes the last gap
between being counted and being inside the condition wait.
- `kGenerousLateToleranceMs` narrows from 800 ms to 400 ms so the wall-clock
scenarios can catch arithmetic applied outside the seam's own argument,
which `waitseam`'s equality check structurally cannot see.
scripts/mutate-cultmesh.mjs gains a plain `release` configuration on both
platforms (CULTMESH_QUIC_DEBUG_ASSERTS=OFF, CMAKE_BUILD_TYPE=Release) running
`polltimeout`, `pollbusy`, `pollhammer` and `zerotimeout`, and seven new
mutation entries: R1 (the release macro divides the duration by 10), W1-W3
(600 ms added, a 500 ms floor, and a 1000/909 scale, each applied outside the
seam's own argument), P1 (the idle predicate also ends on a live connection),
and Z1-Z2 (a 500 ms sleep and a 300 ms spin on a non-positive timeout). Two
stale comments are corrected: the scenario list above `assertScenarios` now
names `pollhammer` among the clock-sensitive entries, and a historical "1300"
probe value that no longer matches the test file is removed. The header's
section 7 intro now says the seam serves both the quiesce and the wait
recording, not quiesce alone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 12:27:56 +02:00
|
|
|
* quiesce is a promise about host calls that are inside the library, and the
|
|
|
|
|
* wait scenarios need the same kind of promise about the exact duration the
|
|
|
|
|
* bridge hands its own condition wait. A scenario that can only sleep and hope
|
|
|
|
|
* cannot tell a bridge that counts and waits correctly from one that does not.
|
Hold host calls inside the bridge so the quiesce is tested, not assumed
The closerace scenario pinned the order of the count and the notify but never
that a host call is counted at all. Deleting the call scope from
`cultmesh_quic_next_event` outright leaves every configuration green — the
pollers finish before the teardown reaches them — and so does inverting the
comparison that makes a positive timeout block, which empties the library before
the close even begins. A scenario that only sleeps and hopes cannot tell any of
that apart from the real thing.
So the bridge says it itself, in development builds only (section 7 of the
header, behind CULTMESH_QUIC_DEBUG_ASSERTS, absent from every shipped binary):
the peak number of host calls counted inside the library, the number counted
when the close began its wait, and a hold that parks a call inside the library
until the scenario releases it. A held call returns the 0 an empty queue would
have and touches the runtime no further, so a bridge that frees the runtime
under it is reported by the scenario rather than crashing inside it.
`holdclose` drives the quiesce through that seam: the calls are counted, they
were still counted when the close began, and the close did not return while they
were held. `closerace` keeps hunting the race and now also refuses to pass when
a poller left before the close began, which is a scenario that raced nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:23:04 +02:00
|
|
|
*
|
|
|
|
|
* It is process-wide, not per-runtime, because the numbers outlive the runtime
|
|
|
|
|
* they describe: `cultmesh_quic_runtime_close` frees the runtime, and the count
|
|
|
|
|
* it waited on is read afterwards. The scenarios open one runtime at a time.
|
|
|
|
|
* --------------------------------------------------------------------------- */
|
|
|
|
|
#if defined(CULTMESH_QUIC_DEBUG_ASSERTS)
|
|
|
|
|
|
|
|
|
|
/* Non-zero arms the hold and resets both counters: every call that reaches the
|
|
|
|
|
* blocking wait in `cultmesh_quic_next_event` parks inside the library instead
|
|
|
|
|
* of returning. Zero releases every held call and disarms it. A held call
|
|
|
|
|
* returns 0 and touches the runtime no further. */
|
|
|
|
|
CULTMESH_QUIC_API void cultmesh_quic_debug_hold_calls(int32_t hold);
|
|
|
|
|
|
|
|
|
|
/* The high-water mark of host calls counted inside the library since the hold
|
|
|
|
|
* was armed. Zero means no call was ever counted. */
|
|
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_debug_peak_calls(void);
|
|
|
|
|
|
|
|
|
|
/* How many host calls were counted inside the library when the last
|
|
|
|
|
* `cultmesh_quic_runtime_close` began its wait, or -1 if no close has begun one
|
|
|
|
|
* since the hold was armed. This is the number the wait exists for. */
|
|
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_debug_calls_at_close(void);
|
|
|
|
|
|
Land the QUIC Cut 3 eighth fix batch: release config, whole-duration seam, pollhammer live objects, zero-timeout bound, closerace starvation fix
Self's ruling of 2026-09-22 named six findings against Soul's eighth pass.
This lands the code side of all six:
- The wait-record seam (CULTMESH_QUIC_DEBUG_RECORD_WAIT) now wraps the whole
duration handed to `wait_for`, not the raw `timeout_ms` before it is
converted to milliseconds, so arithmetic applied after that conversion can
no longer live outside what the seam records. Its "unset" sentinel moves to
a separate `debug_wait_recorded` flag so a recorded -1 could never be
confused with nothing recorded.
- `pollhammer` also runs against a runtime holding a live connection to an
unroutable address (RFC 5737 TEST-NET-1), a best-effort stream on it, and a
real (never-connected) listener, closing the gap where the wait's predicate
was only ever exercised on an empty runtime.
- A new `zerotimeout` scenario pins that `timeout_ms <= 0` returns within
50 ms, probing 0, -1 and INT32_MIN.
- `closerace`'s settle no longer sleeps a guessed duration: where the
development seam exists it waits for `debug_peak_calls` to reach the poller
count (reset before any poller thread exists, so nothing can zero a count a
poller already raised) before a short fixed margin closes the last gap
between being counted and being inside the condition wait.
- `kGenerousLateToleranceMs` narrows from 800 ms to 400 ms so the wall-clock
scenarios can catch arithmetic applied outside the seam's own argument,
which `waitseam`'s equality check structurally cannot see.
scripts/mutate-cultmesh.mjs gains a plain `release` configuration on both
platforms (CULTMESH_QUIC_DEBUG_ASSERTS=OFF, CMAKE_BUILD_TYPE=Release) running
`polltimeout`, `pollbusy`, `pollhammer` and `zerotimeout`, and seven new
mutation entries: R1 (the release macro divides the duration by 10), W1-W3
(600 ms added, a 500 ms floor, and a 1000/909 scale, each applied outside the
seam's own argument), P1 (the idle predicate also ends on a live connection),
and Z1-Z2 (a 500 ms sleep and a 300 ms spin on a non-positive timeout). Two
stale comments are corrected: the scenario list above `assertScenarios` now
names `pollhammer` among the clock-sensitive entries, and a historical "1300"
probe value that no longer matches the test file is removed. The header's
section 7 intro now says the seam serves both the quiesce and the wait
recording, not quiesce alone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 12:27:56 +02:00
|
|
|
/* The whole duration, in milliseconds, most recently handed to the condition
|
|
|
|
|
* wait inside `cultmesh_quic_next_event`, recorded at the point the wait is
|
|
|
|
|
* entered rather than observed from outside. A poll whose queue is not empty,
|
|
|
|
|
* or whose `timeout_ms` is not greater than zero, never enters that wait and
|
|
|
|
|
* leaves this unchanged. -1 until the first such wait, or after
|
|
|
|
|
* `cultmesh_quic_debug_reset_last_wait_ms` — tracked by a separate internal
|
|
|
|
|
* flag, not by -1 being stored here, so a wait that ever recorded exactly -1
|
|
|
|
|
* itself could not be confused with nothing having been recorded.
|
Record the wait, not its shadow: a dev-only seam for cultmesh_quic_next_event
Self's ruling of 2026-09-22 called it after seven Soul passes: a wall-clock
probe against cultmesh_quic_next_event's timeout can never tell the host's
argument from a clamp, floor, round, scale or later-poll fault that happens to
be the identity at the probe's own value, because there is always another
function that matches. Move the observation to where the rule is decided.
Add cultmesh_quic_debug_last_wait_ms / _reset_last_wait_ms, built exactly like
the existing hold seam: gated behind CULTMESH_QUIC_DEBUG_ASSERTS, absent from a
shipped build. CULTMESH_QUIC_DEBUG_RECORD_WAIT wraps the literal timeout_ms
handed to the condition wait as a pass-through, so what is recorded and what is
actually waited on are the same evaluation and cannot diverge except by
changing what the macro wraps.
Two scenarios:
- waitseam asserts the recorded wait equals the host's own argument, exactly,
over 0, 1, 15, 16, 39, 40, 999, 1000, 1001, 7300 and INT32_MAX (the header
states no narrower maximum), and over 40 polls on one runtime past the point
every prior scenario stopped asking. timeout_ms <= 0 never reaches the wait
at all, so its probe checks the honest converse — the seam stays untouched —
which is what catches a seam that records the raw argument on every call
instead of only the value it actually hands its wait.
- pollhammer runs one thread hammering every gate-touching entry point that
cannot itself queue an event, in a yield loop, against another thread's idle
poll, once on a clean runtime and once after a refused listener_open has
left runtime->error non-empty. The idle poll must still stay for its
timeout.
polltimeout, pollbusy and holdtimeout stay as generous-margin proof that the
recorded wait is really waited, not as the precision instrument they used to
have to be: the fastest-of-ten mechanism, the 15 ms probe's tight tolerance,
and the stated-limits table they rested on are gone, because waitseam now
kills every clamp, floor, round, scale, offset and later-poll mutant by
equality regardless of timing. The Windows priority raise these three
scenarios carried is gone with them: a flat 800 ms late tolerance already
covers the worst overshoot the old table recorded (127 ms at normal priority
under load), so raising the process above the load bought nothing the new
margin does not already have. holdtimeout's long probe moves to 1600 ms, above
the stated 1500 ms ceiling on the hold guard, with the hold kept armed to
3000 ms so a guard that parks a naturally-timed-out call anyway overshoots by
well more than the margin.
Verified locally on win32-x64: waitseam and pollhammer pass against the
unmutated bridge; a seam-loosening mutant (recording the raw argument outside
the wait's own branch) fails waitseam's 0 ms probe; an inline +25 ms offset on
the wait's argument fails waitseam by exact mismatch; the S5 hold-guard mutant
(timeout_ms > 1500) fails holdtimeout by 1.4 s overshoot; K1 (++wakes > 64), K2
(!error.empty()) and a no-predicate revert all fail pollhammer's idle poll
within tens of milliseconds. All were hand-reverted and diffed clean against
the working tree before this commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:33:19 +02:00
|
|
|
*
|
|
|
|
|
* This exists because a wall-clock probe cannot tell the host's timeout from
|
|
|
|
|
* any function of it that is the identity at the probe's own value — a floor,
|
|
|
|
|
* a round, a clamp, a later-poll fault, there is always another one that
|
|
|
|
|
* matches. This reads what the bridge actually handed its wait instead of
|
|
|
|
|
* inferring it from how long a call took. */
|
|
|
|
|
CULTMESH_QUIC_API int32_t cultmesh_quic_debug_last_wait_ms(void);
|
|
|
|
|
|
|
|
|
|
/* Resets the value `cultmesh_quic_debug_last_wait_ms` reports to -1, without
|
|
|
|
|
* touching the hold or the two call counters above. */
|
|
|
|
|
CULTMESH_QUIC_API void cultmesh_quic_debug_reset_last_wait_ms(void);
|
|
|
|
|
|
Hold host calls inside the bridge so the quiesce is tested, not assumed
The closerace scenario pinned the order of the count and the notify but never
that a host call is counted at all. Deleting the call scope from
`cultmesh_quic_next_event` outright leaves every configuration green — the
pollers finish before the teardown reaches them — and so does inverting the
comparison that makes a positive timeout block, which empties the library before
the close even begins. A scenario that only sleeps and hopes cannot tell any of
that apart from the real thing.
So the bridge says it itself, in development builds only (section 7 of the
header, behind CULTMESH_QUIC_DEBUG_ASSERTS, absent from every shipped binary):
the peak number of host calls counted inside the library, the number counted
when the close began its wait, and a hold that parks a call inside the library
until the scenario releases it. A held call returns the 0 an empty queue would
have and touches the runtime no further, so a bridge that frees the runtime
under it is reported by the scenario rather than crashing inside it.
`holdclose` drives the quiesce through that seam: the calls are counted, they
were still counted when the close began, and the close did not return while they
were held. `closerace` keeps hunting the race and now also refuses to pass when
a poller left before the close began, which is a scenario that raced nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:23:04 +02:00
|
|
|
#endif /* CULTMESH_QUIC_DEBUG_ASSERTS */
|
|
|
|
|
|
2026-09-16 21:43:01 +02:00
|
|
|
#if defined(__cplusplus)
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#endif /* CULTMESH_QUIC_NATIVE_H */
|