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 with from_tuple(input: T) -> Self associated function.
  • Two implementations: impl<T> FromTuple<(T,...,T,)> for [T; N] and impl<T1, ... TN> FromTuple<(T1,...TN,)> for (T1,...TN,).
  • Function convert<T1, ... TN, Out: FromTuple<(T1,...TN,)>>(tuple: (T1,...TN,)) -> Out, which simply calls Out::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:

  1. 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.
  2. This serializer is intended for use with derived implementation. It may return bogus results when used with customized Serialize.
  3. 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 derived Serialize 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.