ZaStoGram/TMessagesProj/jni/voip/tgcalls/v2wasm/CallCoreABI.h
2026-09-24 16:17:30 +04:00

240 lines
14 KiB
C

#ifndef TGCALLS_V2WASM_CALL_CORE_ABI_H
#define TGCALLS_V2WASM_CALL_CORE_ABI_H
#include <stddef.h>
#include <stdint.h>
// ============================================================================
// tgcalls call-core ABI, version 1.
//
// This is the frozen seam between the fixed native harness ("host") and the
// swappable call-control logic ("core"). It is shaped exactly like the future
// WASM boundary: the three functions below are the module exports, and the
// single emit callback is the module import. All payloads are UTF-8 JSON
// objects tagged with "@type".
//
// Rules (normative):
// - THREADING: all calls into the core happen on one thread (the tgcalls
// media thread). The core never blocks and owns no threads/timers; it asks
// the host for time via the "set_timer" command.
// - REENTRANCY: the emit callback may be invoked only from inside
// tgcalls_core_create / tgcalls_core_on_event, on the same thread. The host
// queues emitted commands and executes them AFTER the core call returns.
// Results of asynchronous commands are delivered as later events.
// - MEMORY: buffers passed to the core are valid only for the duration of the
// call; buffers passed to emit are valid only for the duration of the
// callback. Each side copies what it keeps (WASM linear-memory contract).
// - EXTENSIBILITY: the core MUST ignore events with an unknown "@type". The
// host MUST reply to an unknown or malformed command with an "error" event
// and MUST NOT crash. The config carries "abiVersion"; the core echoes it
// in "core_ready"; on mismatch the host disables the core (drops all
// further commands/events) and reports the call failed.
// - CLOCK: the core has no clock. Every event carries "nowMs" (int64 as JSON
// number, host monotonic milliseconds); the core uses the latest value.
// - BUDGET: data channels are control-plane; modules should stay in the
// ≲10 Hz / KB-scale envelope (enforcement: Phase-4 metering).
//
// Config (tgcalls_core_create):
// { "abiVersion": 1, "isOutgoing": bool,
// "enableP2P": bool, "customParameters": string,
// "rtcServers": [ { "host": s, "port": n, "login": s, "password": s,
// "isTurn": bool, "isTcp": bool } ] }
// No key material, ever.
//
// Events (host -> core): "@type" plus fields, all carry "nowMs":
// signaling_packet { packetB64: s } full plaintext signaling
// packet, base64: seq(4 bytes, network order) || body.
// The host has already opened the AEAD and replay-
// checked the counter (low 30 bits of seq). The body
// layout is core policy: V1 (wire 10.0.0) message
// packing incl. acks/resends; V2 gzip'd JSON.
// pc_renegotiation_needed {}
// pc_ice_candidate { mid: s, mline: n, sdp: s }
// pc_ice_state { state: "new"|"checking"|"connected"|"completed"|
// "failed"|"disconnected"|"closed" }
// pc_connection_state { state: "new"|"connecting"|"connected"|
// "disconnected"|"failed"|"closed" }
// pc_gathering_state { state: "new"|"gathering"|"complete" }
// pc_signaling_state { state: "stable"|"have-local-offer"|
// "have-remote-offer"|"have-local-pranswer"|
// "have-remote-pranswer"|"closed" }
// pc_candidate_pair_changed { local: {type,protocol,address},
// remote: {type,protocol,address} }
// pc_description_created { ok: bool, type: "offer"|"answer", sdp: s,
// error: s } completes pc_create_offer/
// pc_create_answer
// pc_set_local_done { ok: bool, type: "offer"|"answer", sdp: s }
// sdp read back after apply
// pc_set_remote_done { ok: bool, sdpType: "offer"|"answer" }
// pc_track { mid: s, kind: "audio"|"video" } remote track (OnTrack)
// dc_state { label: s, open: bool }
// dc_message { label: s, data?: s, dataB64?: s } text arrives as
// data, binary as dataB64 (base64)
// dc_buffered { label: s, bufferedAmount: n } emitted when a
// channel's send buffer drains to 0 (backpressure:
// send a batch, wait for drain)
// dc_channel { label: s, id: n } remote-announced channel
// (OnDataChannel); registered under its label,
// dc_send/dc_state/dc_message work on it thereafter
// timer { token: n, generation?: n } generation echoes
// whatever set_timer sent (0 if that omitted it)
// stats { sendBitrateKbps: n, curated GetStats reduction;
// transport: { rttMs?, availableOutgoingKbps?,
// availableIncomingKbps?, bytesSent, bytesReceived,
// localCandidateType?, remoteCandidateType? },
// audio: { send?: { bitrateKbps, packetsSent,
// remoteLossFraction?, remoteRttMs?,
// remoteJitterMs? },
// recv?: { bitrateKbps, packetsReceived,
// packetsLost, jitterMs?, audioLevel? } },
// video: { send?: { bitrateKbps, packetsSent,
// frameRate?, frameWidth?, frameHeight?,
// qualityLimitationReason?, remoteLossFraction?,
// remoteRttMs?, remoteJitterMs? },
// recv?: { bitrateKbps, packetsReceived,
// packetsLost, framesDecoded, frameRate?,
// frameWidth?, frameHeight? } } }
// absent measurements omit their keys; bitrates are
// host-computed deltas between polls, EXCEPT the
// top-level sendBitrateKbps which is the max BWE
// (available_outgoing_bitrate / 1024, kept for
// stats-log parity with stock)
// mute { muted: bool }
// battery_low { low: bool }
// video_capture { active: bool, screencast: bool }
// stop {} core replies stats_log + close
// error { message: s, command: s }
//
// Commands (core -> host): "@type" plus fields:
// core_ready { abiVersion: n }
// pc_create { iceTransportsType: "all"|"relay",
// iceServers: [ { urls: [s], username: s,
// password: s } ],
// audioProcessing?: { … } (optional APM overrides, defaults = stock) }
// pc_create_offer {} -> pc_description_created
// pc_create_answer {} -> pc_description_created
// pc_set_local_description { type: s, sdp: s } the SDP to apply (the
// core may munge what it got
// from pc_description_created)
// pc_set_remote_description { sdpType: s, sdp: s }
// pc_add_ice_candidate { mid: s, mline: n, sdp: s }
// pc_restart_ice {} flows back through
// pc_renegotiation_needed
// pc_add_transceiver { id: s (core-chosen), kind: "audio"|"video",
// direction: "sendrecv"|"sendonly"|"recvonly"|
// "inactive",
// codecPreferences: [s] (optional),
// sendEncodings: [ { active?: bool,
// maxBitrateBps?: n, minBitrateBps?: n,
// scaleResolutionDownBy?: n, rid?: s } ] (optional),
// trackSource: "microphone"|"camera"|"none" }
// pc_set_parameters { id: s, degradationPreference?: "disabled"|
// "maintain-framerate"|"maintain-resolution"|
// "balanced",
// encodings: [ same fields as sendEncodings ] }
// host merges into GetParameters() then SetParameters
// pc_set_track_enabled { id: s, enabled: bool }
// pc_remove_track { id: s }
// pc_set_incoming_sink { mid: s } bind the app video sink to
// this incoming transceiver
// pc_create_data_channel { label?: s (default "data"), ordered?: bool,
// negotiated?: bool, id?: n } callable N times;
// labels unique among core-created channels
// (duplicate -> "error" event)
// dc_send { label?: s (default "data"), data?: s, dataB64?: s }
// exactly one of data/dataB64; max 256 KiB; unknown
// label or closed channel -> "error" event
// set_audio_processing { echoCancellation?: bool, noiseSuppression?: bool,
// autoGainControl?: bool, highPassFilter?: bool }
// host ApplyConfig on its retained APM; only supplied
// keys change; autoGainControl maps to the classic AGC
// (gain_controller1). Also accepted at creation as
// pc_create.audioProcessing { same keys }.
// pc_set_configuration { iceServers?: [ { urls: [s], username: s,
// password: s } ],
// iceTransportsType?: "all"|"relay",
// candidatePoolSize?: n }
// host merges ONLY these keys into GetConfiguration()
// then SetConfiguration; webrtc rejections surface as
// "error" events
// signaling_send_packet { packetB64: s } full plaintext packet,
// base64: seq(4, network order) || body, built by the
// core. seq layout: bit31 = single-message-packet,
// bit30 = requires-ack (V1 framing flags), low 30 bits
// = counter. The host enforces a strictly increasing
// counter (AEAD IV freshness), seals with the native
// key, and sends via the native transport routing.
// Framing (seq + gzip) is entirely core-owned; the
// reference core reproduces stock's wire 11.0.0
// framing byte-for-byte.
// set_timer { token: n, generation?: n, delayMs: n } a module
// that never uses generation may omit it; the host
// reads it via number_value(), which defaults a
// missing/non-numeric field to 0
// pc_get_stats {} -> stats event
// emit_state { state: "established"|"failed"|"reconnecting" }
// emit_signal_bars { bars: 0..4 }
// emit_remote_media_state { audio: "active"|"muted",
// video: "inactive"|"paused"|"active" }
// emit_remote_battery_low { low: bool }
// log { message: s }
// stats_log { json: s } host writes at stop
// close {} host closes PC, completes stop
//
// Module form (WASM). Function pointers cannot cross the module boundary;
// a .wasm module implements ABI v1 as (one module instance per call — the
// instance IS the handle):
// exports: core_init(config_ptr: i32, config_len: i32)
// core_on_event(ptr: i32, len: i32)
// rt_alloc(len: i32) -> i32
// rt_free(ptr: i32)
// imports: env.host_emit(ptr: i32, len: i32)
// env.host_deflate(in: i32, in_len: i32, out: i32, out_cap: i32) -> i32
// env.host_inflate(in: i32, in_len: i32, out: i32, out_cap: i32) -> i32
// For each event the host rt_allocs, copies in, calls core_on_event, then
// rt_frees after it returns; the module copies anything it retains.
// host_emit payloads are copied out by the host during the call.
// tgcalls_core_destroy maps to instance destruction (no export).
//
// Host compression service (host_deflate/host_inflate; C form
// tgcalls_host_deflate/tgcalls_host_inflate below): a pure gzip codec —
// deflate wraps in the gzip container at max compression (the same zlib the
// stock implementations use, so compressed bytes match stock exactly);
// inflate accepts gzip (1f 8b) and zlib (78 9c) framing. Returns bytes
// written into out (>= 0) or -1 on failure/insufficient out_cap. Synchronous
// and stateless: mechanism only — WHETHER and WHAT to compress stays core
// policy (V2 framing). Buffer ranges are validated at the module boundary
// (WAMR "(*~*~)i" signature). out_cap doubles as the inflate size limit
// (zip-bomb bound), chosen by the core.
// ============================================================================
#ifdef __cplusplus
extern "C" {
#endif
typedef struct TgcallsCallCore TgcallsCallCore;
// Host compression service (see the doc block above). Provided by the
// harness: natively via utils/gzip (zlib), in the module form as the
// env.host_deflate/env.host_inflate imports (the attributes below bind the
// same C symbols to those imports under a wasm32 build).
#if defined(__wasm__)
__attribute__((import_module("env"), import_name("host_deflate")))
#endif
int32_t tgcalls_host_deflate(const uint8_t *in, size_t inLen, uint8_t *out, size_t outCap);
#if defined(__wasm__)
__attribute__((import_module("env"), import_name("host_inflate")))
#endif
int32_t tgcalls_host_inflate(const uint8_t *in, size_t inLen, uint8_t *out, size_t outCap);
typedef void (*TgcallsCoreEmitFn)(void *userData, const uint8_t *data, size_t len);
TgcallsCallCore *tgcalls_core_create(const char *configJson, TgcallsCoreEmitFn emit, void *userData);
void tgcalls_core_on_event(TgcallsCallCore *core, const uint8_t *data, size_t len);
void tgcalls_core_destroy(TgcallsCallCore *core);
#ifdef __cplusplus
}
#endif
#endif