Skip to main content

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

Project description

Callwire

Callwire v2.1.0: High-performance, bidirectional RPC across 5 languages (Go, Python, Rust, TypeScript, Java) — over raw TCP with MessagePack framing. Roadmap: 12+ languages.

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 .proto codegen)
  • Bidirectional — clients and servers call each other over the same socket
  • 5 languages shipped — Go, Python, Rust, TypeScript, Java (all 4 patterns). Roadmap: C, C++, COBOL, C#, Kotlin, Swift, Ruby, and more
  • v3 Orchestration — one callwire.toml spawns 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 { return a + b })

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

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

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

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

import dev.callwire.core.*;

// 1. Export a function
Server server = new Server();
server.export("add", args -> {
    long a = ((Number) args.get(0)).longValue();
    long b = ((Number) args.get(1)).longValue();
    return a + b;
});
server.serve("localhost", 9090);

// 2. Call a remote function
Client client = new Client();
client.connect("localhost", 9090);
Object result = client.call("add", Arrays.asList(10L, 20L)); // 30

Installation & Publishing

  • npm: npm install @emaad-ansari/callwire (v2.1.0)
  • PyPI: pip install callwire==2.1.0
  • Cargo: cargo add callwire --version 2.1.0
  • Maven Central: dev.callwire:callwire:2.1.0

All packages auto-publish via CI workflow on version bump.

Orchestration (v2)

Workers are auto-discovered by the callwire init CLI and declared in callwire.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:

# 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:

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

FastAPI integration

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 — dynamic module import
from callwire import add
result = add(10, 20)  # routed transparently via registry
// 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 — 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{
    CertPem: cert,
    KeyPem:  key,
})

// Go — TLS client (with optional mTLS)
client, _ := callwire.ConnectWithReconnectTLS("localhost:9090", callwire.TLSConfig{
    CAPem: caCert,
})
# Python — TLS client
client.connect("localhost", 9090, tls={
    "cafile":   "ca.pem",
    "certfile": "client.pem",  # mTLS
    "keyfile":  "client.key",  # mTLS
})
// Rust — TLS client
let client = callwire::TlsConfig { ca_pem: Some(ca_pem), ..Default::default() }
    .connect("127.0.0.1:9090").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

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

# 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


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 12+ (Go, Python, Rust, TS, Java, C, C++, COBOL, C#, Kotlin, Swift, 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 12+ runtimes including a dedicated C# SDK.

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:

  1. Zero-schema across 12+ languages — no other library lets you export a function in Go/Python/Rust/TS/Java/C/C++/COBOL/C#/Kotlin/Swift/Ruby and call it from any of the others without a schema definition or codegen step. Same zero-schema wire format everywhere.

  2. All 4 gRPC patterns, zero-config — unary, server-streaming, client-streaming, bidi-streaming all supported. No .proto files, no codegen. Export a function that streams; it works from any language.

  3. 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.

  4. Built-in orchestrationcallwire init auto-detects workers across all languages from a single config file. Competitors require external process managers (supervisord), Kubernetes, or shell scripts.

  5. Bidirectional symmetry — the same socket serves both client and server roles. Only protosocket offers this; gRPC, Thrift, Cap'n Proto enforce client/server roles.

  6. Protocol simplicity — 4-byte length prefix + MessagePack. Full spec fits on one page (SPEC.md). Implementing from scratch takes hours, not weeks.

  7. 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.

  8. Per-language CLI — each SDK ships its own callwire init with zero cross-language build dependencies.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

callwire-2.1.0.tar.gz (21.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

callwire-2.1.0-py3-none-any.whl (24.3 kB view details)

Uploaded Python 3

File details

Details for the file callwire-2.1.0.tar.gz.

File metadata

  • Download URL: callwire-2.1.0.tar.gz
  • Upload date:
  • Size: 21.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for callwire-2.1.0.tar.gz
Algorithm Hash digest
SHA256 68e16bdeb468eebfb3c7f52691417518ec957baffe8cfb2b6307dc85ade4b381
MD5 29af27d63cef7510cfef32ba02b3a862
BLAKE2b-256 945532eb8e4abad3501768cd4ea6a18b88d3bf9b3733a5426697fe146845f7f9

See more details on using hashes here.

File details

Details for the file callwire-2.1.0-py3-none-any.whl.

File metadata

  • Download URL: callwire-2.1.0-py3-none-any.whl
  • Upload date:
  • Size: 24.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for callwire-2.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bb5e9beb74d79cf23cba3f3ff77d42a19f16c0890195869db3a3f7ba19f183b0
MD5 40a00607c369a25434efc5bf821d734c
BLAKE2b-256 8547f84113fe357bf75490ec474283ec88adfb9a98b38df92772e7752158bcc4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page