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.
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.
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.
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.
| Benchmark | Payload | Latency/op | Throughput |
|---|---|---|---|
| PingLatency | - | ~16 µs | ~133k/s |
| EchoLatency | 64 B | ~15 µs | ~130k/s |
| EchoLatency | 1 KB | ~17 µs | ~122k/s |
| EchoLatency | 64 KB | ~140 µs | ~14k/s |
| EchoLatency | 256 KB | ~456 µs | ~4.3k/s |
| ConcurrentThroughput | 64 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.
08Quick Start
JavaScript / TypeScript
bash
npm install @lambertse/ibridgerC++ (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.sockThe 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.