callwire 2.0.1

High-performance bidirectional RPC over TCP with MessagePack framing — Go, Python, Rust interop
Documentation
# Callwire

**High-performance, bidirectional RPC across Go, Python, Rust, and TypeScript — over raw TCP with MessagePack framing.**

No schemas. No `.proto` files. No codegen. Export a function, call it from anywhere.

---

## Features

- **Zero-schema RPC** — export any function, call it from any language
- **Bidirectional** — clients and servers can call each other over the same socket
- **v2 Orchestration** — one `callwire.toml` spawns and connects workers automatically
- **Dynamic routing** — connect to a registry, call any function without knowing worker addresses
- **TLS & mTLS** — secure transport with optional client certificate auth
- **Batch API** — fire multiple calls concurrently over a single connection
- **Streaming** — server-side streaming via generators / `AsyncIterable`
- **Auto-reconnect** — exponential backoff on connection drops

---

## Quick Start

### Go

```go
import "github.com/emaad/callwire"

// Export a function
callwire.Export("add", func(a, b int) int { return a + b })

// Call a remote function
client, _ := callwire.Connect("localhost:9090")
result, _ := callwire.Ref[int](client, "add")(10, 20) // 30
```

### Python

```python
import callwire

# 1. Export a local function (makes it server-ready)
@callwire.export
def add(a, b):
    return a + b

# 2. Dynamic module import (connects & invokes dynamically)
from callwire import add

result = add(10, 20)  # 30
```

### Rust

```rust
use callwire::{Client, register_unary};

register_unary("add", |(a, b): (i64, i64)| Ok(a + b));

let client = Client::connect("127.0.0.1:9090").await?;
let result: i64 = client.import("add", &(10i64, 20i64)).await?; // 30
```

### TypeScript

```typescript
import { Server, remote } from 'callwire';

// 1. Export local function
const server = new Server();
server.export('add', ([a, b]) => (a as number) + (b as number));
await server.serve('0.0.0.0', 9090);

// 2. Call dynamically using the remote Proxy
const result = await remote.add(10, 20); // 30
```

---

## Orchestration (v2)

Workers are auto-discovered by the `callwire init` CLI and declared in `callwire.toml`:

```toml
[project]
name = "my-project"
version = "1.0.0"

[services.go-worker]
dev_cmd  = "cd go/callwire && go run examples/server.go"
prod_cmd = "./bin/go-worker"

[services.rust-worker]
dev_cmd  = "cd rust && cargo run --quiet --example my-worker"
prod_cmd = "./bin/rust-worker"
```

Generate it with any of the four native CLIs — they all produce the same output:

```bash
# Python
PYTHONPATH=python python3 -m callwire init

# Go
cd go/callwire && go run ./cmd/callwire/ init

# Rust
cargo run --manifest-path rust/Cargo.toml --bin callwire -- init

# TypeScript
npx tsx ts/src/cli.ts init
```

Then call `init()` — Callwire starts a registry, spawns workers, and routes everything automatically:

```python
import callwire

callwire.init()  # reads callwire.toml, spawns workers

# Import functions dynamically as if they were local!
from callwire import add, predict

res1 = add(15, 27)      # → routed to Go worker
res2 = predict("data")  # → routed to Rust worker

callwire.shutdown()
```

See the full demo → [examples/2_orchestrated/demo.py](examples/2_orchestrated/demo.py)

### FastAPI integration

```python
from contextlib import asynccontextmanager
from fastapi import FastAPI
import callwire

@asynccontextmanager
async def lifespan(app: FastAPI):
    await callwire.async_init()
    yield
    await callwire.async_shutdown()

app = FastAPI(lifespan=lifespan)
```

---

## Service Discovery & Dynamic Routing

Workers self-register with the registry. Clients connect once and call anything dynamically — no worker addresses needed.

```python
# Python — dynamic module import
from callwire import add
result = add(10, 20)  # routed transparently via registry
```

```rust
// Rust — connect to registry, route calls transparently
let client = callwire::Client::connect_registry("127.0.0.1:29000").await?;
let sum: i32 = client.import("add", &(10, 20)).await?;
```

