Skip to content

External Frame API

Migo normally runs JavaScript in its own script runtime and renders directly. The external-frame API serves a different deployment model: content scripts run in a separate process, such as the WebKit WebContent process on iOS, which runs outside the host process because of security and JIT restrictions. In this model, the content side encodes one frame’s GPU drawing work as a bounded binary packet and sends it through a custom transport layer to the host process. The host then injects it into the Migo session with migo_session_submit_external_frame; Migo handles rendering and composition without running a JavaScript engine.

Note: This mode is available only in the Performance+ product. Other products do not export the entry points listed on this page. Linking the wrong product produces an unresolved-symbol error at build time; this is an intentional fail-fast design, not a runtime stub that returns failure.

The interface has three functional lanes:

Lane Purpose
Frame ingress lane Submit cross-process frame packets to a session and obtain acceptance/rejection decisions and backpressure signals
Sync barrier lane Provide a request/reply mailbox for calls such as readPixels, getImageData, and toDataURL that must block while waiting for a result
Resource lane Reserve and upload large assets such as texture atlases in chunks outside frame packets; a frame may reference them only after validation succeeds
Service stream Everything content asks the host for that is not drawing – read a file, write a save, load an image, play a sound – admitted strictly in order and answered on a return stream of its own
Content origin The host serves a game’s scripts through the engine’s own module-loading rules, so the source WebKit evaluates is the source the embedded runtime evaluates

In external-frame mode, when creating a session the host must put a 128-bit random key in MigoSessionConfig.launch_nonce. The transport layer carries the same nonce in every frame packet envelope, and Migo uses it to verify that a packet belongs to the correct session.

launch_nonce must be generated by the host (for example, with SecRandomCopyBytes on iOS), not by the Migo library. The host owns both ends of the transport layer and session and is the only party that can safely protect this shared secret. An all-zero value means “no external producer” and causes every frame packet to fail verification — this is also the default behavior when an older host zero-initializes the struct. The nonce must not appear in logs, URLs, or query strings.

When creating an external-frame session with MigoExternalSessionDescriptor, the launch_nonce[16] field is also present in that descriptor and has exactly the same meaning as MigoSessionConfig.launch_nonce.


typedef uint32_t MigoFrameIngressDecision;
#define MIGO_FRAME_INGRESS_ACCEPTED 1U
#define MIGO_FRAME_INGRESS_WOULD_BLOCK 2U
#define MIGO_FRAME_INGRESS_REJECTED 3U
#define MIGO_FRAME_INGRESS_GENERATION_LOST 4U

After migo_session_submit_external_frame succeeds, MigoFrameIngressOutcome.decision holds this type and describes how the packet was handled.

Value Meaning
MIGO_FRAME_INGRESS_ACCEPTED The packet was accepted and consumes one credit until the renderer reports completion and returns it.
MIGO_FRAME_INGRESS_WOULD_BLOCK The packet is well-formed but credits are exhausted; the producer must wait. Do not drop the packet because it may contain state changes needed by later frames.
MIGO_FRAME_INGRESS_REJECTED The packet is malformed or targets the wrong session; it consumes no credit, and the producer must not resend the same bytes.
MIGO_FRAME_INGRESS_GENERATION_LOST The bytes are valid, but the corresponding runtime generation no longer exists (the WebContent process was replaced or the session reloaded); this is not an error, and retrying is ineffective.
typedef struct MigoFrameIngressOutcome {
uint32_t struct_size;
uint32_t abi_version;
uint64_t accepted_sequence;
MigoFrameIngressDecision decision;
uint32_t remaining_credits;
uint32_t wire_error_code;
uint32_t reserved0;
} MigoFrameIngressOutcome;

Written by the library, append-only: the caller must fill struct_size and abi_version before every call or the call returns MIGO_ERROR_INVALID_ARGUMENT. Initialize this structure once and reuse it rather than rebuilding it for every frame.

Field Type Meaning
struct_size uint32_t Caller input. Set to sizeof(MigoFrameIngressOutcome) before the call to bound the library’s writes.
abi_version uint32_t Caller input. Set to MIGO_ABI_VERSION_CURRENT.
accepted_sequence uint64_t Non-zero only for ACCEPTED; monotonically increasing sequence assigned to this packet.
decision MigoFrameIngressDecision Packet disposition; see the table above.
remaining_credits uint32_t Current remaining credits; the producer can use it to decide whether to continue sending.
wire_error_code uint32_t Non-zero only for REJECTED; stable error number for telemetry to distinguish cases such as “packet too short” and “wrong target session” (1–1000 are envelope errors; 1001+ are identity/order errors).
reserved0 uint32_t Reserved; ignore.

typedef uint32_t MigoSyncState;
#define MIGO_SYNC_STATE_FREE 0U
#define MIGO_SYNC_STATE_PENDING 1U
#define MIGO_SYNC_STATE_READY 2U
#define MIGO_SYNC_STATE_FAILED 3U
#define MIGO_SYNC_STATE_CANCELLED 4U

