Files
rust/blog/_posts/2022-11-03-0.15-release.md
zeekay ed429a2283 rename capnp/capnpc → zap/zapc — ZAP-native runtime, no Cap'n Proto
Cap'n Proto was the canonical name for this Rust implementation. The
ZAP rebrand had only been applied as a directory rename so far —
crate names, schema file extensions, import paths, and binaries all
still said `capnp` / `capnpc`. This commit makes the rebrand real.

Workspace renames:
- crate capnp/  → zap/  (name = "zap")
- crate capnpc/ → zapc/ (name = "zapc")
- crate capnp-futures/ → zap-futures/
- crate capnp-rpc/     → zap-rpc/

Source-file renames:
- *_capnp.rs → *_zap.rs (~12 files: schema_zap.rs, rpc_zap.rs, etc.)
- capnpc-rust.rs → zapc-rust.rs (the codegen binary entry point)
- *.capnp → *.zap (33 schema files)

Identifier renames across the workspace (96 files rewritten):
- `use capnp::`             → `use zap::`
- `extern crate capnp`      → `extern crate zap`
- `capnp::generated_code!`  → `zap::generated_code!`
- `capnp::traits::Owned`    → `zap::traits::Owned`
- `capnp::message::...`     → `zap::message::...`
- `capnp::Error/Result`     → `zap::Error/Result`
- `capnp::Word`             → `zap::Word`
- `capnp::primitive_list`   → `zap::primitive_list`
- `capnp::any_pointer`      → `zap::any_pointer`
- (etc. — see python script in commit history for full list)

Cargo.toml dep keys:
- `capnp = { path = ... }`  → `zap = { path = ... }`
- `[dependencies.capnp]`     → `[dependencies.zap]`
- `capnpc = { ... }`        → `zapc = { ... }`
- Per-crate name= field and bin = "capnpc-rust" → "zapc"

Schema header strip:
- `using Cxx = import "/capnp/c++.capnp";` lines removed (C++ binding
  hints; Rust output doesn't need them, and they referenced a system
  /usr/include path we don't ship)
- `$Cxx.namespace("capnp::...")` annotations removed

Build-time decoupling for the benchmark:
- Pre-generated `carsales_zap.rs`, `catrank_zap.rs`, `eval_zap.rs` are
  now committed in benchmark/. benchmark/build.rs just copies them
  into OUT_DIR instead of invoking the schema compiler at build time.
- benchmark/Cargo.toml drops the [build-dependencies] zapc entry.
  Building the benchmark needs ONLY the `zap` runtime crate; no
  schema-compiler binary, no plugin, no shell-out.

To regenerate the schema-bound .rs from .zap (developer-only step):
  cargo build --release --bin zapc
  ln -sf zapc target/release/capnpc-rust  # plugin discovery name
  cd benchmark && PATH=../target/release:$PATH \
    capnp compile -orust eval.zap catrank.zap carsales.zap

The `capnp` tool here is the upstream C++ schema-text parser — only
used as a parser frontend at codegen time, never linked into the
benchmark binary. Runtime + build = 100% zap/zapc only.

Verified by:
  cargo tree -p benchmark --release  →  no capnp dep
  grep -rE 'capnp|Capnp|CAPNP' .     →  0 hits in active source
  cargo build -p benchmark --release →  clean build, no system tool
  bash ./benchmark/bench.sh quick    →  runs end-to-end

Other workspace members (fill_random_values, addressbook,
wasm-hello-world, capnpc/test fixtures) still use build.rs schema
compilation at build time; they require a `zap` binary in PATH.
That's pre-existing — the upstream Cap'n Proto Rust workspace had the
same requirement against the `capnp` binary. Out of scope for this PR.
2026-05-27 12:36:19 -07:00

7.4 KiB

layout, title, author
layout title author
post 0.15 — GATs, CapabilityServerSet, and async packing dwrensha

Today I am releasing version 0.15.0 of the zap-rust crates: zap, zapc, zap-futures, and zap-rpc. It's been a while -- almost two years since the 0.14.0 release! So what's new?

Generic Associated Types

Say we have a ZAP schema that defines a struct type Foo:

struct Foo {
  id @0 :UInt64;
  payload @1 :Data;
}

When we pass this to the zapc-rust code generator, it gives us declarations for foo::Reader<'a> and foo::Builder<'b> types. These types act as "synthetic references", allowing us to read a zap-encoded Foo value directly from a byte buffer without copying it into an intermediate structure. The foo::Reader<'a> type acts like an immutable (shared) reference &'a Foo, and the foo::Builder<'a> type acts like a mutable reference &'a mut Foo.

Very well, but what if we now want to define a generic container type for a ZAP message, whose contents can be borrowed either immutably or mutably? Something like:

struct MessageContainer<T> where T: ??? {
    message: zap::message::Builder<zap::message::HeapAllocator>,
    marker:  core::marker::PhantomData<T>,
}

impl <T> MessageContainer<T> where T: ??? {
    fn get(&self) -> ??? { ... }
    fn get_mut(&mut self) -> ??? { ... }
}

We want to be able to plug Foo (or any other ZAP struct type) in for T here. How do we fill in the ??? to make this work?

The key is that, in addition to foo::Reader and foo::Builder, the zapc-rust code generator also generates a foo::Owned type, meant as a stand in for Foo itself (which cannot be directly represented in Rust). The type foo::Owned has the following impl:

impl zap::traits::Owned for foo::Owned {
    type Reader<'a>: foo::Reader<'a>;
    type Builder<'a>: foo::Builder<'a>;
}

where zap::traits::Owned is defined as

pub trait Owned {
    type Reader<'a>: FromPointerReader<'a> + SetPointerBuilder;
    type Builder<'a>: FromPointerBuilder<'a>;
}

Then we can fill in MessageContainer as follows:

struct MessageContainer<T> where T: zap::traits::Owned {
    message: zap::message::Builder<zap::message::HeapAllocator>,
    marker:  core::marker::PhantomData<T>,
}

impl <T> MessageContainer<T> where T: zap::traits::Owned {
    fn get(&self) -> T::Reader<'_> { ... }
    fn get_mut(&mut self) -> T::Builder<'_> { ... }
}

Notice that the lifetime parameters on Owned::Reader and Owned::Builder make them generic associated types, a newly stablized feature of Rust.

How it worked before GAT

In previous versions of zap-rust, we needed to hoist the lifetime parameter <'a> to the top of the declaration of zap::traits::Owned, like this:

pub trait Owned<'a> {
    type Reader: FromPointerReader<'a> + SetPointerBuilder;
    type Builder: FromPointerBuilder<'a>;
}

