0.19 — ergonomic setters and faster reflection

14 Jan 2024

As of today, version 0.19 of capnproto-rust is available on crates.io.

This release includes improved ergnomics and performance, while also having a notable breaking change involving text fields.

setter ergonomics

Suppose that we have the following struct defined in a Cap’n Proto schema file:

struct Cookie {
  fortune @0 :Text;
  numbers @1 :List(UInt16);

With capnp-v0.18.0 (the previous release), to populate such a struct you would write Rust code like this:

let mut message = capnp::message::Builder::new_default();
let mut root: cookie::Builder = message.init_root();
root.set_fortune("This too shall pass.".into());
let mut numbers = root.init_numbers(6);
numbers.set(0, 4);
numbers.set(1, 8);
numbers.set(2, 15);
numbers.set(3, 16);
numbers.set(3, 23);
numbers.set(3, 42);

This is rather more verbose than you might hope. The setter methods set_fortune() and set_numbers() are geared toward accepting input from other Cap’n Proto messages, rather than from Rust-native values. When we want to call set_fortune() on a Rust-native &str, we first need to convert it into a capnp::text::Reader via the .into() method. Similarly, the set_numbers() method wants a primitive_list::Reader<u16>, and there is no easy way for us to get one of those from a Rust-native &[u16]. Therefore, we avoid that method altogether, and instead opt to use init_numbers() and to invidually set each element of the list.

In capnp-v0.19.0, we can instead directly set these fields from Rust-native values:

let mut message = capnp::message::Builder::new_default();
let mut root: cookie::Builder = message.init_root();
root.set_fortune("This too shall pass.");
root.set_numbers(&[4, 8, 15, 16, 23, 42]);

This is possible because the setter methods have been generalized to accept a value of type impl SetterInput<T>, as follows:

mod cookie {
  impl <'a> Builder<'a> {
    pub fn set_fortune(&mut self, impl SetterInput<capnp::text::Owned>) {
    pub fn set_numbers(&mut self,
                       impl SetterInput<capnp::primitive_list::Owned<u16>>) {

The trait SetterInput<capnp::text::Owned> is implemented both by capnp::text::Reader and by &str, and the trait SetterInput<capnp::primitive_list::Owned<u16>> is implemented by both capnp::primitive_list::Reader<u16> and by &[u16].

breaking change

Unfortunately, this generalization does cause some breakage. If we did not update the old line

root.set_fortune("This too shall pass.".into());

then it would now gives us a type error:

error[E0283]: type annotations needed
    = note: multiple `impl`s satisfying `_: SetterInput<capnp::text::Owned>` found in the `capnp` crate:
            - impl<'a> SetterInput<capnp::text::Owned> for &'a String;
            - impl<'a> SetterInput<capnp::text::Owned> for &'a str;
            - impl<'a> SetterInput<capnp::text::Owned> for capnp::text::Reader<'a>;
note: required by a bound in `cookie::Builder::<'a>::set_fortune`

The problem is that .into() does not know which type to target. The fix is to remove the .into().

Note that the need for such .into() calls was in fact only recently introduced, in the release of version 0.18. Probably we should have delayed that release until we had a solution like the present impl SetterInput generalization, thereby minimizing the churn of downstream code.

faster reflection

The 0.17 release added support for run-time reflection, including a DynamicStruct type that supports looking up fields by name. The initial implementation worked by linearly scanning a struct’s fields. That works fine for small structs, but can get expensive when there are a large number of fields.

In #469, @quartox updated the implementation to use binary search, resulting in a significant performance increase, and matching the capnproto-c++ implementation.

This change involved add a new field to the static RawStructSchema value included in the generated code for each Cap’n Proto type.

-- posted by dwrensha

capnproto-rust on github
more posts