Files
rust/blog/_posts/2015-03-21-error-handling-revisited.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.6 KiB

layout, title, author
layout title author
post error handling revisited dwrensha

Last week I pushed some changes that switch zap-rust over to using return-value-based error handling. In particular, we no longer use the default value fallback strategy discussed in this previous post. Now any method that might fail on malformed input returns a ::std::result::Result<T, ::zap::Error> instead of a bare T.

These changes remove a lot of complexity and have allowed me to delete a significant amount of code. They also provide, I think, a more honest interface for users of the library. Now the type signatures of the getter methods make it clear exactly where input validation errors are possible. The try!() macro makes it easy enough to deal with such errors in a principled manner.

Here is what a small example looks like after the changes:

{% highlight rust %} pub fn print_address_book( address_book: address_book::Reader) -> ::std::result::Result<(), ::zap::Error> { for person in try!(address_book.get_people()).iter() { println!("{}: {}", try!(person.get_name()), try!(person.get_email())); for phone in try!(person.get_phones()).iter() { let type_name = match phone.get_type() { Ok(person::phone_number::Type::Mobile) => "mobile", Ok(person::phone_number::Type::Home) => "home", Ok(person::phone_number::Type::Work) => "work", Err(::zap::NotInSchema(n)) => "UNKNOWN", }; println!(" {} phone: {}", type_name, try!(phone.get_number())); } match person.get_employment().which() { Ok(person::employment::Unemployed(())) => { println!(" unemployed"); } Ok(person::employment::Employer(employer)) => { println!(" employer: {}", try!(employer)); } Ok(person::employment::School(school)) => { println!(" student at: {}", try!(school)); } Ok(person::employment::SelfEmployed(())) => { println!(" self-employed"); } Err(::zap::NotInSchema(_)) => { } } } Ok(()) } {% endhighlight %}

Notice that there are in fact two types of errors being dealt with here. There is ::zap::Error, which gets returned when a malformed pointer field is encountered in the encoded message. There is also ::zap::NotInSchema, which indicates that an enumerant or union discriminant value was outside of the range defined in the schema. The second type of error can occur if the encoded data was constructed using a newer version of the schema. Instead of ignoring such cases, as in the above code, we might instead wish to propagate their errors. Because ::zap::Error implements ::std::error::FromError<::zap::NotInSchema>, we can easily accomplish that using the try!() macro:

{% highlight rust %} //... let type_name = match try!(phone.get_type()) { person::phone_number::Type::Mobile => "mobile", person::phone_number::Type::Home => "home", person::phone_number::Type::Work => "work", }; {% endhighlight %}

A year ago when I wrote the previous post on error handling, the main reason that I decided not to go with return-value-based error handling was that I thought it felt too heavyweight. My sense now is that the try!() macro and FromError trait can make things quite usable.