```typescript
// TypeScript — connect to registry, route calls transparently
const client = new Client();
await client.connectRegistry('127.0.0.1', 29000);
const sum = await client.call<number>('add', [10, 20]);
```

For load-balancing across multiple workers of the same type, use `DiscoverPool`:

```go
pool, _ := callwire.NewDiscoverPool("127.0.0.1:29090", "my-service")
result, _ := callwire.DiscoverRef[string](pool, "say_hello")("World")
```

---

## TLS & mTLS

```go
// Go — TLS server
callwire.ServeWithTLS("0.0.0.0:9090", callwire.TLSConfig{
    CertPem: cert,
    KeyPem:  key,
})

// Go — TLS client (with optional mTLS)
client, _ := callwire.ConnectWithReconnectTLS("localhost:9090", callwire.TLSConfig{
    CAPem: caCert,
})
```

```python
# Python — TLS client
client.connect("localhost", 9090, tls={
    "cafile":   "ca.pem",
    "certfile": "client.pem",  # mTLS
    "keyfile":  "client.key",  # mTLS
})
```

```rust
// Rust — TLS client
let client = callwire::TlsConfig { ca_pem: Some(ca_pem), ..Default::default() }
    .connect("127.0.0.1:9090").await?;
```

```typescript
// TypeScript — TLS server
const server = new Server();
await server.serve('0.0.0.0', 9090, {
  cert: fs.readFileSync('server.pem', 'utf8'),
  key:  fs.readFileSync('server.key', 'utf8'),
});

// TypeScript — TLS client (skip verify for self-signed)
const client = new Client({ tls: { rejectUnauthorized: false } });
await client.connect('127.0.0.1', 9090);

// TypeScript — TLS client with CA verification + mTLS
const clientMTLS = new Client({ tls: {
  ca:   fs.readFileSync('ca.pem', 'utf8'),
  cert: fs.readFileSync('client.pem', 'utf8'),
  key:  fs.readFileSync('client.key', 'utf8'),
}});
await clientMTLS.connect('127.0.0.1', 9090);
```

---

## Streaming

```typescript
// TypeScript — server-side streaming
server.export('count_up', async function* ([n]) {
  for (let i = 1; i <= (n as number); i++) yield i;
});

for await (const chunk of client.callStream<number>('count_up', [5])) {
  console.log(chunk); // 1, 2, 3, 4, 5
}
```

---

## Examples

```
examples/
├── 1_standalone/   — One Go server, one client (Python / Rust / TypeScript)
└── 2_orchestrated/ — One command spawns Go + Rust workers automatically
```

→ [examples/README.md](examples/README.md)

---

## Configuration

| Env Var | Default | Description |
|---|---|---|
| `CALLWIRE_HOST` | `localhost` | Default hostname for auto-serving & clients |
| `CALLWIRE_PORT` | `9090` | Default port |
| `CALLWIRE_AUTO` | `1` | Set to `0` to disable auto-server on Export |
| `CALLWIRE_REGISTRY` | *(set by orchestrator)* | Registry address for worker mode |
| `CALLWIRE_SPAWNED` | *(set by orchestrator)* | `1` when running as a managed worker |

---

## Running Tests

```bash
# Go
cd go/callwire && go test -v ./...

# Python
cd python && .venv/bin/python3 -m unittest discover -s . -p "test_*.py"

# Rust
cd rust && cargo test -- --nocapture

# TypeScript
cd ts && npm test
```

---

## Wire Protocol

Callwire uses a simple, fully-specified binary protocol — implement it in any language.  
→ [SPEC.md](SPEC.md)

---

## Performance

`~33 µs` per round-trip · `~81K calls/sec` on a single connection · **1.3–1.7× faster than gRPC** for unary workloads on Apple M4.

| Metric | Callwire | gRPC | Δ |
|--------|----------|------|---|
| Latency — noop | **32.7 µs** | 57.7 µs | 1.76× faster |
| Latency — add(a, b) | **34.6 µs** | 58.8 µs | 1.70× faster |
| Throughput (10 workers) | **80K calls/sec** | 49K calls/sec | 1.65× faster |
| Throughput (100 workers) | **81K calls/sec** | 62K calls/sec | 1.30× faster |

