03 Nov 2022
Today I am releasing version 0.15.0 of the capnproto-rust crates:
capnp,
capnpc,
capnp-futures,
and capnp-rpc.
It’s been a while – almost two years since the
0.14.0 release!
So what’s new?
Say we have a Cap’n Proto schema
that defines a struct type Foo:
struct Foo {
id @0 :UInt64;
payload @1 :Data;
}
When we pass this to the capnpc-rust code generator, it
gives us declarations for foo::Reader<'a> and foo::Builder<'b> types.
These types act as
“synthetic references”,
allowing us to
read a capnp-encoded Foo value directly from a byte buffer without
copying it into an intermediate structure.
The foo::Reader<'a> type acts like an immutable (shared) reference &'a Foo,
and the foo::Builder<'a> type acts like a mutable reference &'a mut Foo.
Very well, but what if we now want to define a generic container type for a Cap’n Proto message, whose contents can be borrowed either immutably or mutably? Something like:
struct MessageContainer<T> where T: ??? {
message: capnp::message::Builder<capnp::message::HeapAllocator>,
marker: core::marker::PhantomData<T>,
}
impl <T> MessageContainer<T> where T: ??? {
fn get(&self) -> ??? { ... }
fn get_mut(&mut self) -> ??? { ... }
}
We want to be able to plug Foo (or any other Cap’n Proto struct type)
in for T here. How do we fill in the ??? to make this work?
The key is that, in addition to foo::Reader and foo::Builder,
the capnpc-rust code generator also generates
a foo::Owned type, meant as a stand in for Foo itself
(which cannot be directly represented in Rust).
The type foo::Owned has the following impl:
impl capnp::traits::Owned for foo::Owned {
type Reader<'a>: foo::Reader<'a>;
type Builder<'a>: foo::Builder<'a>;
}
where capnp::traits::Owned is defined as
pub trait Owned {
type Reader<'a>: FromPointerReader<'a> + SetPointerBuilder;
type Builder<'a>: FromPointerBuilder<'a>;
}
Then we can fill in MessageContainer as follows:
struct MessageContainer<T> where T: capnp::traits::Owned {
message: capnp::message::Builder<capnp::message::HeapAllocator>,
marker: core::marker::PhantomData<T>,
}
impl <T> MessageContainer<T> where T: capnp::traits::Owned {
fn get(&self) -> T::Reader<'_> { ... }
fn get_mut(&mut self) -> T::Builder<'_> { ... }
}
Notice that the lifetime parameters on Owned::Reader
and Owned::Builder make them
generic associated types,
a newly stablized feature of Rust.
In previous versions of capnproto-rust,
we needed to hoist the lifetime parameter <'a> to the top
of the declaration of capnp::traits::Owned, like this:
pub trait Owned<'a> {
type Reader: FromPointerReader<'a> + SetPointerBuilder;
type Builder: FromPointerBuilder<'a>;
}
Then, usages of the trait often needed higher rank trait bounds (i.e. for<'a>...),
like this:
struct MessageContainer<T> where T: for<'a> capnp::traits::Owned<'a> {
message: capnp::message::Builder<capnp::message::HeapAllocator>,
marker: core::marker::PhantomData<T>,
}
The for<'a> syntax makes this trait look more complicated than it actually is,
so it’s good that we are finally able to remove it!
Consider the following (over)simplified version of Sandstorm’s web publishing interface:
interface BlobHandle {}
interface BlobWriter {
write @0 (chunk: Data);
done @1 () -> (handle :BlobHandle);
}
interface WebSitePublisher {
createBlob @0 () -> (writer :BlobWriter);
set @1 (path :Text, blob :BlobHandle);
}
To add some piece of content to a web site, an consumer of this API would do the following:
createBlob().BlobWriter.write().BlobWriter.done() to get a BlobHandle.BlobHandle to WebSitePublisher.set() for each path that should serve the content.This flow allows uploads to be streamed (step 2), and it allows a single piece of content to be pushed to multiple paths (step 4) without duplicated work.
But how is the server supposed to implement WebSitePublisher.set()?
The BlobHandle that it receives does not have any methods,
so how can anything meaningful be done with it?
Let’s first translate the question into Rust code.
The capnpc-rust-generated code for the above schema will contain
Client structs blob_handle::Client, blob_writer::Client,
web_site_publisher::Client, and Server traits
blob_handle::Server, blob_writer::Server, web_site_publisher::Server.
The idea is that the server will implement the Server traits,
with structs named perhaps BlobHandleImpl, BlobWriterImpl,
and WebSitePublisherImpl, and then will pass these
structs to RPC system via the capnp_rpc::new_client() function.
For example, to create a BlobHandle, the implementation
of blob_writer::Server::done() might do:
let blob_handle = BlobHandleImpl::new(blob_bytes);
let client: blob_handle::Client = capnp_rpc::new_client(blob_handle);
The issue is that once we call capnp_rpc::new_client() we no longer have
access to the underlying BlobHandleImpl object,
so by the time we are in web_site_publisher::Server::set(), we cannot
get to the blob’s bytes.
The solution is to use CapabilityServerSet,
a feature that has existed in capnproto-c++
for a long time, and as of today has been added to capnproto-rust.
If a blob_handle::Client is created via CapabilityServerSet::new_client(),
instead of capnp_rpc::new_client(),
then its underlying BlobHandleImpl can later be retrieved
via CapabilityServerSet::get_local_server().
let mut set: CapabilityServerSet<BlobHandleImpl,blob_handle::Client> =
CapabilityServerSet::new();
...
let blob_handle = BlobHandleImpl::new(blob_bytes);
let client: blob_handle::Client = set.new_client(blob_handle);
Then the implementation of web_site_publisher::Server could do:
if let Some(s) = set.get_local_client(&client).await {
// s has type `&Rc<RefCell<BlobHandleImpl>>`
...
}
Cap’n Proto’s packed codec is a way to reduce message size in exchange for a minimal encoding/decoding cost.
Until recently, capnproto-rust only support the packed codec
via the synchronous capnp::serialize_packed API; if you
wanted to pack your data over async I/O, you were out of luck.
In particular, there was no way to use the RPC system with the packed codec.
That has changed now, with
the addition
of the capnp_futures::serialize_packed module.
It includes standalone functions
serialize_packed::try_read_message() and
serialize_packed::write_message(), as well
as wrappers
struct PackedRead<R> where R: AsyncRead { ... }
and
struct PackedWrite<W> where W: AsyncWrite { ... }
which can be plugged into capnp_rpc::twoparty::VatNetwork::new()
to enable packed RPC.