Projects
Infrastructure
iBridger
Active

iBridger

An IPC/RPC framework that bridges processes across language boundaries - a C++ server and a JavaScript client talking to each other over Unix domain sockets and Protocol Buffers, with no shared memory and no native bindings.
lambertse
December 1, 2025
Tech Stack
cplusplusC++17
typescriptTypeScript
protobuf
cmakeCMake
nodedotjsNode.js
Unix Domain Sockets
Source Code

01What is iBridger?

Imagine you have a C++ program that does the heavy computation, and a JavaScript app that displays the results. They are separate processes - different languages, different runtimes. How do they talk to each other? That is the problem iBridger solves.

iBridger is a lightweight IPC (Inter-Process Communication) and RPC (Remote Procedure Call) framework. You define a service with named methods. Your C++ process registers them. Your JavaScript process calls them. The framework handles everything in between: the connection, the serialization, the error propagation, and the reconnection if the server restarts.

There is no HTTP overhead, no TCP round-trip to localhost, and no network configuration. Communication happens over Unix domain sockets - a fast, file-system-based channel that two processes on the same machine share. The messages are encoded with Protocol Buffers, keeping payloads compact and strongly typed.


02The Problem: Cross-Language IPC Is Harder Than It Looks

Most IPC tutorials show two processes in the same language passing strings through a pipe. Real projects are not like that. You have a performance-critical backend written in C++, a scripting layer in Python or JavaScript, and they need to cooperate without one managing the other's memory or runtime.

The naive approaches all have sharp edges. Shared memory requires locking and crashes one process when the other misbehaves. HTTP to localhost works but adds hundreds of microseconds of overhead and requires a full HTTP server. gRPC is powerful but comes with a heavy build system dependency and code-generation pipelines.

iBridger occupies a different position: the wire protocol is just Unix sockets and Protobuf. Any language that can open a socket and read four bytes can implement a compatible SDK. The JavaScript SDK has no native bindings - it is pure TypeScript running on Node.js. The C++ SDK requires no internet dependency; protobuf can be provided by the system package manager.

🌐
Cross-Language by Design
C++ and JavaScript talk to each other over the same wire protocol. Any language that can open a socket and encode Protobuf can join the network.
~16 µs Latency
Unix domain sockets avoid the TCP stack entirely. Ping round-trips benchmark at ~16 µs, ~130k calls/s on Apple silicon.
🔁
Auto-Reconnect
Both SDKs detect disconnection immediately and retry with exponential backoff. Callers block transparently until the server recovers.
🧩
Typed RPC
Register methods with Protobuf request/response types. The framework handles serialization, correlation IDs, and error propagation.
🛡️
Built-in Health Check
Every server auto-registers a Ping service. Any client can verify liveness with a single call - no extra code.
📦
Zero Native Bindings
The JavaScript SDK is pure TypeScript using Node.js net module. No node-gyp, no native compilation - npm install and go.

03Architecture: Four Clean Layers

iBridger is structured as a four-layer stack. Each layer has a single responsibility, and each is independently replaceable. This is what makes adding a new language SDK straightforward - you reimplement the stack in your language, you do not fork a core.

Four-layer stack (bottom → top)
SDK
ServerBuilder · ClientStub (C++) · IBridgerServer · IBridgerClient (JS)
RPC
Service registry · method dispatch · request-ID correlation
Protocol
Length-prefixed framing · Protobuf Envelope codec
Transport
Unix Domain Socket (macOS/Linux) · Named Pipe (Windows, planned)
C++ SDK
JS/TS SDK
Go SDK (planned)
Python SDK (planned)

The Transport layer owns the socket. On macOS and Linux it is a Unix domain socket (AF_UNIX, SOCK_STREAM). On Windows it will be a Named Pipe. The transport knows nothing about messages; it just moves bytes.

The Protocol layer wraps those bytes in frames: a 4-byte big-endian length prefix followed by a serialized ibridger.Envelope protobuf. This layer knows nothing about services; it just produces and consumes envelopes.

The RPC layer receives envelopes, routes them to registered service handlers by name, and pairs responses back to the request that originated them using a monotonically incrementing request_id.