Full breakdown → [benchmarks/compare_grpc.md](benchmarks/compare_grpc.md)

---

## How It Compares

### vs gRPC

| Dimension | Callwire | gRPC |
|-----------|----------|------|
| **Schema** | None — export any function | Required `.proto` files + codegen |
| **Latency (noop)** | **32.7 µs** | 57.7 µs |
| **Throughput** | **81K calls/sec** | 62K calls/sec |
| **Transport** | Raw TCP (4-byte length + msgpack) | HTTP/2 + HPACK |
| **Bidirectional** | Same socket, any order | HTTP/2 streams (half-duplex per stream) |
| **Orchestration** | Built-in `callwire.toml` + `init()` | External (Kubernetes, Consul, etc.) |
| **Languages** | Go, Python, Rust, TypeScript | 11+ languages |
| **Streaming** | Server-side (generators) | Unary + server + client + bidi |
| **Browser** | No | Yes (gRPC-Web) |
| **Ecosystem** | Minimal | Envoy, gRPC-Gateway, health probes, reflection |

**When to pick Callwire:** services in supported languages, especially polyglot stacks (Go+Python+Rust+TS), where developer velocity matters more than formal API contracts.

**When to pick gRPC:** cross-org APIs, browser clients, languages Callwire doesn't support, existing gRPC infrastructure.

### vs protosocket (Momento)

Rust-only TCP RPC framework (v1: 100KHz, sub-ms p99.9). Callwire has protosocket beat on language coverage (4 runtimes vs 1) and built-in orchestration. protosocket is faster per-core for pure Rust workloads and has production battle-testing at Momento scale.

### vs ZeroRPC / Zero (zeroapi)

Python MessagePack-over-ZeroMQ RPC. Zero hits ~100K req/s on TCP but is Python-only and has a hard `gevent` dependency. Callwire matches that throughput **in every language** and adds TLS, streaming, orchestration, and cross-language interop.

### vs MagicOnion (C#)

MessagePack-over-gRPC for .NET/Unity. Shares Callwire's zero-schema philosophy (C# interfaces instead of `.proto`) but is C#-only and inherits gRPC's HTTP/2 overhead. Callwire is 1.3–1.7× faster on wire latency and spans 4 runtimes.

### vs Cap'n Proto RPC

Zero-copy RPC with time-travel (promise pipelining). Extremely fast deserialization, but requires `.capnp` schemas and supports only 6 languages. Callwire has no schema, wider language coverage, and built-in orchestration.

### vs Apache Thrift

Mature, 20+ language RPC with multiple transports. Requires `.thrift` schemas + codegen, no streaming. Callwire is simpler to set up and faster for the languages it supports.

### vs NPRPC

Feature-rich multi-transport RPC (TCP/WS/HTTP3/QUIC/SharedMemory) for C++/TS/Swift with FlatBuffers. Strong where Callwire doesn't go (C++, browsers, QUIC), but requires `.npidl` schemas and code generation. No orchestration layer.

---

## Moat

Callwire's defensible advantages:

1. **Zero-schema across 4 runtimes** — no other library lets you export a function in Go/Python/Rust/TS and call it from any of the others without a schema definition or codegen step.

2. **Built-in orchestration**`callwire init` auto-detects workers across all languages from a single config file. Competitors require external process managers (supervisord), container orchestration (Kubernetes), or bespoke shell scripts.

3. **Bidirectional symmetry** — the same connection serves both client and server roles. Only protosocket offers this at the transport level; gRPC, Thrift, and Cap'n Proto enforce client/server roles at the API level.

4. **Protocol simplicity** — 4-byte length prefix + MessagePack. The entire spec fits on one page ([SPEC.md]SPEC.md). Implementing from scratch takes hours, not weeks. This is the opposite of HTTP/2 (gRPC), which requires thousands of lines of HPACK, flow control, and stream multiplexing.

5. **Per-language CLI** — each SDK ships its own `callwire init` so there's zero cross-language dependency at build or runtime.

6. **Polyglot performance** — MessagePack encoding is fast in every runtime. Callwire doesn't optimize for a single language at the expense of others; the framing layer is simple enough that every language gets near-native serialization.