Only one sync request may be in flight for a session at a time (the producer is a single agent and cannot start a second request while blocked waiting).

Value Meaning
MIGO_SYNC_STATE_FREE The mailbox is idle; a new request may be posted only in this state.
MIGO_SYNC_STATE_PENDING Posted; the producer is blocked waiting.
MIGO_SYNC_STATE_READY Replied; reply_bytes gives the reply length.
MIGO_SYNC_STATE_FAILED No reply will arrive; error explains why.
MIGO_SYNC_STATE_CANCELLED The producer withdrew the request before receiving a reply.
typedef uint32_t MigoSyncError;
#define MIGO_SYNC_ERROR_ALREADY_PENDING 1U
#define MIGO_SYNC_ERROR_REQUEST_ID_MISMATCH 2U
#define MIGO_SYNC_ERROR_STALE_GENERATION 3U
#define MIGO_SYNC_ERROR_REPLY_TOO_LARGE 4U
#define MIGO_SYNC_ERROR_TIMED_OUT 5U
#define MIGO_SYNC_ERROR_SESSION_ENDED 6U
#define MIGO_SYNC_ERROR_UNSUPPORTED_OPERATION 7U
#define MIGO_SYNC_ERROR_LATE_REPLY 8U
#define MIGO_SYNC_ERROR_BAD_DEADLINE 9U
#define MIGO_SYNC_ERROR_BAD_REPLY_RESERVATION 10U

These error codes remain stable across versions, so the producer can map them to corresponding exception types.

Value Meaning
ALREADY_PENDING A request is already in flight; requests cannot overlap.
REQUEST_ID_MISMATCH The reply’s request ID does not match the in-flight request.
STALE_GENERATION The runtime generation changed; this request is no longer valid.
REPLY_TOO_LARGE The reply exceeds the space reserved by max_reply_bytes.
TIMED_OUT No reply arrived before deadline_nanos.
SESSION_ENDED The Session was destroyed before the reply arrived.
UNSUPPORTED_OPERATION The operation field is not supported by the current build.
LATE_REPLY The reply arrived after the deadline.
BAD_DEADLINE deadline_nanos is zero or unreasonable.
BAD_REPLY_RESERVATION max_reply_bytes is zero or outside the permitted range.
typedef struct MigoSyncRequestDescriptor {
uint32_t struct_size;
uint32_t abi_version;
uint64_t runtime_generation;
uint64_t surface_generation;
uint64_t resource_epoch;
uint64_t triggering_sequence;
uint64_t deadline_nanos;
uint32_t operation;
uint32_t max_reply_bytes;
} MigoSyncRequestDescriptor;

Written by the caller. 64-bit fields precede 32-bit fields so the layout is identical under LP64 and ILP32, with no internal padding (56 bytes).

Field Type Meaning
struct_size uint32_t Set to sizeof(MigoSyncRequestDescriptor).
abi_version uint32_t Set to MIGO_ABI_VERSION_CURRENT.
runtime_generation uint64_t Runtime-generation identifier at the producer.
surface_generation uint64_t Surface-generation identifier referenced by the producer.
resource_epoch uint64_t Resource epoch referenced by the producer.
triggering_sequence uint64_t Last frame sequence submitted before the producer blocked.
deadline_nanos uint64_t Monotonic-clock reading (not wall-clock time); the library returns TIMED_OUT after it expires.
operation uint32_t Type of synchronous operation to execute (defined by the protocol layer).
max_reply_bytes uint32_t Reply-buffer capacity reserved by the producer; a larger reply fails instead of being truncated.
typedef struct MigoSyncOutcome {
uint32_t struct_size;
uint32_t abi_version;
uint32_t request_id;
MigoSyncState state;
uint32_t reply_bytes;
MigoSyncError error;
} MigoSyncOutcome;

Written by the library, append-only. request_id increases monotonically and is never zero; a cleared mailbox contains zero, so this field can be checked for a new result.

Field Type Meaning
struct_size uint32_t Caller input; bounds writes.
abi_version uint32_t Caller input.
request_id uint32_t Monotonic ID of this request; non-zero.
state MigoSyncState Current request state.
reply_bytes uint32_t Non-zero only for READY; actual reply byte count.
error MigoSyncError Non-zero only for FAILED; reason for failure.

Large assets such as texture atlases do not travel through frame packets: packets have a size limit, while textures do not. The resource lane lets producers reserve a slot → upload chunks → wait for digest verification → reference the resource in a frame. Verification completes before GPU objects are created, preventing a digest error from becoming a bound dirty texture.

