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.