# How the Karukan Swift/InputMethodKit Frontend Communicates with the Engine Process

> Learn how the Karukan Swift InputMethodKit frontend communicates with its Rust engine using JSON-RPC over stdio. Understand the core communication mechanism for this macOS input method.

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

---

**The Karukan macOS frontend communicates with its Rust engine process via JSON-RPC messages piped over standard input and standard output streams.**

The Karukan input method editor (IME) for macOS separates its user-facing Swift/InputMethodKit frontend from the heavy-duty conversion logic written in Rust. This architecture requires a robust inter-process communication mechanism that keeps the UI responsive while handling complex kana-kanji conversions. The solution implemented in the `togatoga/karukan` repository uses a lightweight JSON-RPC protocol tunneled through stdin/stdout pipes, eliminating the need for custom sockets or complex IPC frameworks.

## Process Spawning and Pipe Management

The communication channel begins with the **EngineProcess** class in [`karukan-macos/Sources/KarukanIME/EngineProcess.swift`](https://github.com/togatoga/karukan/blob/main/karukan-macos/Sources/KarukanIME/EngineProcess.swift). This class encapsulates the lifecycle management of the `karukan-imserver` binary, which is the Rust-based conversion engine.

When the IME loads, `EngineProcess` initializes a `Process` instance pointing to the bundled server binary at `Bundle.main.bundlePath + "/Contents/MacOS/karukan-imserver"`. It attaches two `Pipe` objects to the child process: `stdinPipe` for sending requests and `stdoutPipe` for receiving responses. This configuration creates a straightforward byte stream interface where the Swift frontend writes JSON to the engine's standard input and reads JSON from its standard output.

The class implements intelligent restart logic through a termination handler that monitors the child process exit status. If `shouldRestart` is set to true, the system schedules an exponential backoff restart sequence. Additionally, `EngineProcess` exposes an `onRestart` closure that the JSON-RPC client uses to reattach its reader loop after the engine process respawns, ensuring seamless recovery from crashes without user intervention.

## JSON-RPC Client Implementation

With the pipes established, **EngineClient** in [`karukan-macos/Sources/KarukanIME/EngineClient.swift`](https://github.com/togatoga/karukan/blob/main/karukan-macos/Sources/KarukanIME/EngineClient.swift) implements the actual JSON-RPC protocol layer. This class owns the file handles from `EngineProcess` and manages all message serialization and deserialization.

The client runs a background read loop on a dedicated dispatch queue that continuously monitors `stdoutPipe.fileHandleForReading` for incoming JSON messages. For **synchronous operations** such as `process_key`, the client encodes a JSON-RPC request containing an `id`, `method`, and `params`, writes it to `stdinPipe.fileHandleForWriting`, then blocks on a `DispatchSemaphore` until the matching response arrives. This synchronous blocking is acceptable because the read loop operates on a background queue, keeping the main thread responsive to macOS IMK events.

For **fire-and-forget notifications** like `log_event` or `commit_candidate`, the client sends the JSON payload without awaiting a response, allowing the frontend to notify the engine of state changes without blocking the input loop.

```swift
// Example: Send a key event synchronously
func handleKey(_ event: NSEvent, client: EngineClient) {
    let request = ProcessKeyRequest(
        keyCode: event.keyCode,
        modifiers: event.modifierFlags.rawValue
    )
    do {
        let response: EngineResult = try client.request(
            method: "process_key",
            params: request,
            responseType: EngineResult.self
        )
        // Update pre-edit text or candidate list from response
    } catch {
        NSLog("KarukanIME: JSON-RPC error – \(error)")
    }
}

// Example: Fire-and-forget notification
client.notify(method: "commit_candidate", params: CommitInfo(candidateId: selectedId))

```

## Message Protocol and Data Structures

To ensure type safety across the Swift/Rust boundary, **EngineProtocol.swift** defines the shared message schema. This file contains Swift structs that mirror the Rust-side definitions for `Request`, `Response`, and `Notification` types, along with a shared `protocol_version` constant.

By maintaining identical JSON shapes in both languages, the system avoids serialization mismatches. The protocol defines specific method strings such as `process_key` for conversion requests and `commit_candidate` for selection events, with corresponding parameter structs that encode key codes, modifier flags, and candidate identifiers.

## IMK Controller Integration

The final layer connects these transport mechanisms to macOS's InputMethodKit framework. **KarukanInputController.swift** holds an `EngineClient` instance and acts as the bridge between IMK events and engine calls.

When the user types a key, the controller receives the `NSEvent` and invokes `engineClient.processKey(event:)`. This method constructs a `ProcessKeyRequest`, sends it via the JSON-RPC channel, and receives an `EngineResult` containing pre-edit text, candidate lists, or commit strings. The controller then forwards these results to the appropriate IMK APIs to update the inline input buffer or display candidate windows.

This architecture cleanly separates concerns: the Swift layer handles only macOS UI events and IMK protocol compliance, while the Rust engine manages dictionary lookup, kana-kanji conversion, and learning cache updates.

## Summary

- **EngineProcess.swift** spawns the `karukan-imserver` binary and manages stdin/stdout pipes with automatic restart capability.
- **EngineClient.swift** implements JSON-RPC over these pipes, supporting both synchronous requests (using `DispatchSemaphore`) and asynchronous notifications.
- **EngineProtocol.swift** provides shared message definitions ensuring Swift/Rust compatibility.
- **KarukanInputController.swift** bridges IMK events to the RPC client, keeping the UI responsive while the engine handles heavy conversion tasks.
- All communication uses plain JSON over standard streams, requiring no custom sockets or complex IPC mechanisms.

## Frequently Asked Questions

### Why does Karukan use JSON-RPC over pipes instead of XPC or network sockets?

The pipe-based approach minimizes dependencies and avoids sandboxing complications common with XPC services. By using stdin/stdout, Karukan leverages standard POSIX streams that work reliably across process boundaries without requiring additional entitlements or socket management code. This design also simplifies debugging, as developers can intercept the JSON traffic by launching the engine manually in a terminal.

### How does the Swift frontend recover if the Rust engine process crashes?

The **EngineProcess** class monitors the child process through a termination handler. When the engine exits unexpectedly, the handler checks the `shouldRestart` flag and initiates an exponential backoff sequence to respawn the process. The `onRestart` closure notifies the **EngineClient** to reattach its reader loop to the new process's stdout pipe, restoring communication without requiring the user to restart the IME.

### What happens if a conversion request times out or the engine hangs?

Since the **EngineClient** uses a `DispatchSemaphore` to wait for synchronous responses, a hung engine would block the requesting thread indefinitely. In practice, the production implementation should include timeout logic on the semaphore wait or leverage the process monitoring in `EngineProcess` to detect unresponsive states and trigger a restart. The modular architecture allows the frontend to degrade gracefully by clearing the pre-edit buffer if the engine becomes unresponsive.

### Is the communication between the frontend and engine bidirectional?

While the primary flow is request-response from Swift to Rust, the protocol supports server-initiated **notifications** sent from engine to frontend. However, the current architecture primarily uses the stdout pipe for responses to client-initiated requests. True bidirectional streaming would require additional message routing logic in [`EngineClient.swift`](https://github.com/togatoga/karukan/blob/main/EngineClient.swift) to handle async server pushes separately from response correlation.