typedef uint32_t MigoResourceState;
#define MIGO_RESOURCE_STATE_RESERVED 0U
#define MIGO_RESOURCE_STATE_UPLOADING 1U
#define MIGO_RESOURCE_STATE_VERIFYING 2U
#define MIGO_RESOURCE_STATE_READY 3U
#define MIGO_RESOURCE_STATE_FAILED 4U
Value Meaning
RESERVED Slot allocated; upload has not started.
UPLOADING Data chunks are being received.
VERIFYING All chunks received; SHA-256 digest is being checked.
READY Verification passed; frame packets may reference this resource.
FAILED Verification failed or timed out; error explains why.
typedef uint32_t MigoResourceError;
#define MIGO_RESOURCE_ERROR_TOO_MANY_RESERVATIONS 1U
#define MIGO_RESOURCE_ERROR_BAD_SIZE 2U
#define MIGO_RESOURCE_ERROR_BAD_CHUNK_COUNT 3U
#define MIGO_RESOURCE_ERROR_UNKNOWN_RESERVATION 4U
#define MIGO_RESOURCE_ERROR_NON_CONTIGUOUS_CHUNK 5U
#define MIGO_RESOURCE_ERROR_CHUNK_OUT_OF_BOUNDS 6U
#define MIGO_RESOURCE_ERROR_DIGEST_MISMATCH 7U
#define MIGO_RESOURCE_ERROR_TIMED_OUT 8U
#define MIGO_RESOURCE_ERROR_EPOCH_ADVANCED 9U
#define MIGO_RESOURCE_ERROR_INCOMPLETE 10U
#define MIGO_RESOURCE_ERROR_NOT_UPLOADING 11U
Value Meaning
TOO_MANY_RESERVATIONS The maximum number of simultaneous reservations has been reached.
BAD_SIZE total_bytes is zero or outside the permitted range.
BAD_CHUNK_COUNT chunk_count is zero or outside the permitted range.
UNKNOWN_RESERVATION reservation_id does not exist in the table.
NON_CONTIGUOUS_CHUNK Uploaded chunk numbers are not contiguous.
CHUNK_OUT_OF_BOUNDS Chunk offset plus length exceeds total_bytes.
DIGEST_MISMATCH All chunks arrived, but SHA-256 does not match the declaration.
TIMED_OUT Upload did not complete before deadline_nanos.
EPOCH_ADVANCED The resource epoch advanced; this reservation is invalid.
INCOMPLETE Not all chunks had arrived at verification time.
NOT_UPLOADING A chunk was sent for a reservation not in UPLOADING state.
typedef struct MigoResourceReservationDescriptor {
uint32_t struct_size;
uint32_t abi_version;
uint64_t total_bytes;
uint64_t deadline_nanos;
uint32_t chunk_count;
uint32_t format;
uint8_t sha256[32];
} MigoResourceReservationDescriptor;

Written by the caller. The library assigns reservation_id (writing it into MigoResourceOutcome); the caller does not provide it, preventing a producer-selected ID from colliding with an existing reservation.

Field Type Meaning
struct_size uint32_t Set to sizeof(MigoResourceReservationDescriptor).
abi_version uint32_t Set to MIGO_ABI_VERSION_CURRENT.
total_bytes uint64_t Total resource size in bytes; must match the uploaded data.
deadline_nanos uint64_t Monotonic-clock deadline; the reservation becomes invalid after it expires.
chunk_count uint32_t Expected number of chunks; upload must be ordered and contiguous.
format uint32_t Producer-declared format tag; opaque to the protocol and passed unchanged to the renderer.
sha256[32] uint8_t[32] SHA-256 digest expected after all chunks are combined; the entire reservation becomes invalid if verification fails.
typedef struct MigoResourceOutcome {
uint32_t struct_size;
uint32_t abi_version;
uint64_t reservation_id;
uint64_t received_bytes;
MigoResourceState state;
MigoResourceError error;
uint32_t next_chunk;
uint32_t reserved0;
} MigoResourceOutcome;

Written by the library, append-only.

Field Type Meaning
struct_size uint32_t Caller input; bounds writes.
abi_version uint32_t Caller input.
reservation_id uint64_t Library-assigned reservation identifier; non-zero means the reservation was established.
received_bytes uint64_t Number of bytes successfully received.
state MigoResourceState Current reservation state.
error MigoResourceError Non-zero only for FAILED.
next_chunk uint32_t Chunk number that the next upload must carry; chunks must be contiguous.
reserved0 uint32_t Reserved; ignore.

typedef struct MigoExternalSessionDescriptor {
uint32_t struct_size;
uint32_t abi_version;
uint8_t launch_nonce[16];
uint32_t max_packet_bytes;
uint32_t max_credits;
} MigoExternalSessionDescriptor;

Configuration descriptor for creating an external-frame session. Unlike the ordinary MigoSessionConfig path, an external-frame session has no script runtime and a different lifecycle model; calling migo_session_load_content on a session created with this descriptor returns MIGO_ERROR_INVALID_STATE.

