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.
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]
.
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.
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.