Files
rust/blog/_posts/2023-09-04-0.18-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.6 KiB

layout, title, author
layout title author
post 0.18 — lazy UTF-8 and no-alloc dwrensha

New release alert! Version 0.18 of zap-rust is now available on crates.io.

If you use zap-rust on data with the Text built-in type, then it's likely that this release will require some updates to your code. But don't worry — the changes are straightforward and they bring some important benefits.

lazy UTF-8 validation

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

struct Foo {
  oneText @0 :Text;
  anotherText @1 :Text;
}

Then, in Rust, these Text fields can be accessed through the text::Reader type:

let my_foo: foo::Reader = ...;
let one_text: zap::text::Reader<'_> = my_foo.get_one_text()?;
let another_text: zap::text::Reader<'_> = my_foo.get_another_text()?;

But what exactly is a text::Reader?

the old definition

In previous versions of zap-rust, the text::Reader type was an alias to Rust's &str type:

pub mod text {
  type Reader<'a> = &'a str;
}

At first glance, this seems like a perfect fit. A ZAP Text value is required to contain valid UTF-8 data, just like a Rust &str, and a text::Reader is meant to represent a reference to that data.

However, in practice, there are some ways in which this representation falls short.

  • performance: Validating UTF-8 data has a cost, and ideally we would like to avoid paying it multiple times on the same data. If text::Reader is just &str, then we need to validate every time that we:

    • copy a text field from one message to another,
    • write a text field to a file,
    • write a file to a text field, or
    • access some sub-range of a text field.

    This goes against the general ZAP philosophy of doing validation as lazily as possible.

  • robustness -- If a text field holds corrupted data, then you still might want to be able to access that data, even if it is not valid UTF-8. For example, imagine that a text field holds log messages from a web server. We should still be able to read the messages, even if they are garbled. Indeed, garbled messages are probably the most interesting ones, as they indicate unexpected behavior. See this issue for more discussion.

the new definition

To address the above-noted shortcomings, version 0.18 of zap-rust defines text::Reader like this:

pub mod text {
  /// Wrapper around utf-8 encoded text.
  /// This is defined as a tuple struct to allow pattern matching
  /// on it via byte literals (for example `text::Reader(b"hello")`).
  #[derive(Copy, Clone, PartialEq)]
  pub struct Reader<'a>(pub &'a [u8]);

  impl<'a> Reader<'a> {
    pub fn as_bytes(self) -> &'a [u8] { ... }
    pub fn to_str(self) -> Result<&'a str, Utf8Error> { ... }
    pub fn to_string(self) -> Result<String, Utf8Error> { ... }
  }

  impl<'a> From<&'a str> for Reader<'a> { ... }
  impl<'a> From<&'a [u8]> for Reader<'a> { ... }}
}

Now consumers can easily access the underlying data, via as_bytes(), and getting it as a &str or String just requires an extra to_str() or to_string() call.

When setting text fields in a message, you will now need to insert some .into() calls to convert from a str or String into a text::Reader, like this:

let name: &str = "alice";
let mut my_foo: foo::Builder = ...;
my_foo.set_one_text("hello world".into())?;
my_foo.set_another_text(format!("hello {name}")[..].into())?;

All this is admittedly more verbose than it was before, but it's in keeping with the general spirit of zap-rust: we are willing to introduce some verbosity if that's what it takes to model ZAP data in a satisfactory way.

no-alloc mode

Another new feature is no-alloc mode.

In version 0.13, zap-rust gained support for no_std environments. However, it still depended on the alloc crate, which can sometimes be a problem for microcontroller targets and kernel programming. (See this issue for some discussion.)

Starting with version 0.18, the zap crate now has an alloc Cargo feature, which can be disabled to remove the alloc dependency.

A side benefit of this change is that now error handling in zap-rust is much less dependent on heap allocation, and so should have better performance and be more reliable.