Field Type Meaning
struct_size uint32_t Set to sizeof(MigoExternalSessionDescriptor).
abi_version uint32_t Set to MIGO_ABI_VERSION_CURRENT.
launch_nonce[16] uint8_t[16] 128-bit little-endian random key generated by the host using a cryptographic random source. An all-zero value is rejected — zero is the natural value of an uninitialized struct, and accepting it would mean accepting zero-byte packets from anyone. Never put it in logs or URLs.
max_packet_bytes uint32_t Maximum bytes per packet; 0 uses the library default. Values above the limit are clamped down, not up — memory-constrained devices may request smaller packets, but nobody may request larger ones.
max_credits uint32_t Maximum number of in-flight frames the producer may hold; 0 uses the library default. It is likewise clamped down to the compile-time range.

/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_submit_external_frame(
MigoSession *session,
const uint8_t *bytes,
size_t byte_count,
MigoFrameIngressOutcome *out_outcome);

Submits a frame packet produced in another process to the session. bytes is borrowed only during the call: before returning, the library copies an accepted packet into its own buffer, so a Swift transport layer may immediately release or reuse the pointer inside its Data. out_outcome is allocated and owned by the caller; struct_size and abi_version must be filled before the call. Initialize it once and reuse it across frames rather than rebuilding it for each frame.

Parameters:

Parameter Description
session Target session handle; must not be NULL.
bytes Frame-packet bytes, borrowed during the call.
byte_count Frame-packet length in bytes.
out_outcome Caller-allocated result structure; struct_size and abi_version must be prefilled.

Returns:

  • MIGO_OK: outcome was written, including cases where the outcome itself reports rejection; a non-OK MigoResult means the call itself could not complete.
  • MIGO_ERROR_INVALID_ARGUMENT: session or bytes is NULL, or the header fields of out_outcome were not filled.
  • MIGO_ERROR_INVALID_STATE: The session was destroyed or no surface is attached yet.

Threading:

May be called from a transport thread. For an accepted packet, the library completes its internal copy before returning, so the caller need not keep bytes alive.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_request_external_frame(MigoSession *session);

Requests one rendered frame from the external producer. In external-frame mode, the producer renders only when driven — Migo’s requestAnimationFrame is driven by host vsync on every platform, and this entry point is the equivalent signal across the process boundary. After receiving a vsync callback, the host calls this function; the producer process receives the signal, renders, and returns a packet through migo_session_submit_external_frame.

Parameters:

Parameter Description
session Target session handle; must not be NULL.

Returns:

  • MIGO_OK: The request was recorded. Requests coalesce into one frame; a request made before the renderer is ready is held and armed when it starts, never lost.
  • MIGO_ERROR_INVALID_STATE: No surface is attached yet (there is no frame clock).
  • MIGO_ERROR_INVALID_ARGUMENT: session is NULL.

The producer’s own requests arrive as control messages on the socket (see migo_session_submit_uplink_control); this entry point is for a host asking on its behalf.

Threading:

Callable from any thread; does not block.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_uplink_message_kind(
const uint8_t *bytes,
size_t byte_count,
MigoUplinkMessageKind *out_kind);

The producer’s socket carries three kinds of message: frame packets, control messages (today, only “request the next frame”) and service messages. The transport asks the library which door a message goes through instead of comparing magic numbers itself — a host that encoded the rule would be one more implementation of the wire format to drift.

  • MIGO_UPLINK_MESSAGE_CONTROL: pass it to migo_session_submit_uplink_control.
  • MIGO_UPLINK_MESSAGE_SERVICE: pass it to migo_session_submit_service.
  • MIGO_UPLINK_MESSAGE_FRAME: pass it to migo_session_submit_external_frame. Everything that is neither a control nor a service message is this, including bytes that are not a frame either — frame ingress refuses them with a verdict the producer sees. A third “unknown” answer would need a third place to report it from.

Needs no session and reads at most the first four bytes. bytes may be NULL when byte_count is 0. Messages that arrive on the content origin (the custom scheme) are always frames and need not be asked about.

Returns: MIGO_ERROR_INVALID_ARGUMENT when out_kind is NULL, or bytes is NULL with a non-zero byte_count; otherwise MIGO_OK.

Threading: stateless; any thread.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_submit_uplink_control(
MigoSession *session,
const uint8_t *bytes,
size_t byte_count,
uint32_t *out_refusal_code);

Reads one control message and acts on it. Migo’s requestAnimationFrame is driven by host vsync on every platform, and the producer’s demand for the next frame has to cross the process boundary to reach here for a frame-clock tick to exist. The format is Uplink control messages in contracts/frame-wire/wire-v1.md.

Rules:

  • Validated whole before anything is acted on: if any part is malformed, none of the message takes effect.
  • *out_refusal_code is 0 when the message was read — including when its requests belong to another runtime generation (ignored rather than refused: the producer that sent them is gone). A non-zero value is a refusal code from 3001 up (see Control refusals in the wire contract); report it the way a refused frame is reported.
  • A frame request made before the renderer is ready is held and armed when it starts — the producer’s first request races the host’s bring-up, and nothing would tell it to ask again.
  • Requests coalesce: one tick answers every request made before it.

Returns:

  • MIGO_OK: *out_refusal_code was written.
  • MIGO_ERROR_INVALID_ARGUMENT: session, bytes or out_refusal_code is NULL, or byte_count is 0.
  • MIGO_ERROR_INVALID_STATE: No surface is attached yet.

