Skip to main content

wincode/schema/
tag_encoding.rs

1//! Tag encoding configuration.
2use {
3    crate::{
4        SchemaRead, SchemaWrite, WriteResult,
5        config::ConfigCore,
6        error::{TagEncodingOverflow, tag_encoding_overflow},
7        io::Writer,
8    },
9    core::any::type_name,
10};
11
12/// Tag encoding trait.
13///
14/// This trait normalizes discriminant encodings to a common type, `u32`.
15/// The reason for this is that `SchemaRead` and `SchemaWrite` implementations
16/// for enums need to match on explicit integer literals.
17///
18/// For example,
19/// ```compile_fail
20/// # use wincode::{SchemaRead, SchemaWrite, config::Config, io::Reader, ReadResult};
21/// # use core::mem::MaybeUninit;
22/// enum Foo {
23///     Bar,
24///     Baz,
25/// }
26///
27/// unsafe impl<'de, C: Config> SchemaRead<'de, C> for Foo {
28///     type Dst = Self;
29///
30///     fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
31///         let tag = C::TagEncoding::get(reader)?;
32///         // Cannot match a generic type with an integer literal.
33///         match tag {
34///             0 => {}
35///             1 => {}
36///             // ...
37///         }
38///         Ok(())
39///     }
40/// }
41/// ```
42///
43/// It is not possible to match on a generic type with an integer literal.
44/// This is because we have no way of telling the compiler that a trait
45/// is represented by a closed set of integer types.
46///
47/// By normalizing the discriminant encoding to a common type, we can
48/// get around this limitation.
49///
50/// ```
51/// # use wincode::{
52/// #     SchemaRead, SchemaWrite,
53/// #     config::Config,
54/// #     io::Reader, ReadResult,
55/// #     tag_encoding::TagEncoding,
56/// #     error::invalid_tag_encoding,
57/// # };
58/// # use core::mem::MaybeUninit;
59/// enum Foo {
60///     Bar,
61///     Baz,
62/// }
63///
64/// unsafe impl<'de, C: Config> SchemaRead<'de, C> for Foo {
65///     type Dst = Self;
66///
67///     fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
68///         let tag = C::TagEncoding::try_into_u32(C::TagEncoding::get(reader)?)?;
69///         // Now we can match on integer literals.
70///         match tag {
71///             0 => {
72///                 // ...
73///             }
74///             1 => {
75///                 // ...
76///             }
77///             _ => {
78///                 return Err(invalid_tag_encoding(tag as usize));
79///             }
80///         }
81///
82///         Ok(())
83///     }
84/// }
85/// ```
86///
87/// A note on performance: in release builds, the generated assembly for this scheme
88/// typically elides conversions to and from the intermediate `u32` type.
89/// Because `TagEncoding` is ultimately monomorphized into concrete integer targets,
90/// the result of `try_from`/`try_into` calls are known at compile time.
91/// All reads, matches, and writes on enums in the crate (including derive macros)
92/// use compile-time integer literals, and in practice it was observed that tags are
93/// loaded at their original width and compared directly to immediates.
94pub trait TagEncoding<C: ConfigCore>:
95    for<'de> SchemaRead<'de, C, Dst = Self::Target> + SchemaWrite<C, Src = Self::Target> + 'static
96{
97    type Target;
98
99    /// Convert a `u32` to the encoding target.
100    fn try_from_u32(value: u32) -> Result<Self::Target, TagEncodingOverflow>;
101
102    /// Convert the encoding target to a `u32`.
103    fn try_into_u32(x: Self::Target) -> Result<u32, TagEncodingOverflow>;
104
105    /// Get the size of the encoding target from the given `u32`.
106    ///
107    /// The `u32` will be converted to the encoding target before calling
108    /// [`SchemaWrite::size_of`] on the target implementation.
109    #[inline(always)]
110    fn size_of_from_u32(value: u32) -> WriteResult<usize> {
111        Self::size_of(&Self::try_from_u32(value)?)
112    }
113
114    /// Write the encoding target from the given `u32` to the given [`Writer`].
115    ///
116    /// The `u32` will be converted to the encoding target before calling
117    /// [`SchemaWrite::write`] on the target implementation.
118    #[inline(always)]
119    fn write_from_u32(writer: impl Writer, value: u32) -> WriteResult<()> {
120        Self::write(writer, &Self::try_from_u32(value)?)
121    }
122}
123
124impl<T, Target, C: ConfigCore> TagEncoding<C> for T
125where
126    T: for<'de> SchemaRead<'de, C, Dst = Target> + SchemaWrite<C, Src = Target> + 'static,
127    Target: TryFrom<u32>,
128    u32: TryFrom<Target>,
129{
130    type Target = Target;
131
132    #[inline(always)]
133    fn try_from_u32(value: u32) -> Result<Self::Target, TagEncodingOverflow> {
134        Target::try_from(value).map_err(|_| tag_encoding_overflow(type_name::<Target>()))
135    }
136
137    #[inline(always)]
138    fn try_into_u32(x: Self::Target) -> Result<u32, TagEncodingOverflow> {
139        u32::try_from(x).map_err(|_| tag_encoding_overflow(type_name::<Target>()))
140    }
141}