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}