The SDK layer is the public API - the ergonomic wrapper that application code actually touches. In C++ that is ServerBuilder and ClientStub. In JavaScript it is IBridgerServer and IBridgerClient.


04The Wire Protocol: What Actually Travels Over the Socket

Every message, in every direction, follows the same binary format. This is what makes cross-language interop work - there is no language-specific encoding, just bytes that any conforming SDK can read.

Wire frame format
4 bytes
BE uint32
payload length
N bytes (≤ 16 MiB)
protobuf ibridger.Envelope
request_id · service · method · payload
Every language SDK speaks this exact format. A C++ server and a JS client interoperate because both decode the same Envelope bytes.

The receiver reads exactly 4 bytes to learn how many payload bytes follow, then reads exactly that many bytes. The payload is a serialized ibridger.Envelope protobuf containing the service name, method name, a request_id, and the serialized request or response body.

Responses mirror the request_id of the request that triggered them. This lets the client maintain a map of pending calls and resolve the right promise when the response arrives - even when multiple calls are in flight concurrently.

The maximum frame size is 16 MiB, enforced by both sides. A zero-length frame is valid and acts as a keepalive no-op. The spec also defines five status codes (OK, NOT_FOUND, INVALID_ARGUMENT, INTERNAL, TIMEOUT) so error handling is consistent across all SDKs.


05SDK APIs: Register Once, Call from Anywhere

Both SDKs follow the same mental model: define a service, register methods with typed request/response shapes, start the server. Clients connect and call methods by name. Everything else is handled.

JavaScript / TypeScript

Install from npm, no native compilation. The typedMethod helper binds Protobuf types to a plain async function.

typescript - server

import { IBridgerServer, typedMethod } from '@lambertse/ibridger' import { ibridger } from '@lambertse/ibridger' const server = new IBridgerServer({ endpoint: '/tmp/my.sock' }) server.register('EchoService', { Echo: typedMethod( ibridger.examples.EchoRequest, ibridger.examples.EchoResponse, async (req) => ({ message: req.message.toUpperCase() }) ) }) await server.start()

typescript - client

import { IBridgerClient } from '@lambertse/ibridger' import { ibridger } from '@lambertse/ibridger' const client = new IBridgerClient({ endpoint: '/tmp/my.sock' }) await client.connect() const resp = await client.call( 'EchoService', 'Echo', { message: 'hello' }, ibridger.examples.EchoRequest, ibridger.examples.EchoResponse ) console.log(resp.message) // "HELLO" client.disconnect()

C++

Subclass ServiceBase and register methods in the constructor. Wire up services with ServerBuilder. The ClientStub uses a template call that deserializes the response directly into your protobuf type.

c++ - service definition

class GreetService : public ibridger::sdk::ServiceBase { public: GreetService() : ServiceBase("GreetService") { register_method<GreetRequest, GreetResponse>( "Hello", [](const GreetRequest& req) { GreetResponse resp; resp.set_message("Hello, " + req.name() + "!"); return resp; }); } };

c++ - server startup

auto server = ibridger::sdk::ServerBuilder() .set_endpoint("/tmp/my.sock") .add_service(std::make_shared<GreetService>()) .build(); server->start(); ::pause(); // wait for SIGINT server->stop();

c++ - client call

ibridger::rpc::ClientConfig cfg; cfg.endpoint = "/tmp/my.sock"; // Auto-reconnect with exponential backoff ibridger::rpc::ReconnectConfig rc; rc.base_delay = std::chrono::milliseconds(200); rc.max_delay = std::chrono::milliseconds(10'000); rc.max_attempts = -1; // unlimited rc.on_reconnect = [] { std::cout << "reconnected\n"; }; cfg.reconnect = rc; ibridger::sdk::ClientStub stub(cfg); stub.connect(); GreetRequest req; req.set_name("world"); auto [resp, err] = stub.call<GreetRequest, GreetResponse>( "GreetService", "Hello", req); if (!err) std::cout << resp.message() << "\n"; // "Hello, world!"

06Resilient Connections: Survive Server Restarts

Long-running processes crash and restart. iBridger handles this transparently. Both SDKs detect disconnection the moment it happens (not on the next call) and fire a configurable onDisconnect callback.

When ReconnectConfig (C++) or a reconnect options object (JS) is provided, calls that fail because the server is down will block with exponential backoff - starting at a configurable base delay and capping at a maximum. From the caller's perspective, the call just takes a bit longer; no special error handling is needed as long as the server comes back within the backoff budget.

One important caveat is documented explicitly: only the send path is retried. If the socket dies after the server has already processed the request but before it sends a response, the call returns an error rather than silently retrying - because retrying would risk double-execution of a non-idempotent operation.


07Performance: What Unix Sockets Make Possible

By staying on Unix domain sockets and skipping the TCP/IP stack entirely, iBridger achieves latencies that HTTP-based IPC cannot match for same-machine communication. The numbers below are from a Debug build on Apple silicon - a Release build is significantly faster.

BenchmarkPayloadLatency/opThroughput
PingLatency-~16 µs~133k/s
EchoLatency64 B~15 µs~130k/s
EchoLatency1 KB~17 µs~122k/s
EchoLatency64 KB~140 µs~14k/s
EchoLatency256 KB~456 µs~4.3k/s
ConcurrentThroughput64 B, 4 threads~33 µs~60k/s (agg.)
ConnectionSetup-~12 µs~90k conn/s

Connection setup itself costs only ~12 µs. This means it is practical to create short-lived connections rather than maintaining a persistent one, though persistent connections are the recommended pattern for high-frequency call paths.


~16 µs
Ping latency
~130k
Calls/sec
2
Language SDKs
16 MiB
Max frame size

08Quick Start

JavaScript / TypeScript

bash

npm install @lambertse/ibridger

C++ (CMake 3.20+, C++17)

bash

cmake -B build -DIBRIDGER_BUILD_TESTS=ON -DIBRIDGER_BUILD_EXAMPLES=ON cmake --build build # Then in your CMakeLists.txt: find_package(ibridger CONFIG REQUIRED) target_link_libraries(my_app PRIVATE ibridger::sdk::cpp)

Cross-language demo (C++ server + JS client)

bash

# Terminal 1 - start the C++ echo server ./build/sdk/cpp/echo_server /tmp/ibridger.sock # Terminal 2 - call it from JavaScript npx ts-node examples/echo-client.ts /tmp/ibridger.sock

The health check built into every server is always available. Call client.ping() from JS, or use ClientStub to call ibridger.Ping/Ping from C++, to verify the server is alive before your first real call.


09Roadmap

C++ SDK
ServerBuilder, ClientStub, typed Protobuf methods, ReconnectConfig.
JavaScript / TypeScript SDK
Pure Node.js - IBridgerServer, IBridgerClient, typedMethod helpers, auto-reconnect.
Cross-language integration tests
C++ server ↔ JS client tested end-to-end; conformance enforced by the wire protocol spec.
Built-in Ping service
Every server auto-registers ibridger.Ping/Ping for health checks.
Windows Named Pipe transport
Unix domain sockets are macOS/Linux only. Named Pipe stub exists; full implementation pending.
Go SDK
Four-component checklist and a full Go SDK outline documented in docs/adding-a-language.md. Implementation planned.
Python SDK
Same wire protocol, asyncio transport, protobuf-generated types. On the roadmap.
Per-call deadline / timeout
Currently a global 30 s default. Per-call deadlines and cancellation will be added.
Streaming RPCs
Request–response only today. Server-side streaming (for progress feeds, log tails, etc.) is on the roadmap.

Bridge your processes
Define a service, register methods, connect from any language. Unix sockets, Protocol Buffers, no native bindings required.
Related Projects
NeoVim Pomodoro Timer
Pomodoro timer with IPC sync across Neovim instances - where the IPC idea was born
NeoVim Toggle Terminal
Floating multi-session terminal for Neovim
On this page
Loading...
N

Subscribe to my newsletter

Get notified when I publish new posts on my blog

© 2026 Minh-Tri Le. All rights reserved.