Threading: call on the transport thread, like migo_session_submit_external_frame; does not block.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_take_external_gl_error(
MigoSession *session,
uint32_t canvas_id,
uint32_t *out_code);

Takes one pending WebGL error code from the specified canvas’s error queue. gl.getError() is synchronous, but the producer issues it in another process; the decoder records the error in the host process, and this entry point lets the producer retrieve it on demand. An empty queue writes 0 (GL_NO_ERROR), as required by the WebGL specification. Each call consumes one error; the producer must loop until it receives GL_NO_ERROR.

Parameters:

Parameter Description
session Target session handle; must not be NULL.
canvas_id Target canvas identifier, assigned by the protocol layer and opaque to the C ABI.
out_code Caller-allocated uint32_t; receives 0 when the queue is empty.

Returns:

  • MIGO_OK: out_code was written, possibly with 0.
  • MIGO_ERROR_INVALID_ARGUMENT: A pointer is NULL, or canvas_id is invalid.
  • MIGO_ERROR_INVALID_STATE: The session was destroyed.

/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_post_sync_request(
MigoSession *session,
const MigoSyncRequestDescriptor *request,
const uint8_t *params,
size_t param_bytes,
uint64_t now_nanos,
MigoSyncOutcome *out_outcome);

Posts a sync-barrier request and answers it in place: the producer is blocked waiting, the transport layer brings the request here, this function completes the answer, and migo_session_take_sync_reply retrieves the bytes. Only one unfinished request is permitted per session; a second is rejected with MIGO_SYNC_ERROR_ALREADY_PENDING.

Parameters:

Parameter Description
session Target session handle; must not be NULL.
request Request descriptor: caller-written; struct_size and abi_version are required, and an uninitialized header is rejected before the request is examined.
params Operation-parameter bytes; may be NULL only when param_bytes is 0. Layout follows each operation’s definition above (for example, the 32-byte readPixels record).
param_bytes Number of parameter bytes.
now_nanos Caller monotonic-clock reading, from the same clock as deadline_nanos; the library does not read a clock itself. Two clocks that happen to agree today are a platform hazard — only the host knows which clock the producer is blocked on.
out_outcome Caller-allocated; its header (struct_size/abi_version) is input. A zero header is rejected with MIGO_ERROR_UNSUPPORTED_ABI while the producer is blocked, effectively wasting its entire deadline on a rejection that can never be delivered.

Returns:

  • MIGO_OK: The answer is in place, including when the answer internally reports MIGO_SYNC_STATE_FAILED; before a rejected producer obtains a request ID, request_id is 0.
  • MIGO_ERROR_INVALID_ARGUMENT: A pointer is NULL or a header field was not filled.
  • MIGO_ERROR_UNSUPPORTED_ABI: abi_version is unknown or struct_size declares a larger record unknown to this build.

Threading:

Blocks until the request is settled: first for the frame it names to be admitted, then for the readback. The library takes the sync handle under the session lock and releases the lock before it blocks, because the frame it waits for arrives through migo_session_submit_external_frame, which needs that lock. Call it on a thread that may block, and never while holding a lock that frame submission needs. The outcome it reports is this request’s own verdict, not whatever the mailbox holds by the time it returns.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_call_sync(
MigoSession *session,
const uint8_t *call,
size_t call_bytes,
uint64_t now_nanos,
uint8_t *header,
size_t header_capacity,
MigoSyncReply **out_reply);

A synchronous call as one body in and one body out, for a producer that cannot share memory with the host. On Apple the content origin is a custom scheme, where WebKit provides no SharedArrayBuffer, so a Worker blocks in a synchronous request. The transport hands its body here unchanged, and sends back, as one response body, the header this writes followed by the reply’s bytes. Both layouts are the wire format’s (contracts/frame-wire/wire-v1.md, “A request as one body” and “An answer as one body”); the transport parses neither.

The body carries how long the producer will wait, not a deadline, because its clock is not the host’s. The deadline is now_nanos plus that wait, so now_nanos is the caller’s monotonic clock, as for migo_session_post_sync_request.

The reply is not copied: *out_reply is the buffer the renderer itself allocated, handed over, with no second copy in the library. A readback can be a full screen at device scale, on the path a producer is blocked on. Read its address and length with migo_sync_reply_bytes, and release it with migo_sync_reply_release once the bytes have been sent.

The verdict is read under the lock that settles the request, the reply is that request’s own readback, and the slot is freed in the same step. There is no take step, and no window in which another request’s answer can be taken for this one.

Parameters:

Parameter Description
session Target session handle; must not be NULL.
call / call_bytes The body, unchanged from the producer. A body above MIGO_SYNC_CALL_MAX_BYTES should be refused by the transport before it is read.
now_nanos Caller monotonic-clock reading.
header / header_capacity Where the answer header is written; at least MIGO_SYNC_ANSWER_HEADER_BYTES. Less is refused before the call is posted, so no readback is spent on an answer that could not be delivered.
out_reply The reply handle when the call was answered with bytes; NULL otherwise, including for every failure, whose verdict is in the header.

