How the iii Engine's WebSocket Protocol Enables Cross-Language Function Invocation Between Python, TypeScript, and Rust
The iii engine uses a unified JSON-over-WebSocket protocol defined in engine/src/protocol.rs to route function calls between workers written in Python, TypeScript, and Rust, enabling seamless polyglot interoperability through language-agnostic message frames.
The iii engine (iii-hq/iii) implements a polyglot function-as-a-service architecture where workers in different languages communicate through a single, shared WebSocket protocol. By standardizing on JSON message frames defined by a central Message enum, the engine enables cross-language function invocation without language-specific serialization layers or adapters. This design allows a Python worker to execute functions registered by TypeScript, Rust services to trigger Node.js handlers, and any combination thereof—all through identical wire formats.
Protocol Architecture and the Message Enum
At the heart of the iii engine's cross-language capability lies a single source of truth defined in engine/src/protocol.rs. This file contains the Message enum, which specifies every frame type traversing the WebSocket connection: WorkerRegistered, RegisterFunction, InvokeFunction, InvocationResult, and InvocationError.
Because all three SDKs (Python, TypeScript/Node, and Rust) serialize and deserialize these identical JSON structures, the engine acts purely as a router. When a worker connects, it performs a handshake (WorkerRegistered), after which the engine can push InvokeFunction frames to any connected worker and receive InvocationResult frames in return. The payload remains untouched JSON as it travels between language runtimes.
Worker Registration and Handshake
Before cross-language invocation can occur, workers must register their exposed functions with the engine. The SDKs open a WebSocket connection to the engine port and send RegisterFunction messages for each capability they offer.
In engine/src/engine/mod.rs, the WebSocket server handles the upgrade from HTTP, tracks connected workers, and stores the mapping between function_id strings and their hosting workers. Each worker receives a unique worker_id upon successful registration.
Python registration (sdk/packages/python/iii/src/iii/iii.py):
from iii import register_worker
iii = register_worker("ws://localhost:49134")
@iii.register_function("hello::greet")
def greet(name: str) -> str:
return f"Hello, {name}!"
TypeScript registration (sdk/packages/node/iii/src/iii.ts):
import { registerWorker } from "iii-sdk";
const iii = registerWorker("ws://localhost:49134");
iii.registerFunction("analytics::track", async (data) => {
console.log("Processing event:", data);
return { status: "recorded" };
});
Rust registration (sdk/packages/rust/iii/src/lib.rs):
use iii_sdk::{register_worker, RegisterFunction};
let iii = register_worker("ws://localhost:49134");
iii.register_function("compute::factorial", |n: u64| {
(1..=n).product::<u64>()
}).await?;
Cross-Language Invocation Flows
Once functions are registered, the engine routes invocations based solely on the function_id, ignoring the implementation language. The protocol supports two invocation patterns defined in engine/src/protocol.rs.
Synchronous Invocation with Result Awaiting
For synchronous calls, the engine generates a unique invocation_id, sends an InvokeFunction frame to the target worker, and blocks until receiving a matching InvocationResult or InvocationError frame.
TypeScript invoking Python:
const result = await iii.invokeFunction({
function_id: "hello::greet",
data: "World"
});
// Returns: "Hello, World!"
The resulting JSON frame carries the original invocation_id, allowing the TypeScript SDK to correlate the response with the pending promise.
Fire-and-Forget Asynchronous Calls
For high-throughput scenarios, workers can invoke functions without awaiting responses by omitting the invocation_id field entirely.
Rust invoking a metrics handler (potentially Python or TypeScript):
iii.invoke_async(InvokeFunction {
function_id: "metrics::track".into(),
data: serde_json::json!({
"event": "click",
"label": "signup"
}),
// No invocation_id → fire-and-forget
}).await;
The engine forwards the message to the appropriate worker (registered in any language) and immediately returns without awaiting a response, maximizing throughput for telemetry and logging workloads.
Why Cross-Language Interoperability Works
The iii engine achieves seamless polyglot function calls through four architectural principles:
-
Single source of truth: The
Messageenum inengine/src/protocol.rsserves as the authoritative schema. Each SDK implements thin wrappers around this definition, ensuring all workers speak the same wire protocol. -
JSON payloads: By standardizing on JSON for all data transfer, the engine avoids language-specific serialization formats. Primitive types, objects, arrays, and binary data (base64-encoded) travel unchanged between Python, TypeScript, and Rust runtimes.
-
Language-agnostic routing: The engine in
engine/src/engine/mod.rshandles only connection management and frame routing. It inspectsfunction_idandinvocation_idfields but never deserializes thedatapayload, maintaining strict isolation between worker implementations. -
Typed SDK abstractions: While the wire format is uniform, each SDK provides idiomatic interfaces—
register_worker()in all three languages hide WebSocket management, frame serialization (serde_jsonin Rust, Python'sjsonmodule, JavaScript'sJSON.stringify), and promise handling.
Summary
- The iii engine defines a unified
Messageenum inengine/src/protocol.rsthat all SDKs use for WebSocket communication. - Workers register functions via
RegisterFunctionmessages, and the engine stores these mappings inengine/src/engine/mod.rs. - Cross-language function invocation occurs when the engine routes
InvokeFunctionframes to workers based onfunction_id, regardless of implementation language. - Synchronous calls use
invocation_idto correlate requests and responses, whileinvoke_asyncprovides fire-and-forget semantics. - JSON payloads ensure data remains interpretable across Python, TypeScript, and Rust without translation layers.
Frequently Asked Questions
How does the iii engine route function calls between different programming languages?
The engine maintains a registry mapping function_id strings to active WebSocket connections. When a worker calls invokeFunction, the engine looks up the target function_id, retrieves the associated worker's connection from its internal state (managed in engine/src/engine/mod.rs), and forwards the JSON frame unchanged. Because the payload format is identical across all SDKs, the receiving worker deserializes the message using its native JSON library and executes the handler.
What happens if a cross-language function invocation fails?
When a registered handler throws an exception or returns an error, the target worker's SDK catches the failure and serializes an InvocationError frame containing the error message and stack trace. The engine routes this frame back to the original caller using the invocation_id. If the calling SDK is TypeScript, the promise rejects; if Python, it raises an exception; if Rust, it returns an Err variant—each SDK translates the protocol-level error into idiomatic language constructs.
Can a Python worker synchronously invoke a Rust function and receive complex data structures?
Yes. Synchronous invocation works identically regardless of language direction. A Python worker can call iii.invoke_function() targeting a Rust-registered function_id, passing JSON-serializable dictionaries. The Rust handler receives the data as serde_json::Value or a typed struct, processes it, and returns a serializable response. The iii engine routes the InvocationResult frame back to Python, where the SDK decodes the JSON into native Python objects (dicts, lists, primitives).
Does the iii WebSocket protocol support binary data transfer between languages?
Yes, though JSON is the primary format, binary data can be transferred by encoding it as base64 strings within the JSON payload. Since all three SDKs (Python's base64, JavaScript's Buffer, and Rust's base64 crates) handle this encoding consistently, workers can exchange images, files, or serialized protobuf messages across language boundaries while remaining compliant with the Message enum protocol defined in engine/src/protocol.rs.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →