NPRPC is a high-performance, multi-transport RPC framework for distributed systems. It features a compact binary protocol with flat-buffer serialization, a type-safe IDL with code generation for C++, TypeScript, and Swift, and first-class streaming support (server, client, and bidirectional streams) over every transport.
- Multiple transports — WebSocket (WS/WSS), HTTP/HTTPS, HTTP/3, TCP, Shared Memory, QUIC, WebTransport
- Streaming RPC — server (
stream<T>), client (client_stream<T>), and bidi (bidi_stream<In,Out>) streams with C++20 coroutines - Type-safe IDL —
.npidl→ C++/TypeScript/Swift stubs withnpidl - Cross-language — seamless C++ ↔ TypeScript/JavaScript ↔ Swift interop
- Browser-first — WebSocket, HTTP, and WebTransport endpoints;
host.jsonbootstrap for static deployments - SSR support — built-in SvelteKit SSR via shared memory IPC (see docs/SSR_ARCHITECTURE.md)
- Cookie auth — httpOnly cookie-based auth for HTTP/WebSocket (see docs/HTTP_AUTH.md)
- POA — Portable Object Adapter for lifecycle management and session-scoped activation
- Nameserver — service discovery and named object binding
| Transport | Use Case | Notes |
|---|---|---|
| TCP | Native IPC, microservices | Lowest overhead, no browser support; optional io_uring backend (experimental) |
| WebSocket | Real-time, bidirectional | Persistent connection, streams supported; TLS via WSS |
| HTTP | Stateless web APIs | Browser-compatible, SSR-capable; TLS via HTTPS |
| HTTP/3 | Modern web | QUIC-based, SSR-capable; requires -DNPRPC_ENABLE_HTTP3=ON |
| WebTransport | Browser streaming | Multiplexed streams over HTTP/3; native stream mapping for stream<T> |
| QUIC | Native next-gen | Multiplexed, encrypted; requires -DNPRPC_ENABLE_QUIC=ON |
| Shared Memory | Same-machine IPC | Zero-copy in some cases; extremely low latency |
// calculator.npidl
module example;
exception CalculationError {
message: string;
code: i32;
}
interface Calculator {
f64 Add(a: in f64, b: in f64);
f64 Subtract(a: in f64, b: in f64);
f64 Multiply(a: in f64, b: in f64);
f64 Divide(a: in f64, b: in f64) raises(CalculationError);
}
npidl calculator.npidl --cpp --ts # C++ + TypeScript
npidl calculator.npidl --cpp --ts --swift # add SwiftGenerates calculator.hpp / calculator.cpp (C++) and calculator.ts (TypeScript).
#include <nprpc/nprpc.hpp>
#include "calculator.hpp"
class CalculatorImpl : public example::ICalculator_Servant {
public:
double Add(double a, double b) override { return a + b; }
double Subtract(double a, double b) override { return a - b; }
double Multiply(double a, double b) override { return a * b; }
double Divide(double a, double b) override {
if (b == 0.0) throw example::CalculationError{"Division by zero", 1};
return a / b;
}
};
int main() {
// Build RPC — chain transport builders, then call build()
auto* rpc = nprpc::RpcBuilder()
.set_log_level(nprpc::LogLevel::info)
.with_hostname("localhost")
.with_tcp(15000)
.with_http(8080)
.root_dir("./public")
.build();
// Create a POA
auto* poa = nprpc::PoaBuilder(rpc)
.with_max_objects(10)
.with_lifespan(nprpc::PoaPolicy::Lifespan::Persistent)
.build();
// Activate object — specify which transports it accepts
CalculatorImpl calc;
auto oid = poa->activate_object(
&calc,
nprpc::ObjectActivationFlags::tcp |
nprpc::ObjectActivationFlags::ws |
nprpc::ObjectActivationFlags::http
);
// Publish for browser clients (writes <root_dir>/host.json)
rpc->add_to_host_json("calculator", oid);
rpc->produce_host_json();
// Or register with the nameserver
auto ns = rpc->get_nameserver("localhost:15001");
ns->Bind(oid, "calculator");
rpc->run(); // blocks; use rpc->start_thread_pool(n) for async
return 0;
}import * as NPRPC from 'nprpc';
import * as example from './gen/calculator';
const rpc = await NPRPC.init();
const ns = NPRPC.get_nameserver('localhost:15001');
const ref = NPRPC.make_ref<NPRPC.ObjectProxy>();
await ns.Resolve('calculator', ref);
const calc = NPRPC.narrow(ref.value, example.Calculator);
console.log(await calc.Add(10, 20)); // 30
try {
await calc.Divide(10, 0);
} catch (e) {
if (e instanceof example.CalculationError)
console.error(`${e.code}: ${e.message}`);
}// host.json is served by the C++ server at /host.json
const host = await fetch('/host.json').then(r => r.json());
const calc = NPRPC.narrow(host.objects.calculator, example.Calculator);
// .http sub-proxy returns values directly
console.log(await calc.http.Add(10, 20)); // 30All three stream directions are supported at the IDL level and map to C++20 coroutines on the server side and range-based iteration / AsyncThrowingStream on clients.
interface FileServer {
// Server → Client (stream<T> is an alias for server_stream<T>)
stream<vector<u8>> DownloadFile(filename: in string);
// Client → Server (void reply after stream closes)
void UploadFile(filename: in string, data: client_stream<vector<u8>>);
// Bidirectional
bidi_stream<string, string> Chat(room: in string);
}
Optional in parameters before the stream keyword are sent in the handshake phase; the server can raise exceptions there before any data flows.
// server_stream — return a StreamWriter<T> coroutine
nprpc::StreamWriter<uint8_t>
FileServerImpl::DownloadFile(std::string_view filename) {
auto data = read_file(filename);
for (uint8_t byte : data)
co_yield byte;
}
// client_stream — StreamReader<T> delivered as a parameter
void FileServerImpl::UploadFile(
std::string_view filename,
nprpc::StreamReader<std::vector<uint8_t>>& data) {
std::ofstream out(std::string(filename), std::ios::binary);
for (auto& chunk : data) // blocking range-based for
out.write((char*)chunk.data(), chunk.size());
}// Server stream — range-based for loop
auto reader = file_server->DownloadFile("large.bin");
for (auto& chunk : reader)
process(chunk);
// Client stream — write chunks then close
auto writer = file_server->UploadFile("upload.bin");
while (has_data())
writer.send(next_chunk());
writer.close();// Server stream
const stream = await fileServer.DownloadFile('large.bin');
for await (const chunk of stream) {
process(chunk);
}
// Bidirectional
const chat = await chatService.Chat('lobby');
chat.send('Hello!');
for await (const msg of chat) {
console.log(msg);
}// Server stream — AsyncThrowingStream
let stream = try client.downloadFile(filename: "large.bin")
for try await chunk in stream {
process(chunk)
}WebTransport is the browser-native streaming transport built on HTTP/3. When an object is advertised with a secured HTTPS endpoint and the server has enable_http3() active, browsers can open a WebTransport session to https://host:port/wt and use it as the NPRPC transport.
- Unary RPC uses a single reliable bidirectional control stream.
stream<T>methods map to server-opened unidirectional WebTransport streams.client_stream<T>maps to client-opened unidirectional streams.bidi_stream<In,Out>maps to a dedicated bidirectional stream.
Enable it server-side by activating objects with ObjectActivationFlags::https and calling enable_http3() on the HTTP builder:
auto* rpc = nprpc::RpcBuilder()
.with_hostname("example.com")
.with_http(443)
.ssl("cert.crt", "key.key")
.enable_http3()
.build();
auto oid = poa->activate_object(&servant,
nprpc::ObjectActivationFlags::https);The TypeScript runtime automatically prefers WebTransport when available (globalThis.WebTransport present) and the object carries a secured endpoint.
Chain .ssl() on the HTTP or QUIC builder. The dhparams file is optional.
auto* rpc = nprpc::RpcBuilder()
.with_hostname("example.com")
.with_http(443)
.ssl("cert.crt", "key.key", "dhparam.pem")
.enable_http3()
.with_tcp(15000)
.build();
// Activate for secure WebSocket only
poa->activate_object(&obj, nprpc::ObjectActivationFlags::wss);// TypeScript client automatically uses wss:// when served over HTTPS
const rpc = await NPRPC.init();When HTTP/3 is enabled with multiple workers, NPRPC uses an eBPF SO_REUSEPORT
selector with a reuseport sockarray to keep QUIC packets pinned to the correct
worker. On Linux this is not an unprivileged operation.
If your application links against libnprpc and starts an HTTP/3 server with
more than one worker, grant capabilities to your application executable after
each build:
sudo setcap cap_net_admin,cap_bpf+ep /path/to/your_server_binary
getcap /path/to/your_server_binaryCapabilities must be applied to the final executable that starts the NPRPC
runtime, not to libnprpc.so.
Notes:
- Rebuilding the binaries may clear file capabilities, so scripts that launch HTTP/3 servers should re-apply them after each rebuild.
- If capabilities are unavailable, the safe fallback is to run HTTP/3 with a single worker; multi-worker HTTP/3 requires the reuseport BPF path.
- In Docker, grant the container the matching capabilities, for example
--cap-add=NET_ADMIN --cap-add=BPF.
// Server: bind by name
auto ns = rpc->get_nameserver("localhost:15001");
ns->Bind(calc_oid, "calculator");
ns->Bind(auth_oid, "authorizator");// Client: resolve by name
const ref = NPRPC.make_ref<NPRPC.ObjectProxy>();
if (await nameserver.Resolve('calculator', ref))
const calc = NPRPC.narrow(ref.value, example.Calculator);Use ObjectIdPolicy::UserSupplied when you need stable IDs baked into a web bundle:
auto* poa = nprpc::PoaBuilder(rpc)
.with_lifespan(nprpc::PoaPolicy::Lifespan::Persistent)
.with_object_id_policy(nprpc::PoaPolicy::ObjectIdPolicy::UserSupplied)
.with_max_objects(16)
.build();
constexpr nprpc::oid_t kCalcId = 0;
poa->activate_object_with_id(kCalcId, &calc,
nprpc::ObjectActivationFlags::tcp | nprpc::ObjectActivationFlags::http);IDs must be in [0, max_objects). With UserSupplied, activate_object (auto-ID) is disabled to prevent mismatches.
// Server
auto* rpc = nprpc::RpcBuilder()
.with_hostname("localhost")
.build();
poa->activate_object(&obj, nprpc::ObjectActivationFlags::shm);
rpc->run();// Client (same machine)
auto* rpc = nprpc::RpcBuilder().build();
auto ns = rpc->get_nameserver("localhost:15001");
Object* obj;
ns->Resolve("my_object", obj);
auto* svc = nprpc::narrow<MyInterface>(obj);
svc->MyMethod(data);Pass the current session context to restrict an object to the caller's connection:
nprpc::ObjectId CreateProcessor() override {
auto proc = std::make_unique<MyProcessor>();
// Only the calling client can reach this object
return poa_->activate_object(proc.release(),
nprpc::ObjectActivationFlags::all,
&nprpc::get_context());
}// IDL — use `object` for interface-typed parameters
interface ObjectManager {
object CreateProcessor(type: in string);
void RegisterProcessor(proc: in object);
}
void RegisterProcessor(nprpc::Object* proc) override {
auto* typed = nprpc::narrow<IDataProcessor>(proc);
if (!typed) throw nprpc::Exception("wrong type");
processors_.emplace_back(typed);
}TypeScript servants can be passed as parameters too — the server can call back on them over a bidirectional transport (WebSocket/WebTransport):
class MyProcessor extends example.IDataProcessor_Servant {
ProcessData(data: Uint8Array): void { /* ... */ }
}
const proc = new MyProcessor();
await manager.RegisterProcessor(proc);NPRPC can serve SvelteKit apps with full SSR over HTTP/3. See docs/SSR_ARCHITECTURE.md for setup and architecture details.
auto* rpc = nprpc::RpcBuilder()
.with_hostname("mysite.com")
.with_http(443)
.ssl("cert.crt", "key.key")
.root_dir("/srv/www")
.enable_http3()
.enable_ssr("/srv/www") // path to SvelteKit build containing index.js
.build();
rpc->run();See docs/HTTP_AUTH.md for the full API reference. Quick example:
// Inside any servant method — read / write httpOnly cookies
auto token = nprpc::get_cookie("session");
nprpc::set_cookie("session", new_token, {
.http_only = true, .secure = true, .same_site = "Strict", .max_age = 86400
});| Category | Tokens |
|---|---|
| Booleans | boolean |
| Integers | i8 i16 i32 i64 u8 u16 u32 u64 |
| Floats | f32 f64 |
| String | string |
| Object ref | object |
| Dynamic array | T[] or vector<T> |
| Fixed array | T[N] |
| Alias | alias Foo = vector<Bar> |
?— nullable/optional field or parameterin— input parameter (by value)out— output parameterraises(E1, E2)— exception specificationasync— fire-and-forget (no reply)[unreliable]— best-effort delivery (QUIC/WebTransport only, others ignore)[force_helpers=1]— emit helperfrom_flat/to_flatfunctions for amessage[trusted=true]— disable strict bounds checking for untrusted input
interface DataService {
// Server → Client
stream<vector<u8>> Download(id: in u32) raises(NotFound);
// server_stream<T> is the canonical spelling; stream<T> is an alias
// Client → Server
void Upload(name: in string, data: client_stream<vector<u8>>);
// Bidirectional
bidi_stream<string, string> Chat(room: in string);
bidi_stream<AAA, CCC> Transform(suffix: in string);
}
module blog;
exception NotFound { id: u32; }
message Post { id: u32; title: string; body: string; }
interface BlogService {
Post GetPost(id: in u32) raises(NotFound);
vector<Post> ListPosts(page: in u32, size: in u32);
async DeletePost(id: in u32);
// Stream all posts matching a query
stream<Post> Search(query: in string) raises(NotFound);
// Live feed — bidi (client sends ack, server sends posts)
bidi_stream<u32, Post> LiveFeed(channel: in string);
}
NPRPC provides native Swift bindings via Swift 6.2+ C++ interop. The full feature set is supported: servants, client proxies, exceptions, object references, async methods, and all three stream directions.
Swift must be built inside a dedicated Docker container because it requires NPRPC and Boost to be compiled with Swift's bundled Clang toolchain.
cd nprpc_swift
# Step 1 — build the Docker image (once)
cd ..
./build-dev-image.sh # builds nprpc-dev:latest used by CMake examples
cd nprpc_swift
# OR build the Swift-specific image directly
docker build -f Dockerfile \
--build-arg USER_ID=$(id -u) \
--build-arg GROUP_ID=$(id -g) \
--build-arg USERNAME=$(id -un) \
-t nprpc-swift-ubuntu ..
# Step 2 — build Boost + OpenSSL inside the container (first time only, ~10 min)
./docker-build-boost.sh
# Step 3 — build libnprpc.so with Swift's Clang
./docker-build-nprpc.sh
# Step 4 — generate Swift stubs from IDL (requires npidl built in Step 3)
./gen_stubs.sh
# Step 5 — build and optionally test the Swift package
./docker-build-swift.sh # build only
./docker-build-swift.sh --test # build + run tests (timeout 15 s)To rebuild the Docker image (after Dockerfile changes):
./docker-build-nprpc.sh --rebuild# Run from repo root; npidl must be built first
npidl myservice.npidl --swift --output-dir nprpc_swift/Sources/NPRPC/Generatedimport NPRPC
class CalculatorImpl: CalculatorServant, @unchecked Sendable {
override func add(a: Float64, b: Float64) throws -> Float64 { a + b }
override func divide(a: Float64, b: Float64) throws -> Float64 {
guard b != 0 else { throw CalculationError(message: "div/0", code: 1) }
return a / b
}
}let rpc = try RpcBuilder()
.setLogLevel(.info)
.setHostname("localhost")
.withTcp(15000)
.withHttp(15001)
.ssl(certFile: "cert.crt", keyFile: "key.key")
.build()
let poa = try rpc.createPoa(maxObjects: 100)
let servant = CalculatorImpl()
let oid = try poa.activateObject(servant, flags: [.tcp, .ws])
let obj = NPRPCObject.fromObjectId(oid)!
let client = narrow(obj, to: Calculator.self)!
let result = try client.add(a: 10, b: 20) // 30.0// Server stream
let stream = try client.downloadFile(filename: "data.bin")
for try await chunk in stream {
process(chunk)
}
// Async fire-and-forget
await client.playerMoved(x: 1.0, y: 2.0, z: 0.0)
// Async with out value
let reply = try await client.method2(arg1: 42)See nprpc_swift/README.md and nprpc_swift/EXAMPLES.md for more.
# Standard dev build (Ninja, RelWithDebInfo, all features)
./configure.sh
cmake --build .build_release -j$(nproc)
# Minimal build (library only)
cmake -S . -B build
cmake --build build -j$(nproc)
# With QUIC + HTTP/3
cmake -S . -B build -DNPRPC_ENABLE_QUIC=ON -DNPRPC_ENABLE_HTTP3=ON
cmake --build build -j$(nproc)
# Run C++ tests
./run_cpp_test.sh # or: ctest --output-on-failure --test-dir .build_releaseSee docs/BUILD.md for all CMake options, the JS/TS build, and install instructions.
NPRPC is benchmarked against gRPC and Cap'n Proto RPC.
./run_benchmark.sh # all suites
./run_benchmark.sh --benchmark_filter=EmptyCall # latency
./run_benchmark.sh --benchmark_filter=LargeData1MB # throughputSee benchmark/README.md for methodology and results.
| Topic | Document |
|---|---|
| Full build options | docs/BUILD.md |
| SSR architecture | docs/SSR_ARCHITECTURE.md |
| Cookie auth API | docs/HTTP_AUTH.md |
| HTTP/3 + WebTransport debugging | .github/skills/http3-webtransport-debugging/SKILL.md |
| Nameserver source | npnameserver/npnameserver.cpp |
| Swift integration tests | nprpc_swift/Tests/NPRPCTests/IntegrationTest.swift |
| C++ test suite | test/src/ |
| JS/TS test suite | test/js/ |
| Live Blog example | examples/live-blog/README.md |
See LICENSE.