Returns:

  • MIGO_OK: The header was written, including for a call that failed or could not be decoded: the producer is blocked on the response whatever happened, and the verdict belongs in it.
  • MIGO_ERROR_INVALID_ARGUMENT: A pointer is NULL, or header_capacity is too small. *out_reply is NULL.
  • MIGO_ERROR_INVALID_STATE: The session has no renderer yet (no surface attached) or was destroyed. No header is written.

Threading:

As for migo_session_post_sync_request: it blocks, and only after releasing the session lock; never call it while holding a lock frame submission needs. A producer has one agent blocked at a time, so one serial queue is enough – but that queue must not be the one frame uploads arrive on, or the frame a read waits for queues behind the read.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_sync_reply_bytes(
const MigoSyncReply *reply,
const uint8_t **out_bytes,
size_t *out_length);

Where a reply’s bytes are, and how many. Valid until the reply is released. On failure *out_bytes is NULL and *out_length is 0.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_sync_reply_release(MigoSyncReply *reply);

Frees a reply; the handle and its bytes are invalid afterwards. NULL is refused with MIGO_ERROR_INVALID_ARGUMENT rather than ignored: releasing a reply that was never received is a caller that lost track of which answers carried bytes.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_poll_sync(
MigoSession *session,
uint64_t now_nanos,
MigoSyncOutcome *out_outcome);

Queries the state of the unfinished request. It is also the only path that makes deadlines observable: while a request is unfinished, nothing else runs, so an expired deadline is settled here. now_nanos and out_outcome have exactly the same semantics as in migo_session_post_sync_request, including the rule that an all-zero header is rejected first and makes the producer wait out its entire deadline for nothing.

The return set is the same (MIGO_OK / MIGO_ERROR_INVALID_ARGUMENT / MIGO_ERROR_UNSUPPORTED_ABI).

Threading: serialized with the Session; it may be called on the transport layer’s regular tick.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_take_sync_reply(
MigoSession *session,
uint8_t *buffer,
size_t capacity,
size_t *out_written);

Copies out the ready reply bytes and frees the slot. Never truncates: if capacity is smaller than the reply, the operation is rejected as a whole and the reply remains in MIGO_SYNC_STATE_READY; the caller can provide a larger buffer and try again. On success *out_written is the actual byte count; on failure it is set to 0, so ignoring the return value cannot make the caller use a stale count as the length.

Returns:

  • MIGO_OK: Bytes copied and the slot freed (the next request may be posted).
  • MIGO_ERROR_INVALID_STATE: No READY reply is available.
  • MIGO_ERROR_INVALID_ARGUMENT: A pointer is NULL.

Threading: serialized with the Session; normally called immediately after poll_sync reports MIGO_SYNC_STATE_READY.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_cancel_sync(
MigoSession *session,
uint64_t now_nanos,
MigoSyncOutcome *out_outcome);

The producer withdraws the request: the unfinished request is settled as MIGO_SYNC_STATE_CANCELLED and the slot is freed. A completed reply is unaffected — cancel is not a way to discard a reply that has not yet been read. The header rules for out_outcome are the same as for the other sync entry points.

Threading: serialized with the Session.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_take_downlink(
MigoSession *session,
uint8_t *buffer,
size_t capacity,
size_t *out_written);

Takes the next message the host owes the producer. The bytes are a downlink envelope carrying two kinds of records: the decision for each submitted frame (decision, remaining credits, and accepted sequence) and frame-clock ticks that drive the producer’s requestAnimationFrame. The host must not assemble envelopes itself — a third implementation of the same wire format is exactly the drift that this project’s gate is intended to prevent; a transport layer that only copies bytes cannot drift.

Rules:

  • Whole message out: If capacity cannot hold the envelope and one record, not a single byte is written and the queue is left unchanged; provide a larger buffer and call again without losing anything. 4096 bytes is enough for every message this queue can produce.
  • out_written equal to 0 is a normal answer, not an error, and must not be busy-looped: between frames there may simply be “nothing to send”.
  • When to call: after every submit, and after the downlink waker (migo_session_set_downlink_waker) fires. The queue is bounded and coalesces ticks; transport lag costs producer scheduling latency, not memory — each record is absolute, so the next record read is the current correct value.
  • A tick carries the window: (remaining_credits, accepted_sequence). A verdict is only sent for a frame, so a producer whose last verdict said zero learns that a credit came back from the tick it asked for.

Returns: a NULL pointer returns MIGO_ERROR_INVALID_ARGUMENT; every other valid call returns MIGO_OK, including when there is nothing to send.

Threading: serialized with the Session; suitable for the transport send thread.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
typedef void (MIGO_CALL *MigoDownlinkWakerFn)(void *user_data);
MigoResult migo_session_set_downlink_waker(
MigoSession *session,
MigoDownlinkWakerFn waker,
void *user_data);

