C++
ZAP C++ SDK — the schema front-end + zero-copy runtime (cpp-core) and the high-level binding (cpp).
C++ Binding
ZAP's C++ surface is split into two repositories with distinct roles:
| Repository | Role |
|---|---|
zap-proto/cpp-core | The canonical schema front-end (the zap compiler binary every language plugin execs) plus the underlying zero-copy C++ runtime and the KJ support library. Derived from libcapnp; ZAP adds the whitespace-significant grammar on top. |
zap-proto/cpp | The high-level C++ binding — the client/server convenience API that builds on the cpp-core runtime. |
If you are generating code for another language, you only need the front-end
from cpp-core (it emits a binary code-generator request that your language's
plugin consumes). If you are writing a C++ program, you use the cpp-core
runtime headers (in namespace zap) plus the cpp binding for RPC convenience.
Build the front-end and runtime
cpp-core builds with Autotools, CMake, or Bazel:
# CMake
cmake -S c++ -B build && cmake --build build
# Autotools
cd c++ && autoreconf -i && ./configure && make -j checkmake check builds the compiler (the zap_tool target) and runs the schema,
compiler, and runtime test suites. The zap binary is what other runtimes shell
out to.
Compile a schema
The front-end parses a .zap file and emits a binary code-generator request on
stdout; a language plugin consumes it. Plugins are resolved as zap-<lang>
executables on PATH.
# Emit a code-generator request to stdout (what a plugin reads on stdin).
zap compile -o- point.zap
# Drive a language plugin found on PATH.
zap compile -orust point.zapZAP schemas are whitespace-significant — blocks are delimited by indentation,
there are no braces, and ordinals are assigned automatically. The brace form
(x @0 :Float32;) still parses, and desugaring is a no-op on it, so both
surfaces produce byte-identical schemas.
Build and read a message
The runtime lives in namespace zap (the cpp-core headers). You build a
message with a MallocMessageBuilder, initialize the root to your generated
struct type, and read it back through a generated Reader — zero-copy, straight
out of the segment memory.
#include <zap/message.h>
#include <zap/serialize.h>
#include "point.zap.h" // generated from point.zap
// Write.
zap::MallocMessageBuilder message;
Point::Builder point = message.initRoot<Point>();
point.setX(1.5);
point.setY(-2.5);
kj::Array<zap::word> words = zap::messageToFlatArray(message);
// Read (zero-copy): the reader borrows the flat array; nothing is decoded.
zap::FlatArrayMessageReader reader(words.asPtr());
Point::Reader p = reader.getRoot<Point>();
// p.getX() == 1.5, p.getY() == -2.5writeMessageToFd / StreamFdMessageReader (in zap/serialize.h) stream a
message to and from a file descriptor; serialize-packed.h and
serialize-async.h provide packed and async framing.
RPC
The runtime's RPC is capability-based with promise pipelining, exposed through
zap/rpc-twoparty.h. TwoPartyServer hosts a bootstrap capability over a
connection; TwoPartyClient connects and retrieves it.
#include <zap/rpc-twoparty.h>
// Server: host an implementation as the bootstrap capability.
zap::TwoPartyServer server(kj::heap<PointTrackerImpl>());
server.accept(std::move(connection)); // an AsyncIoStream
// Client: connect and call the bootstrap interface.
zap::TwoPartyClient client(connection);
PointTracker::Client tracker = client.bootstrap().castAs<PointTracker>();
auto request = tracker.addPointRequest();
request.setP(point);
auto promise = request.send(); // pipelines; await with KJ's event loopPointTracker::Server (generated) is the interface you subclass on the server
side; PointTracker::Client (generated) is the typed proxy on the client side.
Because a request's result can be used as the target of another call before it
resolves, dependent calls collapse into a single round trip.
Documentation
The schema toolchain and core runtime are documented in
zap-proto/cpp-core; the high-level
binding is in zap-proto/cpp. The
Schema Language and Code Generation pages cover
the .zap surface this front-end parses.