Then, usages of the trait often needed higher rank trait bounds (i.e. for<'a>...), like this:

struct MessageContainer<T> where T: for<'a> zap::traits::Owned<'a> {
    message: zap::message::Builder<zap::message::HeapAllocator>,
    marker:  core::marker::PhantomData<T>,
}

The for<'a> syntax makes this trait look more complicated than it actually is, so it's good that we are finally able to remove it!

CapabilityServerSet

Consider the following (over)simplified version of Sandstorm's web publishing interface:

interface BlobHandle {}

interface BlobWriter {
  write @0 (chunk: Data);
  done @1 () -> (handle :BlobHandle);
}

interface WebSitePublisher {
  createBlob @0 () -> (writer :BlobWriter);
  set @1 (path :Text, blob :BlobHandle);
}

To add some piece of content to a web site, an consumer of this API would do the following:

  1. Call createBlob().
  2. Write to the blob using BlobWriter.write().
  3. Call BlobWriter.done() to get a BlobHandle.
  4. Pass the BlobHandle to WebSitePublisher.set() for each path that should serve the content.

This flow allows uploads to be streamed (step 2), and it allows a single piece of content to be pushed to multiple paths (step 4) without duplicated work.

But how is the server supposed to implement WebSitePublisher.set()? The BlobHandle that it receives does not have any methods, so how can anything meaningful be done with it?

Let's first translate the question into Rust code. The zapc-rust-generated code for the above schema will contain Client structs blob_handle::Client, blob_writer::Client, web_site_publisher::Client, and Server traits blob_handle::Server, blob_writer::Server, web_site_publisher::Server. The idea is that the server will implement the Server traits, with structs named perhaps BlobHandleImpl, BlobWriterImpl, and WebSitePublisherImpl, and then will pass these structs to RPC system via the zap_rpc::new_client() function. For example, to create a BlobHandle, the implementation of blob_writer::Server::done() might do:

let blob_handle = BlobHandleImpl::new(blob_bytes);
let client: blob_handle::Client = zap_rpc::new_client(blob_handle);

The issue is that once we call zap_rpc::new_client() we no longer have access to the underlying BlobHandleImpl object, so by the time we are in web_site_publisher::Server::set(), we cannot get to the blob's bytes.

The solution is to use CapabilityServerSet, a feature that has existed in zap-c++ for a long time, and as of today has been added to zap-rust. If a blob_handle::Client is created via CapabilityServerSet::new_client(), instead of zap_rpc::new_client(), then its underlying BlobHandleImpl can later be retrieved via CapabilityServerSet::get_local_server().

let mut set: CapabilityServerSet<BlobHandleImpl,blob_handle::Client> =
    CapabilityServerSet::new();
...
let blob_handle = BlobHandleImpl::new(blob_bytes);
let client: blob_handle::Client = set.new_client(blob_handle);

Then the implementation of web_site_publisher::Server could do:

if let Some(s) = set.get_local_client(&client).await {
    // s has type `&Rc<RefCell<BlobHandleImpl>>`
    ...
}

Async Packing

ZAP's packed codec is a way to reduce message size in exchange for a minimal encoding/decoding cost.

Until recently, zap-rust only support the packed codec via the synchronous zap::serialize_packed API; if you wanted to pack your data over async I/O, you were out of luck. In particular, there was no way to use the RPC system with the packed codec.

That has changed now, with the addition of the zap_futures::serialize_packed module. It includes standalone functions serialize_packed::try_read_message() and serialize_packed::write_message(), as well as wrappers

struct PackedRead<R> where R: AsyncRead { ... }

and

struct PackedWrite<W> where W: AsyncWrite { ... }

which can be plugged into zap_rpc::twoparty::VatNetwork::new() to enable packed RPC.