Installs (or, with NULL, clears) the downlink waker. The library calls it when it queues a downlink record the transport did not cause — a frame-clock tick. A verdict is queued inside a submit the transport made, which drains right after; a tick is not, and without a wake-up it waits until the producer sends something, while a producer waiting for a tick sends nothing.

Rules:

  • Called on the session’s own thread. Schedule the drain; do not perform it in the waker: return promptly, and do not call back into the library from inside it — in particular not migo_session_set_downlink_waker, which waits for the call in progress to return.
  • One waker per session; installing again replaces it. Clearing returns only after any call in progress has returned, so user_data may be freed once it returns.
  • Clear it before migo_session_destroy.

Returns:

  • MIGO_OK: Installed or cleared.
  • MIGO_ERROR_INVALID_ARGUMENT: session is NULL.
  • MIGO_ERROR_INVALID_STATE: No surface is attached yet.

Threading: callable from any thread; the waker is called on the session thread.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
typedef struct MigoOwnedBytes MigoOwnedBytes;
MigoResult migo_owned_bytes_view(
const MigoOwnedBytes *owned,
const uint8_t **out_bytes,
size_t *out_length);

MigoOwnedBytes are bytes the library hands to the caller and owns until migo_owned_bytes_release. A service answer can be a whole file, so it is handed over rather than copied into a caller buffer. This reports where the bytes are and how many; the view is valid until the handle is released.

Returns:

  • MIGO_OK: *out_bytes and *out_length were written.
  • MIGO_ERROR_INVALID_ARGUMENT: a pointer is NULL; writable outputs receive NULL and zero.

Threading: stateless, any thread; do not race a view against migo_owned_bytes_release on the same handle.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_owned_bytes_release(MigoOwnedBytes *owned);

Frees the bytes; the handle is invalid afterwards. Release each handle exactly once.

Returns: MIGO_ERROR_INVALID_ARGUMENT for a NULL handle, otherwise MIGO_OK.

Threading: any thread.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
#define MIGO_SERVICE_MESSAGE_MAX_BYTES 67108864U
MigoResult migo_session_submit_service(
MigoSession *session,
const uint8_t *bytes,
size_t byte_count,
uint32_t *out_refusal_code);

Admits one service message. The service stream carries everything content asks the host to do that is not drawing: read a file, write a save, load an image, play a sound, open a socket. Its messages are sequenced, admitted strictly in order, and answered on a return stream of their own. The format is contracts/frame-wire/wire-v1.md, The service stream; a transport parses none of it.

Transport:

  • A message of at most 65536 bytes arrives on the producer’s socket (migo_uplink_message_kind answers MIGO_UPLINK_MESSAGE_SERVICE); a larger one as a POST to the content origin’s service endpoint. A transport reading a POST refuses one larger than MIGO_SERVICE_MESSAGE_MAX_BYTES before reading it.
  • The two paths reorder, so a message that arrives ahead of the one before it is held – up to 256 messages and MIGO_SERVICE_MESSAGE_MAX_BYTES – and admitted when that one arrives. Either way the call returns at once and a POST can be answered straight away.

Rules:

  • bytes is borrowed for the call; admitted records are copied before it returns.
  • *out_refusal_code receives 0 when the message was admitted, held, or ignored as belonging to another runtime generation; otherwise a code from 4001 up (see Service refusals in the wire contract). A refusal is also reported to the producer on the return stream: the stream is broken from there.
  • Blocks while the session’s queue of admitted work is full, which pushes back on the socket; call it on a thread that may block.

Returns:

  • MIGO_OK: *out_refusal_code was written.
  • MIGO_ERROR_INVALID_ARGUMENT: session, bytes or out_refusal_code is NULL, or byte_count is 0.
  • MIGO_ERROR_INVALID_STATE: no surface attached yet, or the session has ended.

Threading: the transport’s threads (the socket reader or the content origin’s request thread); may block.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_take_service_message(
MigoSession *session,
MigoOwnedBytes **out_message);

Takes the next message of answers and events the host owes the producer. *out_message receives NULL when nothing is queued – the normal answer, not an error. Otherwise send the bytes on the socket unread, then release them with migo_owned_bytes_release.

Call it wherever migo_session_take_downlink is called: the downlink waker fires for both.

Returns:

  • MIGO_OK: *out_message was written (possibly NULL).
  • MIGO_ERROR_INVALID_ARGUMENT: session or out_message is NULL.
  • MIGO_ERROR_INVALID_STATE: no surface attached yet.

Threading: as migo_session_take_downlink; suits the transport’s sending thread.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_take_parked_reply(
MigoSession *session,
uint32_t generation,
uint32_t request_id,
MigoOwnedBytes **out_reply);

Takes a parked answer. An answer too large to send inline on the return stream is parked; the producer asks the content origin for it by generation and request id, and the content origin takes it with this call. Taken once: the library releases its copy as it hands this one over.

