Callwire
Callwire v2.2.0: High-performance, bidirectional RPC across 9 languages (Go, Python, Rust, TypeScript, Java, C, C++, Swift, COBOL) — over raw TCP with MessagePack framing.
No schemas. No .proto files. No codegen. Export a function, call it from anywhere. All 4 gRPC streaming patterns (unary, server-streaming, client-streaming, bidirectional), zero config.
Features
- Zero-schema RPC — export any function, call it from any language
- All 4 gRPC patterns — unary, server-streaming, client-streaming, bidirectional-streaming (full parity with gRPC, no
.protocodegen) - Bidirectional — clients and servers call each other over the same socket
- 9 languages shipped, 8 at full parity — Go, Python, Rust, TypeScript, Java, C, C++, Swift all support all 4 patterns, both client and server. COBOL ships client + server for unary calls (its typical legacy-integration role). Roadmap: C#, Kotlin, Ruby
- v3 Orchestration — one
callwire.tomlspawns and connects workers automatically - Dynamic routing — connect to 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
- Auto-reconnect — exponential backoff on connection drops
Quick Start
Go
import "github.com/emaad/callwire"
// Export a function
callwire.Export("add", func(a, b int) int )
// Call a remote function
client, _ := callwire.Connect("localhost:9090")
result, _ := callwire.Ref[int](client, "add")(10, 20) // 30
Python
# 1. Export a local function (makes it server-ready)
return +
# 2. Dynamic module import (connects & invokes dynamically)
= # 30
Rust
use ;
register_unary;
let client = connect.await?;
let result: i64 = client.import.await?; // 30
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
Java
;
// 1. Export a function
Server server ;
server.;
server.;
// 2. Call a remote function
Client client ;
client.;
long result ; // 30
C
callwire_server_t *server = ;
;
;
// Client
callwire_client_t *client = ;
int64_t result;
; // 30
C++
callwire::Server ;
server.;
server.;
// Client
callwire::Client ;
int64_t result = client.; // 30
Swift
import Callwire
let server = try Server(host: "0.0.0.0", port: 9090)
try server.exportTyped("add") { (a: Int64, b: Int64) in a + b }
try server.serve()
// Client
let client = try Client(host: "localhost", port: 9090)
let result = try client.add(10, 20) // 30 — dynamic call, no .call("add", ...)
COBOL
Client + server, unary calls (COBOL's typical legacy-integration role — connecting to/from modern services with simple numeric/string payloads). Handlers are separate compiled subprograms registered by name:
*> Server: register a handler subprogram
CALL "callwire_cobol_export_int2" USING
BY VALUE WS-SERVER-PTR BY REFERENCE WS-FUNC-ADD
BY REFERENCE WS-PROG-ADD RETURNING WS-RC END-CALL.
*> Client: one CALL statement
CALL "callwire_cobol_call_ints" USING
BY VALUE WS-CLIENT-PTR BY REFERENCE WS-FUNC-ADD
BY REFERENCE WS-ARGS BY VALUE WS-ARGC
BY REFERENCE WS-INT-RESULT RETURNING WS-RC END-CALL.
Full setup → cobol/README.md
Installation
Published packages
- npm:
npm install @emaad-ansari/callwire(v2.2.0) - PyPI:
pip install callwire==2.2.0 - Cargo:
cargo add callwire --version 2.2.0 - Maven Central:
dev.callwire:callwire:2.2.0
These auto-publish via CI on version bump.
Build from source (C, C++, Swift, COBOL)
These SDKs aren't on a package registry yet — build against this repo directly.
C — CMake, no external dependencies:
&& &&
&& &&
Produces libcallwire_core.{a,dylib} + the callwire CLI. #include "callwire.h", link against the static or shared lib.
C++ — header-only (cpp/include/callwire/callwire.hpp), links directly against the C core sources:
&& &&
&& &&
Swift — Swift Package Manager manifest exists (swift/Package.swift), but if swift build fails with a PackageDescription/Foundation SDK-mismatch error in your toolchain, use the bypass build script instead (see swift/README.md for why):
&&
COBOL — requires GnuCOBOL (brew install gnucobol / apt install gnucobol):
&&
Builds and runs both the import-side and export-side (COBOL-hosted server) round-trip tests automatically.
Orchestration (v2)
Workers are auto-discovered by the callwire init CLI and declared in callwire.toml:
[]
= "my-project"
= "1.0.0"
[]
= "cd go/callwire && go run examples/server.go"
= "./bin/go-worker"
[]
= "cd rust && cargo run --quiet --example my-worker"
= "./bin/rust-worker"
Generate it with any of the native CLIs — they all produce the same output:
# Python
PYTHONPATH=python
# Go
&&
# Rust
# TypeScript
# Java
&&
# C (also used by C++/Swift — they share the C core's CLI, no separate one)
&& && && &&
COBOL doesn't ship a callwire init — it's a client/server library (cobol/src/cobol_shim.c), not a standalone CLI tool, matching its scope as a legacy-integration SDK rather than an orchestrated worker.
Then call init() — Callwire starts a registry, spawns workers, and routes everything automatically:
# reads callwire.toml, spawns workers
# Import functions dynamically as if they were local!
= # → routed to Go worker
= # → routed to Rust worker
See the full demo → examples/2_orchestrated/demo.py
FastAPI integration
await
yield
await
=
Service Discovery & Dynamic Routing
Workers self-register with the registry. Clients connect once and call anything dynamically — no worker addresses needed.
# Python — dynamic module import
= # routed transparently via registry
// Rust — connect to registry, route calls transparently
let client = connect_registry.await?;
let sum: i32 = client.import.await?;
// 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:
pool, _ := callwire.NewDiscoverPool("127.0.0.1:29090", "my-service")
result, _ := callwire.DiscoverRef[string](pool, "say_hello")("World")
TLS & mTLS
// Go — TLS server
callwire.ServeWithTLS("0.0.0.0:9090", callwire.TLSConfig)
// Go — TLS client (with optional mTLS)
client, _ := callwire.ConnectWithReconnectTLS("localhost:9090", callwire.TLSConfig)
# Python — TLS client
// Rust — TLS client
let client = TlsConfig
.connect.await?;
// 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 — 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
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
# Go
&&
# Python
&&
# Rust
&&
# TypeScript
&&
# Java
&&
# C
&& && && && &&
# C++
&& && && && &&
# Swift
&&
# COBOL
&&
Wire Protocol
Callwire uses a simple, fully-specified binary protocol — implement it in any language.
→ 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
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 | 9 shipped (Go, Python, Rust, TS, Java, C, C++, Swift, COBOL), roadmap C#/Kotlin/Ruby | 11+ languages |
| Streaming | All 4 (unary, server, client, bidi) | All 4 |
| Browser | No | Yes (gRPC-Web) |
| Ecosystem | Minimal | Envoy, gRPC-Gateway, health probes, reflection |
When to pick Callwire: polyglot services, developer velocity over formal schemas, teams that want zero-config orchestration and legacy-system bridging (COBOL↔Go/Python/Rust in one wire protocol, no middleware).
When to pick gRPC: cross-org APIs, browser clients, extensive tooling ecosystem (reflection, health checks, gRPC-Gateway), mature production observability.
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 9 runtimes today, with a C# SDK on the roadmap.
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 (browsers, QUIC). But Callwire has C++/TS/Swift support (via C core ABI), no schema/codegen, and built-in orchestration. NPRPC's multi-transport is valuable where protocols vary; Callwire focuses on raw-TCP performance and simplicity.
Moat
Callwire's defensible advantages:
-
Zero-schema across 9 shipped languages — no other library lets you export a function in Go/Python/Rust/TS/Java/C/C++/Swift/COBOL and call it from any of the others without a schema definition or codegen step. Same zero-schema wire format everywhere. C#/Kotlin/Ruby on the roadmap.
-
All 4 gRPC patterns, zero-config — unary, server-streaming, client-streaming, bidi-streaming all supported. No
.protofiles, no codegen. Export a function that streams; it works from any language. -
Legacy-to-modern bridge — the only RPC framework connecting COBOL mainframes directly to Go/Rust/TS/Python/Java microservices over the same zero-schema wire protocol. No gateway layer, no middleware required.
-
Built-in orchestration —
callwire initauto-detects workers across all languages from a single config file. Competitors require external process managers (supervisord), Kubernetes, or shell scripts. -
Bidirectional symmetry — the same socket serves both client and server roles. Only protosocket offers this; gRPC, Thrift, Cap'n Proto enforce client/server roles.
-
Protocol simplicity — 4-byte length prefix + MessagePack. Full spec fits on one page (SPEC.md). Implementing from scratch takes hours, not weeks.
-
C core ABI — languages without hand-crafted SDKs can wrap the stable C ABI (
c/include/callwire.h). Swift, COBOL, and others depend on this frozen interface. Lowers barrier for adding new runtimes. -
Per-language CLI — each SDK ships its own
callwire initwith zero cross-language build dependencies.