Files
rust/blog/_posts/2013-11-17-lifetimes.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

3.8 KiB

layout, title, author
layout title author
post lifetime variables and safety dwrensha

Like its C++ counterpart, zap-rust relies heavily on raw pointer manipulation to achieve good performance. In fact, the translation from C++ to Rust for the core low level data structures is quite direct, as you can see by comparing layout.c++ and layout.rs.

One important difference is that the Rust language, in addition to requiring that we explicitly label the pointer manipulations as "unsafe", also provides us with better facilities for building a safe external interface on top of them.

To illustrate, let's look at some ways one might misuse the C++ interface.

Here's a program that builds two Person structs, each a part of its own AddressBook message.

#include "addressbook.zap.h"
#include <zap/message.h>
#include <iostream>

using addressbook::Person;
using addressbook::AddressBook;

Person::Builder returnPersonBuilder(int id) {
  ::zap::MallocMessageBuilder message;

  auto addressBook = message.initRoot<AddressBook>();
  auto people = addressBook.initPeople(1);

  people[0].setId(id);
  people[0].setName("Alice");

  return people[0];
}

int main(int argc, char* argv[]) {
  auto person1 = returnPersonBuilder(123);
  auto person2 = returnPersonBuilder(456);
  std::cout << person1.getId() << "\n";
  return 0;
}

You might expect the program to print "123", but it actually prints "456". The problem is that the Person::Builder returned by the returnPersonBuilder() function is unsafe to use because it outlives its MessageBuilder.

Here is a snippet showing a related problem.

{
  ::zap::MallocMessageBuilder message;

  auto addressBook = message.initRoot<AddressBook>();
  auto people = addressBook.initPeople(1);

  auto alice = people[0];
  alice.setId(123);

  auto person = message.initRoot<Person>();

  std::cout << alice.getId() << "\n";
}

You might expect this code to print "123", but it actually prints "0" because alice is no longer valid after message has been initialized a second time.

Both of these errors could be statically detected and prevented in Rust. The key is to arrange that the MessageBuilder::initRoot() function borrow a reference to the message that invokes it, and to keep track of the lifetime of that borrow. The Rust typechecker will then be able to detect if the message is borrowed again or if some sub-builder of it---whose type will be annotated with the lifetime---outlives the lifetime.

To make this concrete, in Rust the signature of MessageBuilder::initRoot could look something like this:

pub fn initRoot<'a, T : FromStructBuilder<'a>>(&'a mut self) -> T;

where FromStructBuilder is the trait

pub trait FromStructBuilder<'a> {
    fn fromStructBuilder(structBuilder : StructBuilder<'a>) -> Self;
}

and StructBuilder is a low-level type which should not be exposed to the safe user interface. Here 'a is the lifetime variable that tracks the borrow of the message builder. The generated code for AddressBook and Person will then contain implementations for the FromStructBuilder trait:

impl <'a> FromStructBuilder<'a> for AddressBook::Builder<'a> { ... }
impl <'a> FromStructBuilder<'a> for Person::Builder<'a> { ... }

Unfortunately, there's one hitch: the Rust compiler does not yet quite support this kind of interplay between lifetime variables and traits. This is why I have been so eagerly watching Rust issues 5121, 7331, and 10391.

In the meantime, zap-rust does partially enforce the kind of lifetime safety described above, but only for message readers, not message builders, and only using a somewhat roundabout strategy that makes it awkward to support the more complex ZAP types like lists of lists.