*out_reply receives NULL when there is no such answer – a producer asking twice, or asking for another generation’s; answer that request with a 404.

Returns:

  • MIGO_OK: *out_reply was written (possibly NULL).
  • MIGO_ERROR_INVALID_ARGUMENT: session or out_reply is NULL.
  • MIGO_ERROR_INVALID_STATE: no surface attached yet.

Threading: the content origin’s request thread; does not block.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
MigoResult migo_session_copy_content_root(
MigoSession *session,
char *buffer,
size_t capacity,
size_t *out_length);

Where the loaded content’s code is: the directory the host serves at the content origin’s root, written as NUL-terminated UTF-8.

On this execution migo_session_load_content mounts the content before it returns – the entry module is evaluated by WebKit in another process, and that process’s host needs this directory to serve it – so the root is known as soon as that call succeeds.

Returns:

  • MIGO_OK: written; *out_length is the length without the NUL.
  • MIGO_ERROR_INVALID_ARGUMENT: out_length is NULL, buffer is NULL with a non-zero capacity, or capacity cannot hold the path and its NUL. When the capacity is too small *out_length is still set, so the caller can retry.
  • MIGO_ERROR_INVALID_STATE: no content loaded yet.
  • MIGO_ERROR_INTERNAL: the path cannot be spelled in UTF-8 (refused rather than lossily rewritten).

Threading: any thread.


/* Derived example: no tests/c_host coverage yet; the header is authoritative */
#define MIGO_CONTENT_MODULE_SERVED 0U /* the bytes are the module's source */
#define MIGO_CONTENT_MODULE_NOT_FOUND 1U /* the bytes are why: answer 404 */
#define MIGO_CONTENT_MODULE_REFUSED 2U /* the bytes are why: answer 403 */
#define MIGO_CONTENT_MODULE_UNREADABLE 3U /* the bytes are why: answer 500 */
MigoResult migo_session_read_content_module(
MigoSession *session,
const char *path,
size_t path_length,
MigoOwnedBytes **out_module,
uint32_t *out_status);

The source of the content module at path – a path on the content origin, such as "/game.js" – as the engine evaluates it: resolved through the mounted package (subpackage overlays and pack-backed packages included), contained in it, UTF-8, and with the module loader’s one rewrite applied, so a CommonJS entry runs wrapped exactly as it does in the embedded runtime. The content origin answers every script request of a game with this rather than with the file.

Rules:

  • path is borrowed UTF-8 of path_length bytes.
  • *out_module receives the bytes – the source, or the reason it was not served, as *out_status says – which the caller releases with migo_owned_bytes_release.
  • Reads the file on the calling thread.

Returns:

  • MIGO_OK: *out_module and *out_status were written.
  • MIGO_ERROR_INVALID_ARGUMENT: a pointer is NULL, path_length is 0, or path is not UTF-8.
  • MIGO_ERROR_INVALID_STATE: no content loaded yet.

Threading: the content origin’s request thread; it does file IO, so keep it off the main thread.


Before filling any descriptor struct, zero it and then set struct_size and abi_version:

/* Derived example: no tests/c_host coverage yet; the header is authoritative */
/* ingress outcome: initialize once, reuse across frames */
MigoFrameIngressOutcome outcome = {0};
outcome.struct_size = sizeof(outcome);
outcome.abi_version = MIGO_ABI_VERSION_CURRENT;
while (has_frame()) {
const uint8_t *pkt = next_packet(&pkt_len);
MigoResult r = migo_session_submit_external_frame(
session, pkt, pkt_len, &outcome);
if (r != MIGO_OK) { /* the call failed; not a frame-level refusal */ break; }
switch (outcome.decision) {
case MIGO_FRAME_INGRESS_ACCEPTED:
/* outcome.accepted_sequence tracks completion notifications */
break;
case MIGO_FRAME_INGRESS_WOULD_BLOCK:
/* backpressure: retry once credit returns; never drop this frame */
wait_for_credit();
break;
case MIGO_FRAME_INGRESS_REJECTED:
/* malformed packet; log outcome.wire_error_code */
break;
case MIGO_FRAME_INGRESS_GENERATION_LOST:
/* the runtime generation is gone; do not retry */
break;
}
}

Ownership rules:

  • MigoFrameIngressOutcome, MigoSyncOutcome, and MigoResourceOutcome are allocated by the caller; the library only writes them and does not retain them.
  • MigoSyncRequestDescriptor, MigoResourceReservationDescriptor, and MigoExternalSessionDescriptor are filled by the caller and borrowed for the duration of the call; they may be released after the call returns.
  • The bytes pointer passed to migo_session_submit_external_frame is borrowed only during the call; the library completes its copy before returning.
  • MigoOwnedBytes are allocated by the library and handed to the caller, who releases each exactly once with migo_owned_bytes_release.
  • session.mdx — MigoSessionConfig.launch_nonce and session lifecycle
  • surface.mdx — surface attach/detach and vsync driving
  • engine.mdx — engine creation and capability query