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.
5.3 KiB
layout, title, author
| layout | title | author |
|---|---|---|
| post | asynchronous I/O with promises | dwrensha |
My Rust implementation of the ZAP remote procedure call protocol was designed in a bygone era. Back then, Rust's runtime library provided thread-like "tasks" that were backed by libgreen and were therefore "cheap to spawn." These enabled CSP-style programming with beautifully simple blocking I/O operations that were, under the hood, dispatched through libuv. While the question of whether this model was actually efficient was a matter of much discussion, I personally enjoyed using it and found it easy to reason about.
For better or worse, the era of libgreen has ended. Code originally written for libgreen can still work, but because each "task" is now its own system-level thread, calling them "lightweight" is more of a stretch than ever. As I've maintained zap-rpc/rust over the past year, its need for a different approach to concurrency has become increasingly apparent.
Introducing GJ
GJ is a new Rust library that provides abstractions for event-loop concurrency and asynchronous I/O, aiming to meet the needs of ZAP RPC. The main ideas in GJ are taken from KJ, a C++ library that forms the foundation of zap-c++. At Sandstorm, we have been successfully using KJ-based concurrency in our core infrastructure for a while now; some examples you can look at include a bridge that translates between HTTP and this ZAP interface, and a ZAP driver to a FUSE filesystem.
The core abstraction in GJ is the Promise<T>, representing
a computation that may eventually resolve to a value of type T.
Instead of blocking, any non-immediate operation in GJ
returns a promise that gets fulfilled upon the operation's completion.
To use a promise, you register a callback with the then() method.
For example:
{% highlight rust %} pub fn connect_then_write(addr: gj::io::NetworkAddress) -> gj::Promise<()> { return addr.connect().then(|stream| { // The connection has succeeded. Let's write some data. return Ok(stream.write(vec![1,2,3])); }).then(|(stream, _)| { // The first write has succeeded. Let's write some more. return Ok(stream.write(vec![4,5,6])); }).then(|(stream, _)| { // The second write has succeeded. Let's just return; return Ok(gj::Promise::fulfilled(())); }); } {% endhighlight %}
Callbacks registered with then() never move between threads, so they do
not need to be thread-safe.
In Rust jargon, the callbacks are FnOnce closures that need not be Send.
This means that you can share mutable data between them
without any need for mutexes or atomics. For example, to share a counter,
you could do this:
{% highlight rust %} pub fn ticker(counter: Rc<Cell>, delay_ms: u64) -> gj::Promise<()> { return gj::io::Timer.after_delay_ms(delay_ms).then(move |()| { println!("the counter is at: {}", counter.get()); counter.set(counter.get() + 1); return Ok(ticker(counter, delay_ms)); }); }
pub fn two_tickers() -> gj::Promise<Vec<()>> { let counter = Rc::new(Cell::new(0)); return gj::join_promises(vec![ticker(counter.clone(), 500), ticker(counter, 750)]); } {% endhighlight %}
If you do want to use multiple threads, GJ makes it easy to set up an event loop in each and to communicate between them over streams of bytes.
To learn more about what's possible with GJ, I encourage you to explore some of more complete examples in the git repo.
Onwards!
Two things in particular have made working GJ especially fun so far:
- KJ is written in clean, modern C++ that translates nicely into idiomatic Rust. The translation is fairly direct most of the time, and parts that don't translate directly make for fun puzzles! For one such nontrival translation, compare KJ's AsyncOutputStream to GJ's AsyncWrite.
- The excellent mio library allows us to not worry
about system-specific APIs. It provides a uniform abstraction on top of
epollon Linux andkqueueon OSX, and maybe someday evenIOCPin Windows.
Although basics of GJ are operational today, there's still a lot of work left to do. If this is a project that sounds interesting or useful to you, I'd love to have your help!