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.
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:
- Call
createBlob(). - Write to the blob using
BlobWriter.write(). - Call
BlobWriter.done()to get aBlobHandle. - Pass the
BlobHandletoWebSitePublisher.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.