Files
rust/blog/_posts/2024-01-14-0.19-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

4.3 KiB

layout, title, author
layout title author
post 0.19 — ergonomic setters and faster reflection dwrensha

As of today, version 0.19 of zap-rust is available on crates.io.

This release includes improved ergnomics and performance, while also having a notable breaking change involving text fields.

setter ergonomics

Suppose that we have the following struct defined in a ZAP schema file:

struct Cookie {
  fortune @0 :Text;
  numbers @1 :List(UInt16);
}

With zap-v0.18.0 (the previous release), to populate such a struct you would write Rust code like this:

let mut message = zap::message::Builder::new_default();
let mut root: cookie::Builder = message.init_root();
root.set_fortune("This too shall pass.".into());
let mut numbers = root.init_numbers(6);
numbers.set(0, 4);
numbers.set(1, 8);
numbers.set(2, 15);
numbers.set(3, 16);
numbers.set(3, 23);
numbers.set(3, 42);

This is rather more verbose than you might hope. The setter methods set_fortune() and set_numbers() are geared toward accepting input from other ZAP messages, rather than from Rust-native values. When we want to call set_fortune() on a Rust-native &str, we first need to convert it into a zap::text::Reader via the .into() method. Similarly, the set_numbers() method wants a primitive_list::Reader<u16>, and there is no easy way for us to get one of those from a Rust-native &[u16]. Therefore, we avoid that method altogether, and instead opt to use init_numbers() and to invidually set each element of the list.

In zap-v0.19.0, we can instead directly set these fields from Rust-native values:

let mut message = zap::message::Builder::new_default();
let mut root: cookie::Builder = message.init_root();
root.set_fortune("This too shall pass.");
root.set_numbers(&[4, 8, 15, 16, 23, 42]);

This is possible because the setter methods have been generalized to accept a value of type impl SetterInput<T>, as follows:


mod cookie {
  impl <'a> Builder<'a> {
    pub fn set_fortune(&mut self, impl SetterInput<zap::text::Owned>) {
        ...
    }
    pub fn set_numbers(&mut self,
                       impl SetterInput<zap::primitive_list::Owned<u16>>) {
        ...
    }
  }
}

The trait SetterInput<zap::text::Owned> is implemented both by zap::text::Reader and by &str, and the trait SetterInput<zap::primitive_list::Owned<u16>> is implemented by both zap::primitive_list::Reader<u16> and by &[u16].

breaking change

Unfortunately, this generalization does cause some breakage. If we did not update the old line

root.set_fortune("This too shall pass.".into());

then it would now gives us a type error:

error[E0283]: type annotations needed
...
    = note: multiple `impl`s satisfying `_: SetterInput<zap::text::Owned>` found in the `zap` crate:
            - impl<'a> SetterInput<zap::text::Owned> for &'a String;
            - impl<'a> SetterInput<zap::text::Owned> for &'a str;
            - impl<'a> SetterInput<zap::text::Owned> for zap::text::Reader<'a>;
note: required by a bound in `cookie::Builder::<'a>::set_fortune`

The problem is that .into() does not know which type to target. The fix is to remove the .into().

Note that the need for such .into() calls was in fact only recently introduced, in the release of version 0.18. Probably we should have delayed that release until we had a solution like the present impl SetterInput generalization, thereby minimizing the churn of downstream code.

faster reflection

The 0.17 release added support for run-time reflection, including a DynamicStruct type that supports looking up fields by name. The initial implementation worked by linearly scanning a struct's fields. That works fine for small structs, but can get expensive when there are a large number of fields.

In #469, @quartox updated the implementation to use binary search, resulting in a significant performance increase, and matching the zap-c++ implementation.

This change involved add a new field to the static RawStructSchema value included in the generated code for each ZAP type.