ZAP C++

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.bin

Next Steps