Expand description
Serde serializer generating Rust code.
This crate can be used to “embed” something into code, having only some serialized
data, like JSON or YAML. This way, you’ll mostly escape runtime cost of deserialization,
nearly as if you’ve written the same data directly in code by hand.
Of course, in most cases this cost is already negligible, but for crates which use
large blobs of data this crate can come in handy, improving startup times, and can
eliminate the need for serde
as runtime dependency.
§Usage
In general, to embed some code into crate, you have to use the build script
and include!
macro. Inside the build script, you’ll generate
some code with one of the functions provided by uneval
,
and then include the generated file, like this:
let value = include!(concat!(env!(OUT_DIR), "/file_name.rs"));
§How does it work?
Of course, we can’t always directly construct the code for the desired value (more on this in the Limitations section below). However, in many cases the information provided by Serde is enough.
For every case, we’ll provide an example of how the generated code can look like, as a sequence of
let
statements, where the left part is written by hand and the right one is assumed to be generated.
§Primitives
Number literals, such as i8
or f32
, are directly written into the output. The only tricky part
is that we have to use suffixed literals, e.g. 1u8
or 1.1f64
- otherwise we’d run into the problem
with the float values which are in fact integers, since they would be output as integer literals,
not as float ones (i.e. 1
and not 1.0
) and so wouldn’t typecheck.
Boolean and character literals are also simply written directly - no surprises here.
Example:
let _: i8 = 12i8;
let _: u128 = 12345u128;
let _: f32 = -1f32;
let _: f64 = 12345.6789f64;
let _: char = 'c';
let _: bool = true;
§Strings
When Serde gives us something string-like, we have to make some kind of conversion, since
string literals are of type &'static str
, and string-like fields in serializable structs are
usually of some owned type, like String
. We assume that every such type would be convertible to
String
using Into
, so we simply emit a string literal with call to into
.
Example:
let _: String = "string value".into();
Byte strings are handled as byte sequences, as recommended by Serde itself, and so we’ll discuss them below.
§Tuple structs and unit values
Unit type (()
), unit structs and unit variants (including None
) are emitted simply by using
the type name. Tuple structs and variants (and newtype-flavored ones, including Some
)
are emitted by writing their name (with the enum name, if necessary), parenthesis,
and serializing the inner values.
Example:
struct TupleStruct((), Option<u8>, Option<u8>);
let _: TupleStruct = TupleStruct((), None, Some(1u8));
§Vec-like types (sequences)
Vec
-like structures are constructed using the temporary Vec
. We assume that every such type will
implement FromIterator
, so we emit the call to vec!
macro,
serialize the data and finalize the emit with call to into_iter().collect()
.
This is not exactly zero-cost, but it seems that this is the minimal.
Example:
let _: Vec<u32> = vec![1u32, 2u32, 3u32].into_iter().collect();
§Tuples and arrays
That’s where it becomes tricky.
The problem is that Serde doesn’t distinguish between this two kinds of values: they both are treated
as sequences with known length, called “tuples” internally; as a consequence, we don’t know at the emit time,
which of them we’ll be generating. But in the Rust code, they are created with entirely different syntax,
and there’s no easy way to convert one into another. So, we decided to emit a little “runtime”
(consisting of small #[inline]
functions, so it should in fact be zero-cost), which will
correctly handle the data according to the type being requested.
The idea is, in fact, directly borrowed from the collect
/FromIterator
pair: we can call collect
on every iterator value, and, as long as the target type implements FromIterator
with the necessary
parameters, collect
will do its job. We’re using not the trait method, but the free function (the reason is
that with the trait we would sometimes have a chain of type inferences, which Rust is unable to solve);
however, this doesn’t change the overall picture.
In general, here’s what being generated:
- A
FromTuple<T>
trait withfrom_tuple(input: T) -> Self
associated function. - Two implementations:
impl<T> FromTuple<(T,...,T,)> for [T; N]
andimpl<T1, ... TN> FromTuple<(T1,...TN,)> for (T1,...TN,)
. - Function
convert<T1, ... TN, Out: FromTuple<(T1,...TN,)>>(tuple: (T1,...TN,)) -> Out
, which simply callsOut::from_tuple(tuple)
.
Then, the value itself is created by the call to convert
, with tuple of serialized values as argument.
Depending on whether the target expects the array or tuple, convert
will select one particular implementation.
Example:
let tuple: (i32, f32, String) = {
trait FromTuple<T>: Sized {
fn from_tuple(tuple: T) -> Self;
}
impl<T> FromTuple<(T,T,T,)> for [T; 3] {
#[inline]
fn from_tuple(tuple: (T,T,T,)) -> Self {
[tuple.0,tuple.1,tuple.2]
}
}
impl<T0,T1,T2> FromTuple<(T0,T1,T2,)> for (T0,T1,T2,) {
#[inline]
fn from_tuple(tuple: (T0,T1,T2,)) -> Self {
tuple
}
}
#[inline]
fn convert<T0,T1,T2, Out: FromTuple<(T0,T1,T2,)>>(tuple: (T0,T1,T2,)) -> Out {
Out::from_tuple(tuple)
}
convert((1i32,1f32,"tuple entry".into()))
};
// Check that the tuple is indeed created as desired.
assert_eq!(tuple, (1i32,1f32,"tuple entry".to_string()));
let arr: [i32; 4] = {
trait FromTuple<T>: Sized {
fn from_tuple(tuple: T) -> Self;
}
impl<T> FromTuple<(T,T,T,T,)> for [T; 4] {
#[inline]
fn from_tuple(tuple: (T,T,T,T,)) -> Self {
[tuple.0,tuple.1,tuple.2,tuple.3]
}
}
impl<T0,T1,T2,T3> FromTuple<(T0,T1,T2,T3,)> for (T0,T1,T2,T3,) {
#[inline]
fn from_tuple(tuple: (T0,T1,T2,T3,)) -> Self {
tuple
}
}
#[inline]
fn convert<T0,T1,T2,T3, Out: FromTuple<(T0,T1,T2,T3,)>>(tuple: (T0,T1,T2,T3,)) -> Out {
Out::from_tuple(tuple)
}
convert((1,2,3,4))
};
// Check that the array is indeed created as desired.
assert_eq!(arr, [1, 2, 3, 4]);
§Zero-sized arrays
Since the code presented above would work only for non-empty tuples, we have to handle the “empty tuple” case differently. Fortunately for us, the “real” empty tuple is handled as a unit, so we can directly emit the code which yields an empty array:
let arr: [i32; 0] = {
#[inline]
fn convert<T>(_: ()) -> [T; 0] {
[]
}
convert(())
};
§Maps
Since Rust doesn’t have the notion of map literals, we can’t construct one directly. However, standard map-like
types (HashMap
, BTreeMap
) implement FromIterator<(K, V)>
, i.e. they can be built from the iterator of
key-value pairs. uneval
generates code according to this convention: we create a Vec
of pairs, which is then
converted into map with into_iter().collect()
.
Example:
let _: std::collections::HashMap<i32, String> = vec![
(1, "first".into()),
(100, "one hundredth".into()),
].into_iter().collect();
§Structs
Last but not the least, this case is relatively simple. Emitted code is simply the struct construction -
i.e. the struct name, the curly braces and a list of pairs of the form {field name}: {serialized value}
.
Example:
struct Struct { boolean: bool, number: i32, string: String }
let _: Struct = Struct {
boolean: true,
number: 1i32,
string: "string".into()
};
§Limitations
There are some cases when uneval
will be unable to generate valid code. Namely:
- Since Serde doesn’t provide us the full path to the type in question (and in most cases it’s simply unable to), all the structs and enums used during value construction must be in scope. As a consequence, all of them must have distinct names - otherwise, there will be name clashes.
- This serializer is intended for use with derived implementation. It may return bogus results
when used with customized
Serialize
. - It is impossible to consume code for the type with private fields outside from the module it is defined in.
In fact, to be able to use this type with
uneval
, you’ll have to distribute two copies of your crate, one of which would only export the definition with derivedSerialize
to be used by serializer during the build-time of the second copy. (Isn’t this a bit too complex?)
Re-exports§
pub use funcs::to_file;
pub use funcs::to_out_dir;
pub use funcs::to_string;
pub use funcs::write;
Modules§
- Error returned by Uneval serializer.
- Convenience functions to be used with Uneval.
- Implementation of the Uneval serializer.