--- name: kkrpc-interop description: Use when implementing kkrpc clients or servers in non-TypeScript languages, speaking the stable compact protocol, transports, and reference implementations in Go, Python, Rust, or Swift. version: 1.0.0 license: MIT metadata: author: kkrpc domain: cross-language-rpc tags: - rpc - interop - protocol - go - python - rust - swift compatibility: Works with any language that can parse JSON and implement stdio/WebSocket transports --- # kkrpc Language Interop Implement kkrpc clients and servers in non-JS languages by speaking the stable compact JSON protocol used by TypeScript kkrpc endpoints. Start with the default value-oriented protocol. Async iterable streaming and remote references are opt-in advanced protocols; implement them only when the interop boundary explicitly needs them. ## Reference Implementations | Language | Location | Transports | | -------- | ------------------------------ | ---------------- | | Go | `interop/go/kkrpc/` | stdio, WebSocket | | Python | `interop/python/kkrpc/` | stdio, WebSocket | | Rust | `interop/rust/src/` | stdio, WebSocket | | Swift | `interop/swift/Sources/kkrpc/` | stdio, WebSocket | ## Core Protocol Interop uses JSON records. Stdio transports send newline-delimited UTF-8 JSON. WebSocket transports send the same JSON in text frames. ### Request ```json { "t": "q", "id": "a1b2-c3d4", "op": "call", "p": ["math", "add"], "a": [1, 2] } ``` Fields: - `t`: message tag, always `q` for requests. - `id`: unique request identifier. - `op`: one of `call`, `get`, `set`, or `new`. - `p`: method or property path as string segments. - `a`: argument array for calls and constructors. - `v`: value for property writes. ### Response ```json { "t": "r", "id": "a1b2-c3d4", "v": 3 } ``` Errors use `e` with compact error fields: ```json { "t": "r", "id": "a1b2-c3d4", "e": { "n": "Error", "m": "Division by zero", "s": "optional stack" } } ``` ### Callback Invocation ```json { "t": "cb", "id": "callback-id", "a": ["progress", 50] } ``` ## Callback Argument Encoding When sending a callable argument, generate an ID, store the callable locally, and replace the argument with a marker object. ```json { "__kkrpc_next_arg__": "callback", "id": "callback-id" } ``` When receiving this marker, create a wrapper function. Invoking the wrapper sends `t: "cb"` with the marker ID and argument array. When receiving callback arguments from JS, unwrap value envelopes before invoking local callbacks. ```json { "__kkrpc_next_arg__": "value", "v": "actual value" } ``` Default callback invocation is fire-and-forget. If a non-JS implementation needs callback return values, object handles, or `releaseProxy()` behavior, it must also implement the `kkrpc/remote-refs` envelope and internal `op: "ref"` request flow. ## Opt-in Advanced Protocols `kkrpc/streaming` adds async iterable stream records: - `t: "sq"` for stream pull/return/throw control requests - `t: "sr"` for stream value/completion/error responses `kkrpc/remote-refs` adds explicit `proxy(value)` envelopes with `"__kkrpc_ref__": true` plus internal `op: "ref"` requests for apply/get/set/call/release. Do not assume these features are present when connecting to a default `kkrpc` endpoint. ## UUID Generation Use any ID generator with low collision risk. IDs only need to match requests and responses within one connection. ```python import uuid def generate_uuid() -> str: return str(uuid.uuid4()) ``` ## Transport Layer Every transport should provide these operations: ```text read() -> string? # Read one message, null/None if closed write(message) # Write one JSON string close() # Close connection ``` Stdio transports append `\n` and flush immediately. WebSocket transports send and receive text frames containing a single JSON record. ## Client Algorithm 1. Convert a dotted method name such as `math.add` to `p: ["math", "add"]`. 2. Encode arguments, replacing callables with callback marker objects. 3. Send a request record with `t: "q"`, unique `id`, `op`, `p`, and optional `a` or `v`. 4. Store a pending completion keyed by request ID. 5. In the read loop, resolve pending calls for `t: "r"` records. 6. In the read loop, dispatch `t: "cb"` records to stored callbacks. ## Server Algorithm 1. Read JSON messages from the transport. 2. Ignore records that are not `t: "q"` unless your server also stores callbacks. 3. Dispatch `op: "call"` by joining `p` into the registered method name. 4. Dispatch `op: "get"` and `op: "set"` against registered property handlers if supported. 5. Encode successful results as `{ "t": "r", "id": requestId, "v": result }`. 6. Encode failures as `{ "t": "r", "id": requestId, "e": { "n": name, "m": message } }`. ## Minimal Client Pseudocode ```python def call(method, *args): request_id = generate_uuid() payload = { "t": "q", "id": request_id, "op": "call", "p": method.split("."), "a": [encode_arg(arg) for arg in args], } transport.write(json.dumps(payload)) response = wait_for_response(request_id) if "e" in response: raise RpcError(response["e"].get("m", "RPC error")) return response.get("v") ``` ## Minimal Server Pseudocode ```python def handle(message): if message.get("t") != "q": return try: method = ".".join(message.get("p", [])) args = [decode_arg(arg) for arg in message.get("a", [])] result = api[method](*args) transport.write(json.dumps({"t": "r", "id": message["id"], "v": result})) except Exception as exc: transport.write(json.dumps({ "t": "r", "id": message["id"], "e": {"n": exc.__class__.__name__, "m": str(exc)}, })) ``` ## Testing Checklist - Call JS server methods from the non-JS client. - Call non-JS server methods from a JS client. - Verify nested method paths. - Verify property get and set if implemented. - Verify callback marker encoding and `t: "cb"` dispatch. - If implemented, verify streaming `t: "sq"`/`t: "sr"` flow control. - If implemented, verify remote-reference `op: "ref"` apply/get/set/call/release behavior. - Verify error responses preserve name and message. - Verify stdio newline framing and WebSocket text framing. ## Common Pitfalls - Do not send dotted method names in the wire message. Use path arrays in `p`. - Do not wrap responses in custom result objects. Put successful values in `v`. - Do not use binary WebSocket frames for the JSON interop protocol. - Do not forget to unwrap `{ "__kkrpc_next_arg__": "value", "v": ... }` callback arguments. - Do not rely on JS-specific transfer slots or structured clone in language interop. - Do not assume async iterables or remote references are part of the default core protocol. - Do not send `op: "ref"` to a default-core endpoint; only `kkrpc/remote-refs` endpoints implement those requests.