Examples
Practical examples of using ZAP C++ for serialization and RPC
Examples
This page contains practical examples of using ZAP C++ for serialization and RPC.
Basic Serialization
Address Book Schema
# addressbook.zap
@0xdbb9ad1f14bf0b36;
struct Person {
id @0 :UInt32;
name @1 :Text;
email @2 :Text;
phones @3 :List(PhoneNumber);
struct PhoneNumber {
number @0 :Text;
type @1 :Type;
enum Type {
mobile @0;
home @1;
work @2;
}
}
}
struct AddressBook {
people @0 :List(Person);
}Writing Data
#include <zap/message.h>
#include <zap/serialize-packed.h>
#include <fcntl.h>
#include "addressbook.zap.h"
void writeAddressBook(int fd) {
zap::MallocMessageBuilder message;
auto addressBook = message.initRoot<AddressBook>();
auto people = addressBook.initPeople(2);
// First person
auto alice = people[0];
alice.setId(123);
alice.setName("Alice");
alice.setEmail("alice@example.com");
auto alicePhones = alice.initPhones(1);
alicePhones[0].setNumber("+1-555-1234");
alicePhones[0].setType(Person::PhoneNumber::Type::MOBILE);
// Second person
auto bob = people[1];
bob.setId(456);
bob.setName("Bob");
bob.setEmail("bob@example.com");
auto bobPhones = bob.initPhones(2);
bobPhones[0].setNumber("+1-555-5678");
bobPhones[0].setType(Person::PhoneNumber::Type::HOME);
bobPhones[1].setNumber("+1-555-9999");
bobPhones[1].setType(Person::PhoneNumber::Type::WORK);
// Write to file
zap::writePackedMessageToFd(fd, message);
}Reading Data
#include <zap/serialize-packed.h>
#include <iostream>
#include "addressbook.zap.h"
void readAddressBook(int fd) {
zap::PackedFdMessageReader message(fd);
auto addressBook = message.getRoot<AddressBook>();
for (auto person : addressBook.getPeople()) {
std::cout << person.getName().cStr()
<< " (id=" << person.getId() << ")\n";
std::cout << " Email: " << person.getEmail().cStr() << "\n";
for (auto phone : person.getPhones()) {
const char* typeName;
switch (phone.getType()) {
case Person::PhoneNumber::Type::MOBILE:
typeName = "mobile";
break;
case Person::PhoneNumber::Type::HOME:
typeName = "home";
break;
case Person::PhoneNumber::Type::WORK:
typeName = "work";
break;
}
std::cout << " Phone (" << typeName << "): "
<< phone.getNumber().cStr() << "\n";
}
}
}Working with Unions
Shape Schema
# shape.zap
@0xa1b2c3d4e5f67890;
struct Shape {
name @0 :Text;
union {
circle @1 :Circle;
rectangle @2 :Rectangle;
triangle @3 :Triangle;
}
struct Circle {
radius @0 :Float64;
}
struct Rectangle {
width @0 :Float64;
height @1 :Float64;
}
struct Triangle {
base @0 :Float64;
height @1 :Float64;
}
}
struct Drawing {
shapes @0 :List(Shape);
}Creating and Reading Shapes
#include "shape.zap.h"
#include <cmath>
// Calculate area based on shape type
double calculateArea(Shape::Reader shape) {
switch (shape.which()) {
case Shape::CIRCLE: {
auto circle = shape.getCircle();
return M_PI * circle.getRadius() * circle.getRadius();
}
case Shape::RECTANGLE: {
auto rect = shape.getRectangle();
return rect.getWidth() * rect.getHeight();
}
case Shape::TRIANGLE: {
auto tri = shape.getTriangle();
return 0.5 * tri.getBase() * tri.getHeight();
}
}
return 0;
}
// Create shapes
void createShapes(zap::MallocMessageBuilder& message) {
auto drawing = message.initRoot<Drawing>();
auto shapes = drawing.initShapes(3);
// Circle
shapes[0].setName("Circle A");
auto circle = shapes[0].initCircle();
circle.setRadius(5.0);
// Rectangle
shapes[1].setName("Rectangle B");
auto rect = shapes[1].initRectangle();
rect.setWidth(10.0);
rect.setHeight(20.0);
// Triangle
shapes[2].setName("Triangle C");
auto tri = shapes[2].initTriangle();
tri.setBase(8.0);
tri.setHeight(6.0);
}Echo RPC Server and Client
Echo Schema
# echo.zap
@0x8e5322c1e9282534;
interface Echo {
echo @0 (message :Text) -> (reply :Text);
}Server Implementation
// echo_server.cpp
#include <zap/ez-rpc.h>
#include <zap/message.h>
#include <kj/debug.h>
#include <iostream>
#include "echo.zap.h"
class EchoImpl final : public Echo::Server {
public:
kj::Promise<void> echo(EchoContext context) override {
auto message = context.getParams().getMessage();
KJ_LOG(INFO, "Received", message);
context.getResults().setReply(kj::str("Echo: ", message));
return kj::READY_NOW;
}
};
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <address:port>\n";
return 1;
}
// Start server
zap::EzRpcServer server(kj::heap<EchoImpl>(), argv[1]);
auto& waitScope = server.getWaitScope();
std::cout << "Listening on " << argv[1] << "...\n";
// Run forever
kj::NEVER_DONE.wait(waitScope);
}Client Implementation
// echo_client.cpp
#include <zap/ez-rpc.h>
#include <iostream>
#include "echo.zap.h"
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " <address:port> <message>\n";
return 1;
}
// Connect to server
zap::EzRpcClient client(argv[1]);
auto& waitScope = client.getWaitScope();
// Get the Echo capability
Echo::Client echo = client.getMain<Echo>();
// Make request
auto request = echo.echoRequest();
request.setMessage(argv[2]);
// Wait for response
auto response = request.send().wait(waitScope);
std::cout << response.getReply().cStr() << "\n";
return 0;
}Calculator with Promise Pipelining
Calculator Schema
# calculator.zap
@0xf9e5a3d2c1b08765;
interface Calculator {
evaluate @0 (expression :Expression) -> (value :Float64);
getOperator @1 (op :Operator) -> (func :Function);
enum Operator {
add @0;
subtract @1;
multiply @2;
divide @3;
}
}
interface Function {
call @0 (params :List(Float64)) -> (value :Float64);
}
struct Expression {
union {
literal @0 :Float64;
call :group {
function @1 :Function;
params @2 :List(Expression);
}
}
}Pipelining Client
// calculator_client.cpp
#include <zap/ez-rpc.h>
#include <iostream>
#include "calculator.zap.h"
int main() {
zap::EzRpcClient client("localhost:5000");
auto& waitScope = client.getWaitScope();
Calculator::Client calc = client.getMain<Calculator>();
// Get the multiply operator - this returns a Function capability
auto getOpRequest = calc.getOperatorRequest();
getOpRequest.setOp(Calculator::Operator::MULTIPLY);
auto opPromise = getOpRequest.send();
// Pipeline: immediately call the function without waiting
// for getOperator to complete!
auto callRequest = opPromise.getFunc().callRequest();
auto params = callRequest.initParams(3);
params.set(0, 2.0);
params.set(1, 3.0);
params.set(2, 4.0);
// Both calls are sent in a single round-trip
auto result = callRequest.send().wait(waitScope);
std::cout << "2 * 3 * 4 = " << result.getValue() << "\n";
// Output: 2 * 3 * 4 = 24
return 0;
}Memory-Mapped Files
#include <zap/serialize.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
class MappedMessage {
public:
MappedMessage(const char* filename) {
fd_ = open(filename, O_RDONLY);
if (fd_ < 0) {
KJ_FAIL_SYSCALL("open", errno, filename);
}
struct stat st;
if (fstat(fd_, &st) < 0) {
close(fd_);
KJ_FAIL_SYSCALL("fstat", errno);
}
size_ = st.st_size;
data_ = mmap(nullptr, size_, PROT_READ, MAP_PRIVATE, fd_, 0);
if (data_ == MAP_FAILED) {
close(fd_);
KJ_FAIL_SYSCALL("mmap", errno);
}
// Create reader directly from mapped memory
words_ = kj::ArrayPtr<const zap::word>(
reinterpret_cast<const zap::word*>(data_),
size_ / sizeof(zap::word));
reader_ = kj::heap<zap::FlatArrayMessageReader>(words_);
}
~MappedMessage() {
reader_ = nullptr;
if (data_ != MAP_FAILED) munmap(data_, size_);
if (fd_ >= 0) close(fd_);
}
template <typename T>
typename T::Reader getRoot() {
return reader_->getRoot<T>();
}
private:
int fd_ = -1;
void* data_ = MAP_FAILED;
size_t size_ = 0;
kj::ArrayPtr<const zap::word> words_;
kj::Own<zap::FlatArrayMessageReader> reader_;
};
// Usage
void useMappedFile() {
MappedMessage msg("large-data.zap");
auto data = msg.getRoot<LargeDataSet>();
// Access data directly from disk without copying
}JSON Conversion
#include <zap/compat/json.h>
#include <zap/message.h>
#include <iostream>
void convertToJson(Person::Reader person) {
zap::JsonCodec codec;
kj::String json = codec.encode(person);
std::cout << json.cStr() << std::endl;
}
void parseFromJson(kj::StringPtr json, zap::MallocMessageBuilder& message) {
zap::JsonCodec codec;
auto person = message.initRoot<Person>();
codec.decode(json, person);
}CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(examples CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Zap REQUIRED)
# Generate C++ from schemas
zap_generate_cpp(ZAP_SRCS ZAP_HDRS
addressbook.zap
shape.zap
echo.zap
calculator.zap
)
# Address book example
add_executable(addressbook
addressbook_main.cpp
${ZAP_SRCS}
)
target_include_directories(addressbook PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(addressbook PRIVATE Zap::zap)
# Echo server and client
add_executable(echo_server echo_server.cpp ${ZAP_SRCS})
target_include_directories(echo_server PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(echo_server PRIVATE
Zap::zap
Zap::zap-rpc
Zap::kj
Zap::kj-async
)
add_executable(echo_client echo_client.cpp ${ZAP_SRCS})
target_include_directories(echo_client PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(echo_client PRIVATE
Zap::zap
Zap::zap-rpc
Zap::kj
Zap::kj-async
)Running the Examples
# Build
mkdir build && cd build
cmake ..
cmake --build .
# Run echo example
./echo_server localhost:5000 &
./echo_client localhost:5000 "Hello, World!"
# Output: Echo: Hello, World!
# Address book
./addressbook write > addressbook.bin
./addressbook read < addressbook.binNext Steps
- Explore the API Reference
- Learn about RPC
- Understand the KJ Library