# How the karukan-imserver JSON-RPC 2.0 Protocol Facilitates macOS Frontend Communication

> Discover how the karukan-imserver JSON-RPC 2.0 protocol simplifies macOS frontend communication. Learn how it enables type-safe language interoperability between Swift and Rust.

- Repository: [Hitoshi Togasaki/karukan](https://github.com/togatoga/karukan)
- Tags: how-to-guide
- Published: 2026-07-03

---

**The karukan-imserver JSON-RPC 2.0 protocol enables macOS frontend communication through a lightweight, newline-delimited JSON-RPC 2.0 channel over stdin and stdout, allowing the Swift-based Input Method Kit (IMK) extension to synchronously invoke Rust engine methods while maintaining type safety across language boundaries.**

The karukan input method engine separates its high-performance Rust core from the macOS frontend through a well-defined IPC mechanism. According to the togatoga/karukan source code, the `karukan-imserver` binary exposes a JSON-RPC 2.0 interface that the Swift-based Input Method Extension uses to process keystrokes, manage candidates, and handle composition states without blocking the main thread.

## Process Spawning and Transport Layer

The macOS frontend initiates communication by spawning `karukan-imserver` as a child process via `EngineProcess`. The transport layer uses **newline-delimited JSON-RPC 2.0** messages exchanged over the child process's *stdin* and *stdout* streams.

The Swift side writes JSON-RPC request objects to the server's *stdin* and reads response lines from *stdout*. Each line represents a complete JSON-RPC message, while any text written to *stderr* is treated as log output only. This unidirectional flow ensures that the Rust engine can operate independently while the Swift frontend manages the Input Method Kit lifecycle.

*Source:* [`karukan-macos/Sources/KarukanIME/EngineClient.swift`](https://github.com/togatoga/karukan/blob/main/karukan-macos/Sources/KarukanIME/EngineClient.swift) lines 3-8, 31-33.

## JSON-RPC 2.0 Message Envelope

The protocol implements the standard JSON-RPC 2.0 envelope structure containing `jsonrpc`, `id`, `method`, `params`, `result`, and `error` fields. On the Rust side, these structures are defined in [`karukan-im/src/server/protocol.rs`](https://github.com/togatoga/karukan/blob/main/karukan-im/src/server/protocol.rs) as `Request`, `Response`, and `RpcError` structs.

According to the source code at lines 38-55, the envelope guarantees that every request receives a matching response with the same identifier, enabling asynchronous dispatch while preserving call ordering. The `jsonrpc` field is always set to `"2.0"` to indicate protocol compliance.

## Method Catalogue and API Surface

The server exposes a minimal, purpose-built API surface designed specifically for IME operations. The following methods define the contract between the macOS frontend and the Rust engine:

| Method | Parameters | Return Type |
|--------|------------|-------------|
| `init` | `{}` | `InitResult` |
| `process_key` | `ProcessKeyParams` | `KeyResult` |
| `select_candidate` | `SelectCandidateParams` | `KeyResult` |
| `commit` | `{}` | `KeyResult` |
| `reset` | `{}` | `{}` |
| `set_surrounding_text` | `SurroundingTextParams` | `{}` |
| `save_learning` | `{}` | `{}` |
| `status` | `{}` | `StatusResult` |

The Rust definitions for these parameter and result types reside in the same protocol file. For example, `ProcessKeyParams` (lines 101-110) captures key symbols and modifier states, while `InitResult` (lines 126-131) returns the protocol version and engine configuration.

## Swift Protocol Mirror

The macOS frontend maintains type safety through a Swift mirror of the Rust protocol defined in [`EngineProtocol.swift`](https://github.com/togatoga/karukan/blob/main/EngineProtocol.swift). All structs conform to `Decodable` (and `Encodable` where needed) and use automatic **snake_case-to-camelCase** conversion via a custom `JSONDecoder`.

The `makeProtocolDecoder` function at line 75 configures the decoder to convert snake_case field names from the wire format into Swift's camelCase convention. This ensures that the Swift structs match the literal JSON field names sent by the Rust server while maintaining Swift naming conventions.

*Source:* [`karukan-macos/Sources/KarukanIME/EngineProtocol.swift`](https://github.com/togatoga/karukan/blob/main/karukan-macos/Sources/KarukanIME/EngineProtocol.swift) lines 1-30.

## Request Lifecycle and Synchronization

The `EngineClient` class manages the complete request lifecycle through two primary mechanisms: sending and response handling.

**Sending requests:** The `sendRequest` method constructs a JSON object containing `"jsonrpc":"2.0"`, a monotonically increasing `"id"`, the `"method"` name, and a `"params"` dictionary. The object is serialized, appended with a newline (`0x0A`), and written to the server's stdin.

**Reading responses:** The `startReaderLoop` reads arbitrary chunks from stdout, splits on the newline byte (`0x0A`), and passes each line to `handleResponse`. The response JSON is parsed, the `"id"` field locates the pending completion closure, and the `"result"` object is re-encoded and returned to the caller.

*Source:* [`EngineClient.swift`](https://github.com/togatoga/karukan/blob/main/EngineClient.swift) sections 31-34 (sending) and 66-92 (reading).

### Synchronous vs. Asynchronous Operations

The protocol supports both blocking and non-blocking call patterns:

- **Synchronous key processing:** The `processKeySync` method blocks using a `DispatchSemaphore` because the Input Method Kit's `handle` callback must immediately return whether the key was consumed. This method sends the request, waits for the response, and returns the decoded `KeyResult`.

- **Fire-and-forget actions:** Operations like `save_learning` or `set_surrounding_text` use `sendRequest` with no-op completions, allowing the engine to process these updates asynchronously without blocking the UI.

## Version Safety and Cross-Language Type Safety

The protocol includes a **versioning guard** to prevent incompatible frontend-engine combinations. During initialization, the server returns a `protocol_version` field in the `InitResult`. The Swift client checks this against the expected `PROTOCOL_VERSION` constant (currently `1`) defined in Rust.

If the versions mismatch, the client can abort the connection, preventing subtle serialization errors when the wire format changes. This mechanism ensures that updates to either the Rust engine or Swift frontend maintain backward compatibility or fail explicitly.

The project maintains a **single source of truth** by keeping the canonical type definitions in [`protocol.rs`](https://github.com/togatoga/karukan/blob/main/protocol.rs) and manually mirroring them in [`EngineProtocol.swift`](https://github.com/togatoga/karukan/blob/main/EngineProtocol.swift). Continuous integration tests in [`EngineProtocolTests.swift`](https://github.com/togatoga/karukan/blob/main/EngineProtocolTests.swift) verify that the Swift decoder can correctly parse real `InitResult` JSON payloads, catching drift between the language implementations.

## Code Example

The following Swift code demonstrates typical interactions with the karukan-imserver JSON-RPC 2.0 protocol:

```swift
// Initialize the engine (sent automatically on server restart)
client.initAsync()

// Send a key event synchronously (required for IMK handle:)
let keyEvent = EngineKeyEvent(
    keysym: 0x0061, 
    modifiers: .init(shift: false, control: false, alt: false, super: false)
)
if let result = client.processKeySync(keyEvent) {
    // result.actions contains UI updates (preedit, candidates, etc.)
    applyEngineActions(result.actions)
}

// Commit the current composition
if let commitResult = client.commitSync() {
    applyEngineActions(commitResult.actions)
}

// Update surrounding text (inform the engine about external edits)
client.setSurroundingTextAsync(text: fullText, cursorPos: caret)

```

## Summary

- The **karukan-imserver JSON-RPC 2.0 protocol** uses newline-delimited messages over stdin/stdout to bridge the Swift macOS frontend and Rust IME engine.
- **Message envelopes** follow the JSON-RPC 2.0 standard with monotonic IDs for request-response correlation.
- The API surface includes seven primary methods: `init`, `process_key`, `select_candidate`, `commit`, `reset`, `set_surrounding_text`, and `save_learning`.
- **Type safety** is maintained through mirrored Rust and Swift structs with automatic case conversion.
- **Synchronization** supports both blocking calls (for key processing) and asynchronous operations (for state updates).
- **Version guards** prevent incompatible frontend-engine combinations by validating `protocol_version` during initialization.

## Frequently Asked Questions

### How does the karukan-imserver JSON-RPC 2.0 protocol handle concurrent requests?

The protocol handles concurrency through a monotonic ID system where each request receives a unique identifier. The `EngineClient` maintains a dictionary of pending completion closures keyed by these IDs. When `startReaderLoop` parses a response, it matches the response ID to the pending request and dispatches the result on the appropriate queue. This allows multiple asynchronous operations to be in flight while ensuring that synchronous calls like `processKeySync` can block using a `DispatchSemaphore` without deadlocking the response reader.

### What transport mechanism does the karukan-imserver use for JSON-RPC communication?

The transport mechanism uses **standard input/output streams** rather than network sockets or pipes. The Swift frontend spawns `karukan-imserver` as a child process and writes JSON-RPC messages followed by newline characters (`0x0A`) to the child's stdin. Responses are read line-by-line from stdout, with each line representing a complete JSON object. This approach eliminates network overhead and firewall concerns while providing a reliable, byte-stream-based communication channel between the macOS Input Method Kit extension and the Rust engine.

### Why does the protocol require synchronous key processing for certain operations?

Synchronous processing is required because the **Input Method Kit (IMK)** framework mandates immediate responses from the `handle` callback to determine whether a keystroke was consumed by the IME. When a user types, macOS calls the input method's `handle` method, which must return a boolean indicating consumption before the next event processes. The `processKeySync` method uses a `DispatchSemaphore` to block the calling thread until the Rust engine returns a `KeyResult`, ensuring the IME can correctly intercept or pass through keystrokes without introducing input latency or event reordering.

### How does the protocol prevent version mismatches between the Swift frontend and Rust backend?

The protocol implements a **version handshake** during the `init` method call. The Rust server returns a `protocol_version` field (currently hardcoded to `1` in the `PROTOCOL_VERSION` constant) within the `InitResult` struct. The Swift client compares this value against its own expected version constant and can terminate the connection if they differ. This verification occurs before any other API calls, ensuring that structural changes to JSON payloads or method signatures do not cause silent deserialization failures or undefined behavior in the IME.