Files
rust/blog/_posts/2016-01-11-async-rpc.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.2 KiB

layout, title, author
layout title author
post RPC system rewritten to use async I/O dwrensha

Today I've released verson 0.6 of the three main ZAP crates: the runtime, the code generator, and the remote procedure call system.

The biggest change is that the RPC system is now asynchronous, built on top of the GJ event loop library.

promises

ZAP's RPC protocol allows method calls to be made on distributed objects. It has a built-in notion of pipelining that can minimize network round-trips, and it allows object references to be transmitted within messages, alongside plain-old-data.

Such a system has an inherent need to deal with concurrency, both in its internal implementation in its publicly-exposed interfaces.

For example, it's common for a user to want to implement an RPC method that makes a bunch of other RPC calls and then collects their results before returning. How should these actions compose? In the new version of zap-rpc, which uses GJ's Promise abstraction, each of the inner method calls returns a promise, and those promises can be collected with Promise::all() to form a new promise which can then be returned from the outer method.

To see more concretely what this kind of thing looks like, take a look at the calculator example, which also showcases some fancier features. There are also some more-practical examples in the form of Sandstorm apps: a simple GET/PUT/DELETE server and a word game.

error handling

A Promise<T, E> is essentially a deferred Result<T, E>, so it should be no surprise that today's release pertains to our continuing story about error handling.

Last time, we described our switch to using Result<T,E> pervasively, so that we could return an Err(e) on a decode error, rather than panicking. That switch had some costs:

  1. We now need to sprinkle try!()s in our code, one for any time we dereference a ZAP pointer.
  2. We also need to edit some return types from T to Result<T, E>, and in some cases need to define helper functions so that try!() has a place to return to.

In my opinion, (1) is not so bad, and it has the advantage of making control flow more clear. The proposed ? operator would make this even nicer.

In the asynchronous world of ZAP RPC, (2) becomes less of a hassle, because most functions that need to read a ZAP message are asynchronous, and therefore already return a Promise<T, E>. In such cases, we can use the pry!() macro that GJ defines. The pry!() macro acts like try!(), but in the early return case returns Promise::err(e) rather than Result::Err(e).

a simpler error type

One error-related question that often arises when designing interfaces that use Promise<T,E> is: what concrete error type should be plugged in for E?

In previous versions of zap-rust, I had defined an error enum zap::Error with various cases, one of which wrapped a std::io::Error. This got me into trouble when I wanted to start using Promise::fork(), which requires that E be Clone. The problem is that std::io::Error is not Clone!

To address this problem, I have redefined ::zap::Error to follow the design of kj::Exception. It's now a very simple struct with a String description and an ErrorKind enum, where the only variants of ErrorKind are Failed, Disconnected, Overloaded, and Unimplemented. The observation here is that there seems to be very little gained by defining hierarchies of errors wrapping other errors.