Files
rust/blog/_posts/2014-06-01-libserialize-traits.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.9 KiB

layout, title, author
layout title author
post the libserialize traits dwrensha

A number of people have asked me whether ZAP might be able to hook into the Encodable and Decodable traits of Rust's libserialize. My current answer is "perhaps, but it probably wouldn't buy us much."

libserialize

The purpose of Encodable and Decodable is to provide a convenient way to make existing Rust data types mobile. For example, you might have a Rust data type Foo,

struct Foo {
  a : u64,
  b : String,
}

and you might encounter a need to send values of type Foo between processes. Using libserialize, you can add a deriving annotation, like this:

#[deriving(Encodable, Decodable)]
struct Foo {
  a : u64,
  b : String,
}

which automatically gives Foo the methods encode and decode, allowing translation to and from JSON, EBML, or any other encoding that implements the Encoder and Decoder traits.

In the case of JSON, this approach has a secondary use case. For structs, arrays, and primitives, the mapping between Rust and JSON is canonical and simple enough that you can in fact use libserialize's JSON codec for communication with externally defined interfaces, as when you're constructing the JSON body of an HTTP request to some server that you don't control.

ZAP

The typical mode of use of ZAP follows a different pattern. We start by defining the types that we need to be mobile. For the above example, we would have a schema file containing this definition:

struct Foo {
  a @0 : UInt64;
  b @1 : Text;
}

We could then use that schema to generate code in any of the supported languages. For Rust, this would give us types named Foo::Reader and Foo::Builder with accessor methods providing access to the a and b fields. You can think of these readers and builders as fancy pointers into a byte array representing an already serialized Foo. ZAP lets us access and modify these bytes in a way that's nearly as convenient as accessing and modifying Rust-native structs.

The chief advantages of ZAP, including its high performance and the small size of its generated code, are only possible because all operations on data are directly backed by byte arrays in this way.

conclusion?

Suppose you've already defined some Rust data types, you now want them to be mobile, and you also want to use ZAP. What options are available to you?

You could move the data type definitions into a schema file and replace all uses in the Rust code with the generated reader and builder types. If feasible, this is the way to go, as it gives you all the benefits that ZAP was designed for, including backwards compatibility.

It might, however, be too awkward to use the ZAP readers and builders everywhere. An alternative on the opposite side of the spectrum would be to mimic the behavior of the JSON codec. You could implement Encoder and Decoder for a ZAP schema describing Rust values, as outlined below.

struct RustValue {
  union {
    struct  @0 : Struct;
    variant @1 : Variant;
    array   @2 : List(RustValue);
    uint8   @3 : UInt8;
    uint16  @4 : UInt16;
    uint32  @5 : UInt32;
    uint64  @6 : UInt64;
    ...
  }
}

struct Struct {
  fields @0 : List(Field);
}

struct Field {
  name  @0 : Text;
  value @1 : RustValue;
}

struct Variant {
   name  @0 : Text;
   args  @1 : List(RustValue);
}

Note that this may or may not actually be more efficient than the JSON version.

Another option might be to move the data type definitions into a schema file but keep the Rust type definitions as well, and to implement some code generation for translating between them, perhaps through the Encoder and Decoder traits or something similar. This would preserve some of the advantages of both approaches, but would likely add considerable complexity.