hydro_lang/sim/codec.rs
1//! Serialization codecs for simulation inputs and outputs.
2
3use serde::Serialize;
4use serde::de::DeserializeOwned;
5#[cfg(stageleft_runtime)]
6use stageleft::quote_type;
7#[cfg(stageleft_runtime)]
8use syn::parse_quote;
9
10#[cfg(stageleft_runtime)]
11use crate::staging_util::get_this_crate;
12
13/// A serialization codec for values crossing the simulation dylib boundary.
14///
15/// The test and simulation dylib exchange only encoded bytes, so custom codecs do not need to
16/// use [`serde`]. See [`RmpCodec`] for the default.
17///
18/// The codec is selected purely at the type level, as a type parameter of
19/// [`Stream::sim_output_with`], [`Location::sim_input_with`] and their cluster
20/// counterparts (e.g. `stream.sim_output_with::<MyCodec>()`). No codec value is ever
21/// constructed, so a codec is typically a unit struct, but any type will do.
22///
23/// [`Stream::sim_output_with`]: crate::prelude::Stream::sim_output_with
24/// [`Location::sim_input_with`]: crate::location::Location::sim_input_with
25///
26/// # Defining a custom codec
27///
28/// Generated dylib code refers to the codec by its definition path. The codec must therefore:
29///
30/// * be public through to the crate root, and
31/// * live outside `#[cfg(test)]` and integration-test targets.
32///
33/// Its serialization library and `hydro_lang` with the `sim` feature must be regular
34/// dependencies, not dev-dependencies.
35///
36/// ```
37/// use hydro_lang::sim::codec::SimCodec;
38///
39/// pub struct Message(u32);
40///
41/// pub struct MessageCodec;
42///
43/// impl SimCodec<Message> for MessageCodec {
44/// fn encode(value: &Message) -> Vec<u8> {
45/// value.0.to_le_bytes().to_vec()
46/// }
47///
48/// fn decode(bytes: &[u8]) -> Message {
49/// Message(u32::from_le_bytes(bytes.try_into().unwrap()))
50/// }
51/// }
52/// ```
53pub trait SimCodec<T> {
54 /// Encodes `value` for transport across the simulation dylib boundary.
55 fn encode(value: &T) -> Vec<u8>;
56
57 /// Decodes a value received across the simulation dylib boundary.
58 fn decode(bytes: &[u8]) -> T;
59}
60
61/// The default simulation codec, using [MessagePack](https://msgpack.org/) via [`rmp_serde`].
62///
63/// Values are encoded with named struct fields ([`rmp_serde::to_vec_named`]), making the
64/// encoding self-describing. Unlike `bincode` (the previous default), this supports serde
65/// attributes that require `deserialize_any`, such as `#[serde(untagged)]` and
66/// `#[serde(flatten)]`.
67#[derive(Clone, Copy, Debug, Default)]
68pub struct RmpCodec;
69
70impl<T: Serialize + DeserializeOwned> SimCodec<T> for RmpCodec {
71 fn encode(value: &T) -> Vec<u8> {
72 rmp_serde::to_vec_named(value).unwrap()
73 }
74
75 fn decode(bytes: &[u8]) -> T {
76 rmp_serde::from_slice(bytes).unwrap()
77 }
78}
79
80#[cfg(stageleft_runtime)]
81pub(crate) fn staged_serialize<T, C: SimCodec<T>>() -> syn::Expr {
82 let root = get_this_crate();
83 let t_type = quote_type::<T>();
84 let codec_type = quote_type::<C>();
85
86 parse_quote! {
87 #root::runtime_support::stageleft::runtime_support::fn1_type_hint::<#t_type, _>(
88 |data| {
89 #root::runtime_support::dfir_rs::bytes::Bytes::from(
90 <#codec_type as #root::__staged::sim::codec::SimCodec<#t_type>>::encode(&data)
91 )
92 }
93 )
94 }
95}
96
97#[cfg(stageleft_runtime)]
98pub(crate) fn staged_deserialize<T, C: SimCodec<T>>() -> syn::Expr {
99 let root = get_this_crate();
100 let t_type = quote_type::<T>();
101 let codec_type = quote_type::<C>();
102
103 parse_quote! {
104 |res| {
105 let bytes = res.unwrap();
106 <#codec_type as #root::__staged::sim::codec::SimCodec<#t_type>>::decode(&bytes)
107 }
108 }
109}