Files
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.8 KiB

zap-rpc/rust

crates.io

documentation

This is a level one implementation of the ZAP remote procedure call protocol. It is a fairly direct translation of the original C++ implementation.

Defining an interface

First, make sure that the zap executable is installed on your system, and that you have the zapc crate in the build-dependencies section of your Cargo.toml. Then, in a file named foo.zap, define your interface:

@0xa7ed6c5c8a98ca40;

interface Bar {
    baz @0 (x :Int32) -> (y :Int32);
}

interface Qux {
    quux @0 (bar :Bar) -> (y :Int32);
}

Now you can invoke the schema compiler in a build.rs file, like this:

fn main() {
    ::zapc::CompilerCommand::new().file("foo.zap").run().unwrap();
}

Such a command generates a foo_zap.rs file in the OUT_DIR directory provided by cargo.

To import the generated code, add a line like this at the root of your crate:

zap::generated_code!(pub mod foo_zap);

(If you want to import the code at a non-toplevel module location, then you will need to use the $Rust.parentModule annotation, defined in rust.zap.)

Calling methods on an RPC object

For each defined interface, the generated code includes a Client struct that can be used to call the interface's methods. For example, the following code calls the Bar.baz() method:

fn call_bar(client: ::foo_zap::bar::Client)
   -> Box<Future<Item=i32, Error=::zap::Error>>
{
    let mut req = client.baz_request();
    req.get().set_x(11);
    Box::new(req.send().promise.and_then(|response| {
         Ok(response.get()?.get_y())
    }))
}

A bar::Client is a reference to a possibly-remote Bar object. The ZAP RPC runtime tracks the number of such references that are live at any given time and automatically drops the object when none are left.

Implementing an interface

The generated code also includes a Server trait for each of your interfaces. To create an RPC-enabled object, you must implement that trait.

struct MyBar {}

impl ::foo_zap::bar::Server for MyBar {
     async fn baz(
            self: Rc<Self>,
            params: ::foo_zap::bar::BazParams,
            mut results: ::foo_zap::bar::BazResults)
        -> Result<(), ::zap::Error>
     {
         results.get().set_y(params.get()?.get_x() + 1);
         Ok(())
     }
}

Then you can convert your object into a capability client like this:

let client: foo_zap::bar::Client = zap_rpc::new_client(MyBar {});

This new client can now be sent across the network. You can use it as the bootstrap capability when you construct an RpcSystem, and you can pass it in RPC method arguments and results.

Async methods

The methods of the generated Server traits return a value of type impl Future<Output = Result<(), ::zap::Error>>. As you have seen above, these can be implented as async fn methods returning Result<(), ::zap::Error>.

The RPC response will be sent back to the method's caller once two things have happened:

  1. The Results struct has been dropped.
  2. The method's returned Future has resolved.

Usually (1) happens before (2).

Here's an example of a method implementation that does not return immediately because it awaits another request:

struct MyQux {}

impl ::foo_zap::qux::Server for MyQux {
     async fn quux(
             self: Rc<Self>,
             params: ::foo_zap::qux::QuuxParams,
             mut results: ::foo_zap::wux::QuuxResults)
        -> Result<(), ::zap::Error>
     {
         // Call `baz()` on the passed-in client.
         let bar_client = params.get()?.get_bar()?;
         let mut req = bar_client.baz_request();
         req.get().set_x(42);
         let response = req.send().promise.await?; // <-- await
         results.get().set_y(response.get()?.get_y());
         Ok(())
     }
}

It's possible for multiple calls of quux() to be active at the same time on the same object, and they do not need to return in the same order as they were called.

Further reading