# How OpenLogi Uses tarpc and bincode for Cross-Platform IPC Over Local Sockets

> Discover how OpenLogi leverages tarpc and bincode for efficient cross-platform IPC. Learn about their Unix domain socket and named pipe implementation for seamless communication.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-09-06

---

**OpenLogi implements a unified IPC protocol where the GUI client and background agent communicate over Unix-domain sockets on macOS/Linux and named pipes on Windows, using tarpc for RPC framing and bincode for binary serialization.**

The OpenLogi source code demonstrates a clean abstraction for local inter-process communication (IPC) that works identically across operating systems. The tarpc/bincode IPC protocol powers all communication between the desktop interface and the agent process, with platform-specific socket types hidden behind a common transport layer.

## Architecture Overview: tarpc + bincode IPC in OpenLogi

The protocol stack consists of three layers working together:

- **tarpc** — Provides the RPC framework, request/response matching, and code generation via `#[tarpc::service]`
- **bincode** — Handles compact binary serialization of Rust data structures
- **interprocess** — Abstracts the underlying socket type (Unix domain sockets vs. Windows named pipes)

This design lets developers write RPC code once and run it on any supported platform without modification.

## Service Contract Definition

The RPC interface is defined in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs) using the tarpc service macro.

### Generated Client and Server Glue

The `#[tarpc::service]` annotation on the `Agent` trait produces:

- `AgentClient` — Used by GUI and CLI tools to call agent methods
- `serve` function — Connects the trait implementation to the transport in the agent binary

All methods follow strict request/response semantics. Even server-to-client pushes like `observe` are modeled as long-lived requests that the client initiates and keeps open.

### Version Negotiation

The contract includes a `protocol_version` method that returns a wire-format version number. Both client and server compare this against `crate::PROTOCOL_VERSION` (currently **29**) to verify compatibility before proceeding with other calls.

## Cross-Platform Transport Layer

The transport implementation in [`crates/openlogi-ipc/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/transport.rs) resolves platform differences at bind and connect time, then presents identical `Stream` and `Listener` types to the rest of the code.

### Endpoint Resolution

The `endpoint_name()` function returns an `interprocess::local_socket::Name`:

| Platform | Result |
|----------|--------|
| **Unix** (macOS/Linux) | Filesystem path: `$HOME/.config/openlogi/agent.sock` via `openlogi_core::paths::agent_socket_path()` |
| **Windows** | Named pipe: `\\.\pipe\openlogi-agent.sock` |

### Connection Establishment

**Server side (`bind()`)** — Creates a `Listener`. On Unix, it also creates parent directories for the socket file.

**Client side (`connect()`)** — Creates a `Stream` connected to the resolved endpoint. The same `Stream::connect` call works for both socket types due to the `interprocess` abstraction.

### Protocol Wrapping

The `wrap(stream)` function layers the RPC protocol over any raw stream:

```rust
pub fn wrap<S>(stream: S) -> Transport
where
    S: AsyncRead + AsyncWrite + Send + 'static,
{
    tarpc::serde_transport::Transport::from((
        stream,
        Bincode::default(),           // bincode serialization
        LengthDelimitedCodec::new(), // length-delimited framing
    ))
}

```

This identical wrapping runs on both platforms. The only variance is the underlying `Stream` implementation provided by `interprocess`.

## Client-Side Connection Flow

The client implementation in [`crates/openlogi-ipc/src/client.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/client.rs) establishes connections with a mandatory version handshake:

```rust
use openlogi_ipc::client::{self, ConnectError};
use openlogi_ipc::transport;

async fn start_agent_client() -> Result<(), ConnectError> {
    // Connect to the platform-specific socket/pipe
    let stream = transport::connect().await?;
    
    // Wrap with tarpc + bincode framing
    let transport = transport::wrap(stream);
    
    // Spawn the tarpc client
    let client = openlogi_ipc::AgentClient::new(
        client::Config::default(),
        transport,
    ).spawn();
    
    // Version handshake before any other calls
    let version = client
        .protocol_version(openlogi_ipc::context::current())
        .await?;
    
    println!("Agent protocol version: {}", version);
    Ok(())
}

```

The handshake ensures that GUI and agent binaries are compatible before exposing functionality to users.

## Server-Side Listening and Serving

The agent binary in [`crates/openlogi-agent/src/server.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/server.rs) binds the transport and serves requests:

```rust
use openlogi_ipc::transport;
use openlogi_ipc::Agent; // concrete trait implementation

async fn run_ipc_server() -> std::io::Result<()> {
    // Platform-specific listener (Unix socket or Windows pipe)
    let listener = transport::bind()?;
    
    // Accept and serve connections
    let server = tarpc::server::BaseChannel::with_config(
        listener,
        tarpc::server::Config::default(),
    );
    
    server.serve(Agent::new()).await?;
    Ok(())
}

```

Each accepted connection passes through `transport::wrap` inside the serving machinery, applying identical bincode framing before dispatching to the `Agent` implementation.

## Wire Stability Guarantees

The protocol maintains compatibility through several mechanisms:

- **Length-delimited framing** — `tokio_util::codec::LengthDelimitedCodec` prepends message lengths, so receivers know exact boundaries regardless of OS buffering behavior
- **Deterministic bincode encoding** — Field order and enum variant indices are serialized predictably
- **Immutable method ordering** — tarpc encodes method calls as enum variant indices; changing order breaks the wire format
- **Explicit versioning** — `PROTOCOL_VERSION` bumps on any breaking change

These guarantees allow CI pipelines to validate the "golden wire format" and catch accidental protocol changes.

## Summary

- The tarpc/bincode IPC protocol in OpenLogi abstracts Unix sockets and Windows named pipes through the `interprocess` crate
- Service contracts in [`ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/ipc.rs) generate type-safe client and server code
- [`transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/transport.rs) handles platform-specific bind/connect logic, then wraps streams with identical framing
- Mandatory version handshakes prevent incompatible client-agent pairs from communicating
- Length-delimited framing and stable bincode encoding ensure reliable cross-platform operation

## Frequently Asked Questions

### How does OpenLogi handle IPC differently on Windows versus Unix?

Both platforms use the same tarpc and bincode layers. The only difference is the underlying socket type: Unix-domain sockets on macOS/Linux and named pipes on Windows. The `interprocess` crate and the custom [`transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/transport.rs) module hide this difference, exposing identical `Stream` and `Listener` types to the RPC code.

### Why does OpenLogi use bincode instead of JSON or Protobuf?

Bincode provides compact, fast binary serialization with zero-copy deserialization where possible. Since both client and agent are Rust binaries built from the same source tree, bincode's Rust-centric design avoids schema files and external tooling while maintaining deterministic wire formats suitable for version compatibility checks.

### What happens if the GUI and agent have different protocol versions?

The client calls `protocol_version` immediately after connecting and compares the result against its own `PROTOCOL_VERSION` constant (currently 29). If they differ, the client can refuse to proceed, prompt for upgrade, or implement backward-compatible fallback logic. This prevents undefined behavior from mismatched method expectations.

### How does the length-delimited codec prevent message corruption?

`LengthDelimitedCodec` prefixes each serialized message with its byte length. The receiver reads this length first, then reads exactly that many bytes before attempting deserialization. This eliminates framing errors caused by partial reads, buffer coalescing, or platform-specific pipe behavior, ensuring bincode always receives complete, valid messages.