From f18676a3eaaed5935939b12d5c9eda3f92f30a9f Mon Sep 17 00:00:00 2001 From: liuqiang Date: Thu, 3 Sep 2026 13:57:09 +0800 Subject: [PATCH] feat: add SonicDeserialize derive --- Cargo.toml | 6 + sonic-derive/Cargo.toml | 15 + sonic-derive/src/attr.rs | 142 +++++++++ sonic-derive/src/codegen.rs | 474 ++++++++++++++++++++++++++++ sonic-derive/src/lib.rs | 121 +++++++ sonic-derive/src/model.rs | 122 +++++++ src/__private.rs | 48 +++ src/lib.rs | 8 + tests/deserialize_with_benchmark.rs | 286 +++++++++++++++++ tests/sonic_deserialize.rs | 461 +++++++++++++++++++++++++++ 10 files changed, 1683 insertions(+) create mode 100644 sonic-derive/Cargo.toml create mode 100644 sonic-derive/src/attr.rs create mode 100644 sonic-derive/src/codegen.rs create mode 100644 sonic-derive/src/lib.rs create mode 100644 sonic-derive/src/model.rs create mode 100644 src/__private.rs create mode 100644 tests/deserialize_with_benchmark.rs create mode 100644 tests/sonic_deserialize.rs diff --git a/Cargo.toml b/Cargo.toml index f3727650..be0e7c17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,11 @@ bytes = "1.10" cfg-if = "1.0" faststr = { version = "0.2", features = ["serde"] } itoa = "1.0" +phf = { version = "0.11", features = ["macros"], optional = true } ref-cast = "1.0" serde = { version = "1.0", features = ["rc", "derive"] } simdutf8 = "0.1" +sonic-derive = { path = "sonic-derive", version = "0.1.0", optional = true } sonic-number = { path = "./sonic-number", version = "0.1" } sonic-simd = { path = "./sonic-simd", version = "0.1" } thiserror = "2.0" @@ -42,6 +44,10 @@ serde_json = { version = "1.0", features = ["float_roundtrip", "raw_value"] } [features] default = [] +# Generate an official `serde::Deserialize` implementation with compile-time +# field dispatch for wide named structs. +derive = ["dep:phf", "dep:sonic-derive"] + # Use an arbitrary precision number type representation when parsing JSON into `sonic_rs::Value`. # This allows the JSON numbers will be serialized without loss of precision. arbitrary_precision = [] diff --git a/sonic-derive/Cargo.toml b/sonic-derive/Cargo.toml new file mode 100644 index 00000000..eb04439e --- /dev/null +++ b/sonic-derive/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sonic-derive" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +description = "High-performance Serde-compatible derive for sonic-rs" +repository = "https://github.com/cloudwego/sonic-rs" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full", "parsing"] } diff --git a/sonic-derive/src/attr.rs b/sonic-derive/src/attr.rs new file mode 100644 index 00000000..7c16abd6 --- /dev/null +++ b/sonic-derive/src/attr.rs @@ -0,0 +1,142 @@ +use syn::{meta::ParseNestedMeta, Attribute, LitStr, Path, Result, Token}; + +#[derive(Clone, Default)] +pub(crate) enum DefaultKind { + #[default] + None, + Default, + Path(Path), +} + +#[derive(Default)] +pub(crate) struct ContainerAttrs { + pub(crate) default: DefaultKind, + pub(crate) deny_unknown_fields: bool, + pub(crate) expecting: Option, + pub(crate) name: Option, + pub(crate) serde_path: Option, + pub(crate) sonic_path: Option, + pub(crate) force_phf: bool, +} + +#[derive(Default)] +pub(crate) struct FieldAttrs { + pub(crate) aliases: Vec, + pub(crate) default: DefaultKind, + pub(crate) deserialize_name: Option, + pub(crate) deserialize_with: Option, + pub(crate) skip_deserializing: bool, +} + +pub(crate) fn parse_container_attrs(attrs: &[Attribute]) -> Result { + let mut out = ContainerAttrs::default(); + for attr in attrs { + if attr.path().is_ident("sonic") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("force_phf") { + out.force_phf = true; + Ok(()) + } else if meta.path.is_ident("crate") { + out.sonic_path = Some(parse_lit_str(meta)?.parse()?); + Ok(()) + } else { + Err(meta.error("unsupported #[sonic(...)] option")) + } + })?; + } else if attr.path().is_ident("serde") { + attr.parse_nested_meta(|meta| parse_container_serde_meta(meta, &mut out))?; + } + } + Ok(out) +} + +pub(crate) fn parse_field_attrs(attrs: &[Attribute]) -> Result { + let mut out = FieldAttrs::default(); + for attr in attrs { + if attr.path().is_ident("sonic") { + return Err(syn::Error::new_spanned( + attr, + "SonicDeserialize has no field-level #[sonic(...)] options", + )); + } + if attr.path().is_ident("serde") { + attr.parse_nested_meta(|meta| parse_field_serde_meta(meta, &mut out))?; + } + } + Ok(out) +} + +fn parse_container_serde_meta(meta: ParseNestedMeta<'_>, out: &mut ContainerAttrs) -> Result<()> { + if meta.path.is_ident("default") { + out.default = parse_default(meta)?; + } else if meta.path.is_ident("deny_unknown_fields") { + out.deny_unknown_fields = true; + } else if meta.path.is_ident("expecting") { + out.expecting = Some(parse_lit_str(meta)?.value()); + } else if meta.path.is_ident("crate") { + out.serde_path = Some(parse_lit_str(meta)?.parse()?); + } else if meta.path.is_ident("rename") { + parse_rename(meta, &mut out.name)?; + } else { + return Err( + meta.error("SonicDeserialize MVP does not support this container-level Serde option") + ); + } + Ok(()) +} + +fn parse_field_serde_meta(meta: ParseNestedMeta<'_>, out: &mut FieldAttrs) -> Result<()> { + if meta.path.is_ident("rename") { + parse_rename(meta, &mut out.deserialize_name)?; + } else if meta.path.is_ident("alias") { + out.aliases.push(parse_lit_str(meta)?.value()); + } else if meta.path.is_ident("default") { + out.default = parse_default(meta)?; + } else if meta.path.is_ident("deserialize_with") { + out.deserialize_with = Some(parse_lit_str(meta)?.parse()?); + } else if meta.path.is_ident("skip") || meta.path.is_ident("skip_deserializing") { + out.skip_deserializing = true; + } else if meta.path.is_ident("skip_serializing") { + // Serialization-only option; accepted because this derive does not own Serialize. + } else if meta.path.is_ident("skip_serializing_if") + || meta.path.is_ident("serialize_with") + || meta.path.is_ident("getter") + { + let _ = parse_lit_str(meta)?; + } else { + return Err( + meta.error("SonicDeserialize MVP does not support this field-level Serde option") + ); + } + Ok(()) +} + +fn parse_rename(meta: ParseNestedMeta<'_>, target: &mut Option) -> Result<()> { + if meta.input.peek(Token![=]) { + *target = Some(parse_lit_str(meta)?.value()); + return Ok(()); + } + meta.parse_nested_meta(|nested| { + if nested.path.is_ident("deserialize") { + *target = Some(parse_lit_str(nested)?.value()); + Ok(()) + } else if nested.path.is_ident("serialize") { + let _ = parse_lit_str(nested)?; + Ok(()) + } else { + Err(nested.error("expected `serialize` or `deserialize`")) + } + }) +} + +fn parse_default(meta: ParseNestedMeta<'_>) -> Result { + if meta.input.peek(Token![=]) { + Ok(DefaultKind::Path(parse_lit_str(meta)?.parse()?)) + } else { + Ok(DefaultKind::Default) + } +} + +fn parse_lit_str(meta: ParseNestedMeta<'_>) -> Result { + meta.value()?.parse() +} diff --git a/sonic-derive/src/codegen.rs b/sonic-derive/src/codegen.rs new file mode 100644 index 00000000..75cc29da --- /dev/null +++ b/sonic-derive/src/codegen.rs @@ -0,0 +1,474 @@ +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote, ToTokens}; +use syn::{Ident, LitStr, Path}; + +use crate::{ + attr::DefaultKind, + model::{FieldInfo, StructModel}, +}; + +const PHF_THRESHOLD: usize = 32; + +pub(crate) fn expand(model: &StructModel) -> TokenStream2 { + let struct_ident = &model.ident; + let container = &model.container; + let serde_path = container + .serde_path + .clone() + .unwrap_or_else(|| syn::parse_str("::serde").expect("valid serde path")); + let sonic_path: TokenStream2 = container + .sonic_path + .clone() + .unwrap_or_else(|| syn::parse_str("::sonic_rs").expect("valid sonic-rs path")) + .into_token_stream(); + let use_phf = container.force_phf || model.accepted.len() >= PHF_THRESHOLD; + + let field_names: Vec<_> = model + .fields + .iter() + .filter(|field| field.de_id.is_some()) + .flat_map(|field| field.names.iter()) + .map(|name| LitStr::new(name, Span::call_site())) + .collect(); + let numeric_entries = numeric_field_entries(&model.fields); + let accepted_entries: Vec<_> = model + .accepted + .iter() + .map(|(name, id)| (LitStr::new(name, Span::call_site()), *id as u32)) + .collect(); + + let de_fields: Vec<_> = model + .fields + .iter() + .filter_map(|field| field.de_id.map(|id| (field, id))) + .collect(); + let field_variants: Vec<_> = de_fields + .iter() + .map(|(_, id)| format_ident!("__field{id}")) + .collect(); + let ignore_variant = (!container.deny_unknown_fields).then(|| quote!(__ignore,)); + + let lookup = field_lookup(use_phf, &accepted_entries, &sonic_path); + let id_to_variant = de_fields.iter().map(|(_, id)| { + let id = *id as u32; + let variant = format_ident!("__field{id}"); + quote!(::core::option::Option::Some(#id) => + ::core::result::Result::Ok(__Field::#variant)) + }); + let numeric_to_variant = numeric_entries.iter().map(|(fields_index, id)| { + let numeric = *fields_index; + let variant = format_ident!("__field{id}"); + quote!(#numeric => ::core::result::Result::Ok(__Field::#variant)) + }); + + let unknown_str = if container.deny_unknown_fields { + quote!(::core::result::Result::Err( + <__E as #serde_path::de::Error>::unknown_field(__value, FIELDS))) + } else { + quote!(::core::result::Result::Ok(__Field::__ignore)) + }; + let unknown_numeric = if container.deny_unknown_fields { + let msg = LitStr::new( + &format!("field index 0 <= i < {}", field_names.len()), + Span::call_site(), + ); + quote!(::core::result::Result::Err( + <__E as #serde_path::de::Error>::invalid_value( + #serde_path::de::Unexpected::Unsigned(__value), &#msg))) + } else { + quote!(::core::result::Result::Ok(__Field::__ignore)) + }; + let invalid_bytes = if container.deny_unknown_fields { + quote!(::core::result::Result::Err( + <__E as #serde_path::de::Error>::invalid_value( + #serde_path::de::Unexpected::Bytes(__value), &self))) + } else { + quote!(::core::result::Result::Ok(__Field::__ignore)) + }; + + let wrappers = de_fields.iter().filter_map(|(field, id)| { + let path = field.attrs.deserialize_with.as_ref()?; + let wrapper = format_ident!("__SonicWith{id}"); + let ty = &field.ty; + Some(quote! { + struct #wrapper { + value: #ty, + } + + impl<'de> #serde_path::Deserialize<'de> for #wrapper { + #[inline] + fn deserialize<__D>(__deserializer: __D) + -> ::core::result::Result + where + __D: #serde_path::Deserializer<'de>, + { + ::core::result::Result::Ok(#wrapper { + value: #path(__deserializer)?, + }) + } + } + }) + }); + + let default_init = container_default_init(&container.default, struct_ident); + let map_declarations = de_fields.iter().map(|(field, id)| { + let var = format_ident!("__field{id}"); + let ty = &field.ty; + quote!(let mut #var: ::core::option::Option<#ty> = ::core::option::Option::None;) + }); + let map_arms = de_fields.iter().map(|(field, id)| { + let var = format_ident!("__field{id}"); + let variant = format_ident!("__field{id}"); + let canonical = LitStr::new(&field.canonical, Span::call_site()); + let read = if field.attrs.deserialize_with.is_some() { + let wrapper = format_ident!("__SonicWith{id}"); + quote!(#serde_path::de::MapAccess::next_value::<#wrapper>(&mut __map)?.value) + } else { + let ty = &field.ty; + quote!(#serde_path::de::MapAccess::next_value::<#ty>(&mut __map)?) + }; + quote! { + __Field::#variant => { + if ::core::option::Option::is_some(&#var) { + return ::core::result::Result::Err( + <__A::Error as #serde_path::de::Error>::duplicate_field(#canonical)); + } + #var = ::core::option::Option::Some(#read); + } + } + }); + let ignored_map_arm = (!container.deny_unknown_fields).then(|| { + quote! { + __Field::__ignore => { + let _ = #serde_path::de::MapAccess::next_value::<#serde_path::de::IgnoredAny>( + &mut __map, + )?; + } + } + }); + + let map_finalize = de_fields.iter().map(|(field, id)| { + let var = format_ident!("__field{id}"); + let missing = missing_map_expr(field, &container.default, &serde_path, &sonic_path); + quote! { + let #var = match #var { + ::core::option::Option::Some(__value) => __value, + ::core::option::Option::None => #missing, + }; + } + }); + + let mut seq_index = 0usize; + let seq_reads: Vec<_> = model + .fields + .iter() + .enumerate() + .map(|(field_index, field)| { + let var = format_ident!("__seq_field{field_index}"); + if let Some(id) = field.de_id { + let read = if field.attrs.deserialize_with.is_some() { + let wrapper = format_ident!("__SonicWith{id}"); + quote!(::core::option::Option::map( + #serde_path::de::SeqAccess::next_element::<#wrapper>(&mut __seq)?, + |__wrap| __wrap.value, + )) + } else { + let ty = &field.ty; + quote!(#serde_path::de::SeqAccess::next_element::<#ty>(&mut __seq)?) + }; + let missing = missing_seq_expr( + field, + &container.default, + seq_index, + &model.expecting, + &serde_path, + ); + seq_index += 1; + quote! { + let #var = match #read { + ::core::option::Option::Some(__value) => __value, + ::core::option::Option::None => #missing, + }; + } + } else { + let missing = skipped_expr(field, &container.default); + quote!(let #var = #missing;) + } + }) + .collect(); + + let map_construct_fields = model.fields.iter().map(|field| { + let ident = &field.ident; + if let Some(id) = field.de_id { + let value = format_ident!("__field{id}"); + quote!(#ident: #value) + } else { + let skipped = skipped_expr(field, &container.default); + quote!(#ident: #skipped) + } + }); + let seq_construct_fields = model.fields.iter().enumerate().map(|(index, field)| { + let ident = &field.ident; + let var = format_ident!("__seq_field{index}"); + quote!(#ident: #var) + }); + + let type_name = LitStr::new(&model.type_name, Span::call_site()); + let expecting = LitStr::new(&model.expecting, Span::call_site()); + + quote! { + #[automatically_derived] + impl<'de> #serde_path::Deserialize<'de> for #struct_ident { + #[inline] + fn deserialize<__D>(__deserializer: __D) + -> ::core::result::Result + where + __D: #serde_path::Deserializer<'de>, + { + #(#wrappers)* + + #[allow(non_camel_case_types)] + enum __Field { + #(#field_variants,)* + #ignore_variant + } + + struct __FieldVisitor; + + impl<'de> #serde_path::de::Visitor<'de> for __FieldVisitor { + type Value = __Field; + + fn expecting(&self, __formatter: &mut ::core::fmt::Formatter) + -> ::core::fmt::Result + { + __formatter.write_str("field identifier") + } + + #[inline] + fn visit_u64<__E>(self, __value: u64) + -> ::core::result::Result + where + __E: #serde_path::de::Error, + { + match __value { + #(#numeric_to_variant,)* + _ => #unknown_numeric, + } + } + + #[inline] + fn visit_str<__E>(self, __value: &str) + -> ::core::result::Result + where + __E: #serde_path::de::Error, + { + #lookup + match __field_id { + #(#id_to_variant,)* + _ => #unknown_str, + } + } + + #[inline] + fn visit_bytes<__E>(self, __value: &[u8]) + -> ::core::result::Result + where + __E: #serde_path::de::Error, + { + match ::core::str::from_utf8(__value) { + ::core::result::Result::Ok(__value) => + #serde_path::de::Visitor::visit_str(self, __value), + ::core::result::Result::Err(_) => #invalid_bytes, + } + } + } + + impl<'de> #serde_path::Deserialize<'de> for __Field { + #[inline] + fn deserialize<__D>(__deserializer: __D) + -> ::core::result::Result + where + __D: #serde_path::Deserializer<'de>, + { + #serde_path::Deserializer::deserialize_identifier( + __deserializer, + __FieldVisitor, + ) + } + } + + struct __Visitor { + marker: ::core::marker::PhantomData<#struct_ident>, + } + + impl<'de> #serde_path::de::Visitor<'de> for __Visitor { + type Value = #struct_ident; + + fn expecting(&self, __formatter: &mut ::core::fmt::Formatter) + -> ::core::fmt::Result + { + __formatter.write_str(#expecting) + } + + #[inline] + fn visit_seq<__A>(self, mut __seq: __A) + -> ::core::result::Result + where + __A: #serde_path::de::SeqAccess<'de>, + { + #default_init + #(#seq_reads)* + ::core::result::Result::Ok(#struct_ident { + #(#seq_construct_fields,)* + }) + } + + #[inline] + fn visit_map<__A>(self, mut __map: __A) + -> ::core::result::Result + where + __A: #serde_path::de::MapAccess<'de>, + { + #(#map_declarations)* + while let ::core::option::Option::Some(__key) = + #serde_path::de::MapAccess::next_key::<__Field>(&mut __map)? + { + match __key { + #(#map_arms)* + #ignored_map_arm + } + } + #default_init + #(#map_finalize)* + ::core::result::Result::Ok(#struct_ident { + #(#map_construct_fields,)* + }) + } + } + + const FIELDS: &'static [&'static str] = &[#(#field_names,)*]; + #serde_path::Deserializer::deserialize_struct( + __deserializer, + #type_name, + FIELDS, + __Visitor { marker: ::core::marker::PhantomData }, + ) + } + } + } +} + +fn field_lookup( + use_phf: bool, + accepted_entries: &[(LitStr, u32)], + sonic_path: &TokenStream2, +) -> TokenStream2 { + let keys = accepted_entries.iter().map(|(name, _)| name); + let ids = accepted_entries.iter().map(|(_, id)| id); + if use_phf { + quote! { + static __SONIC_FIELDS: #sonic_path::__private::phf::Map<&'static str, u32> = + #sonic_path::__private::phf::phf_map! { + #(#keys => #ids,)* + }; + let __field_id = __SONIC_FIELDS.get(__value).copied(); + } + } else { + quote! { + let __field_id = match __value { + #(#keys => ::core::option::Option::Some(#ids),)* + _ => ::core::option::Option::None, + }; + } + } +} + +fn numeric_field_entries(fields: &[FieldInfo]) -> Vec<(u64, usize)> { + let mut fields_index = 0u64; + let mut entries = Vec::new(); + for field in fields { + if let Some(id) = field.de_id { + for _ in &field.names { + entries.push((fields_index, id)); + fields_index += 1; + } + } + } + entries +} + +fn container_default_init(default: &DefaultKind, struct_ident: &Ident) -> TokenStream2 { + match default { + DefaultKind::None => quote!(), + DefaultKind::Default => { + quote!(let __default: #struct_ident = ::core::default::Default::default();) + } + DefaultKind::Path(path) => quote!(let __default: #struct_ident = #path();), + } +} + +fn missing_map_expr( + field: &FieldInfo, + container_default: &DefaultKind, + serde_path: &Path, + sonic_path: &TokenStream2, +) -> TokenStream2 { + if let Some(expr) = explicit_default_expr(&field.attrs.default) { + return expr; + } + if !matches!(container_default, DefaultKind::None) { + let ident = &field.ident; + return quote!(__default.#ident); + } + let canonical = LitStr::new(&field.canonical, Span::call_site()); + if field.attrs.deserialize_with.is_some() { + quote! { + return ::core::result::Result::Err( + <__A::Error as #serde_path::de::Error>::missing_field(#canonical)) + } + } else { + let ty = &field.ty; + quote!(#sonic_path::__private::missing_field::<#ty, __A::Error>(#canonical)?) + } +} + +fn missing_seq_expr( + field: &FieldInfo, + container_default: &DefaultKind, + index: usize, + expecting: &str, + serde_path: &Path, +) -> TokenStream2 { + if let Some(expr) = explicit_default_expr(&field.attrs.default) { + return expr; + } + if !matches!(container_default, DefaultKind::None) { + let ident = &field.ident; + return quote!(__default.#ident); + } + let expecting = LitStr::new(expecting, Span::call_site()); + quote! { + return ::core::result::Result::Err( + <__A::Error as #serde_path::de::Error>::invalid_length(#index, &#expecting)) + } +} + +fn skipped_expr(field: &FieldInfo, container_default: &DefaultKind) -> TokenStream2 { + if let Some(expr) = explicit_default_expr(&field.attrs.default) { + return expr; + } + if !matches!(container_default, DefaultKind::None) { + let ident = &field.ident; + quote!(__default.#ident) + } else { + quote!(::core::default::Default::default()) + } +} + +fn explicit_default_expr(default: &DefaultKind) -> Option { + match default { + DefaultKind::None => None, + DefaultKind::Default => Some(quote!(::core::default::Default::default())), + DefaultKind::Path(path) => Some(quote!(#path())), + } +} diff --git a/sonic-derive/src/lib.rs b/sonic-derive/src/lib.rs new file mode 100644 index 00000000..a9333062 --- /dev/null +++ b/sonic-derive/src/lib.rs @@ -0,0 +1,121 @@ +//! `SonicDeserialize` derives the official `serde::Deserialize` trait while +//! replacing wide generated field-name matches with a compile-time PHF table. + +use proc_macro::TokenStream; +use syn::{parse_macro_input, DeriveInput, Result}; + +mod attr; +mod codegen; +mod model; + +#[proc_macro_derive(SonicDeserialize, attributes(serde, sonic))] +pub fn derive_sonic_deserialize(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +fn expand(input: DeriveInput) -> Result { + let model = model::StructModel::from_input(input)?; + Ok(codegen::expand(&model)) +} + +#[cfg(test)] +mod tests { + use syn::parse_quote; + + use super::*; + + fn assert_rejects(input: DeriveInput, expected: &str) { + let error = expand(input).unwrap_err().to_string(); + assert!(error.contains(expected), "{error}"); + } + + #[test] + fn rejects_generics_instead_of_guessing_bounds() { + assert_rejects( + parse_quote! { + struct Generic { value: T } + }, + "non-generic", + ); + } + + #[test] + fn rejects_flatten_instead_of_changing_buffering_semantics() { + assert_rejects( + parse_quote! { + struct Flattened { + #[serde(flatten)] + extra: std::collections::BTreeMap, + } + }, + "field-level Serde option", + ); + } + + #[test] + fn rejects_enums_until_variant_codegen_is_implemented() { + assert_rejects( + parse_quote! { + enum Choice { A, B } + }, + "does not yet support enums", + ); + } + + #[test] + fn rejects_container_options_that_change_field_or_type_semantics() { + assert_rejects( + parse_quote! { + #[serde(rename_all = "camelCase")] + struct RenameAll { value: i64 } + }, + "container-level Serde option", + ); + assert_rejects( + parse_quote! { + #[serde(transparent)] + struct Transparent { value: i64 } + }, + "container-level Serde option", + ); + assert_rejects( + parse_quote! { + #[serde(from = "i64")] + struct FromAttr { value: i64 } + }, + "container-level Serde option", + ); + assert_rejects( + parse_quote! { + #[serde(try_from = "i64")] + struct TryFromAttr { value: i64 } + }, + "container-level Serde option", + ); + } + + #[test] + fn rejects_field_options_that_need_extra_lifetime_or_buffering_logic() { + assert_rejects( + parse_quote! { + struct Borrowed { + #[serde(borrow)] + value: std::borrow::Cow<'static, str>, + } + }, + "field-level Serde option", + ); + assert_rejects( + parse_quote! { + struct Bounded { + #[serde(bound(deserialize = "String: serde::Deserialize<'de>"))] + value: String, + } + }, + "field-level Serde option", + ); + } +} diff --git a/sonic-derive/src/model.rs b/sonic-derive/src/model.rs new file mode 100644 index 00000000..9a07884e --- /dev/null +++ b/sonic-derive/src/model.rs @@ -0,0 +1,122 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use proc_macro2::Ident; +use syn::{Data, DeriveInput, Fields, Result, Type}; + +use crate::attr::{parse_container_attrs, parse_field_attrs, ContainerAttrs, FieldAttrs}; + +pub(crate) struct FieldInfo { + pub(crate) ident: Ident, + pub(crate) ty: Type, + pub(crate) attrs: FieldAttrs, + pub(crate) canonical: String, + pub(crate) names: Vec, + pub(crate) de_id: Option, +} + +pub(crate) struct StructModel { + pub(crate) ident: Ident, + pub(crate) container: ContainerAttrs, + pub(crate) fields: Vec, + pub(crate) accepted: BTreeMap, + pub(crate) type_name: String, + pub(crate) expecting: String, +} + +impl StructModel { + pub(crate) fn from_input(input: DeriveInput) -> Result { + if !input.generics.params.is_empty() || input.generics.where_clause.is_some() { + return Err(syn::Error::new_spanned( + &input.generics, + "SonicDeserialize MVP supports only non-generic, non-borrowing structs", + )); + } + + let named = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => fields, + _ => { + return Err(syn::Error::new_spanned( + &data.fields, + "SonicDeserialize MVP supports only named structs", + )) + } + }, + Data::Enum(data) => { + return Err(syn::Error::new_spanned( + data.enum_token, + "SonicDeserialize MVP does not yet support enums", + )) + } + Data::Union(data) => { + return Err(syn::Error::new_spanned( + data.union_token, + "SonicDeserialize does not support unions", + )) + } + }; + + let container = parse_container_attrs(&input.attrs)?; + let mut fields = Vec::with_capacity(named.named.len()); + let mut accepted = BTreeMap::::new(); + let mut next_de_id = 0usize; + + for field in &named.named { + let ident = field.ident.clone().expect("named field"); + let attrs = parse_field_attrs(&field.attrs)?; + let canonical = attrs + .deserialize_name + .clone() + .unwrap_or_else(|| ident.to_string()); + let mut names = BTreeSet::new(); + names.insert(canonical.clone()); + names.extend(attrs.aliases.iter().cloned()); + let names: Vec<_> = names.into_iter().collect(); + let de_id = if attrs.skip_deserializing { + None + } else { + let id = next_de_id; + next_de_id += 1; + for name in &names { + if let Some(previous) = accepted.insert(name.clone(), id) { + if previous != id { + return Err(syn::Error::new_spanned( + field, + format!( + "deserialize field name `{name}` is used by multiple fields" + ), + )); + } + } + } + Some(id) + }; + fields.push(FieldInfo { + ident, + ty: field.ty.clone(), + attrs, + canonical, + names, + de_id, + }); + } + + let type_name = container + .name + .clone() + .unwrap_or_else(|| input.ident.to_string()); + let expecting = container + .expecting + .clone() + .unwrap_or_else(|| format!("struct {type_name}")); + + Ok(Self { + ident: input.ident, + container, + fields, + accepted, + type_name, + expecting, + }) + } +} diff --git a/src/__private.rs b/src/__private.rs new file mode 100644 index 00000000..1be0c982 --- /dev/null +++ b/src/__private.rs @@ -0,0 +1,48 @@ +//! Implementation details used by `SonicDeserialize` expansions. +//! +//! This module is public only because proc-macro output is compiled in the +//! downstream crate. It is not a stable user-facing interface. + +use core::marker::PhantomData; + +pub use phf; +use serde::de::{Deserialize, Deserializer, Error, Visitor}; + +/// Preserve Serde's missing-field behavior: a missing `Option` is `None`, +/// while every other missing required type produces `Error::missing_field`. +pub fn missing_field<'de, V, E>(field: &'static str) -> Result +where + V: Deserialize<'de>, + E: Error, +{ + struct MissingFieldDeserializer(&'static str, PhantomData); + + impl<'de, E> Deserializer<'de> for MissingFieldDeserializer + where + E: Error, + { + type Error = E; + + fn deserialize_any(self, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(Error::missing_field(self.0)) + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_none() + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf unit unit_struct newtype_struct seq tuple tuple_struct + map struct enum identifier ignored_any + } + } + + V::deserialize(MissingFieldDeserializer(field, PhantomData)) +} diff --git a/src/lib.rs b/src/lib.rs index 3a3808e5..caf6a589 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,8 @@ #![allow(clippy::needless_lifetimes)] #![doc(test(attr(warn(unused))))] +extern crate self as sonic_rs; + mod config; pub mod error; mod index; @@ -17,12 +19,18 @@ pub mod serde; pub mod value; pub mod writer; +#[cfg(feature = "derive")] +#[doc(hidden)] +pub mod __private; + // re-export FastStr pub use ::faststr::FastStr; // re-export the serde trait pub use ::serde::{Deserialize, Serialize}; #[doc(inline)] pub use reader::Read; +#[cfg(feature = "derive")] +pub use sonic_derive::SonicDeserialize; #[doc(inline)] pub use crate::error::{Error, Result}; diff --git a/tests/deserialize_with_benchmark.rs b/tests/deserialize_with_benchmark.rs new file mode 100644 index 00000000..db4ce322 --- /dev/null +++ b/tests/deserialize_with_benchmark.rs @@ -0,0 +1,286 @@ +#![cfg(feature = "derive")] + +use std::{fmt::Write, hint::black_box, time::Instant}; + +use serde::{Deserialize, Deserializer, Serialize}; +use sonic_rs::SonicDeserialize; + +fn identity_i64<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + i64::deserialize(deserializer) +} + +macro_rules! define_plain { + ($name:ident, $deserialize:path) => { + #[derive(Debug, PartialEq, Serialize, $deserialize)] + struct $name { + f01: i64, + f02: i64, + f03: i64, + f04: i64, + f05: i64, + f06: i64, + f07: i64, + f08: i64, + f09: i64, + f10: i64, + f11: i64, + f12: i64, + f13: i64, + f14: i64, + f15: i64, + f16: i64, + f17: i64, + f18: i64, + f19: i64, + f20: i64, + f21: i64, + f22: i64, + f23: i64, + f24: i64, + f25: i64, + f26: i64, + f27: i64, + f28: i64, + f29: i64, + f30: i64, + f31: i64, + f32: i64, + f33: i64, + f34: i64, + f35: i64, + f36: i64, + f37: i64, + f38: i64, + f39: i64, + f40: i64, + f41: i64, + f42: i64, + f43: i64, + f44: i64, + f45: i64, + f46: i64, + f47: i64, + f48: i64, + f49: i64, + f50: i64, + f51: i64, + f52: i64, + f53: i64, + f54: i64, + f55: i64, + f56: i64, + f57: i64, + f58: i64, + f59: i64, + f60: i64, + f61: i64, + f62: i64, + f63: i64, + f64: i64, + } + }; +} + +macro_rules! define_with { + ($name:ident, $deserialize:path) => { + #[derive(Debug, PartialEq, Serialize, $deserialize)] + struct $name { + #[serde(deserialize_with = "identity_i64")] + f01: i64, + #[serde(deserialize_with = "identity_i64")] + f02: i64, + #[serde(deserialize_with = "identity_i64")] + f03: i64, + #[serde(deserialize_with = "identity_i64")] + f04: i64, + #[serde(deserialize_with = "identity_i64")] + f05: i64, + #[serde(deserialize_with = "identity_i64")] + f06: i64, + #[serde(deserialize_with = "identity_i64")] + f07: i64, + #[serde(deserialize_with = "identity_i64")] + f08: i64, + #[serde(deserialize_with = "identity_i64")] + f09: i64, + #[serde(deserialize_with = "identity_i64")] + f10: i64, + #[serde(deserialize_with = "identity_i64")] + f11: i64, + #[serde(deserialize_with = "identity_i64")] + f12: i64, + #[serde(deserialize_with = "identity_i64")] + f13: i64, + #[serde(deserialize_with = "identity_i64")] + f14: i64, + #[serde(deserialize_with = "identity_i64")] + f15: i64, + #[serde(deserialize_with = "identity_i64")] + f16: i64, + #[serde(deserialize_with = "identity_i64")] + f17: i64, + #[serde(deserialize_with = "identity_i64")] + f18: i64, + #[serde(deserialize_with = "identity_i64")] + f19: i64, + #[serde(deserialize_with = "identity_i64")] + f20: i64, + #[serde(deserialize_with = "identity_i64")] + f21: i64, + #[serde(deserialize_with = "identity_i64")] + f22: i64, + #[serde(deserialize_with = "identity_i64")] + f23: i64, + #[serde(deserialize_with = "identity_i64")] + f24: i64, + #[serde(deserialize_with = "identity_i64")] + f25: i64, + #[serde(deserialize_with = "identity_i64")] + f26: i64, + #[serde(deserialize_with = "identity_i64")] + f27: i64, + #[serde(deserialize_with = "identity_i64")] + f28: i64, + #[serde(deserialize_with = "identity_i64")] + f29: i64, + #[serde(deserialize_with = "identity_i64")] + f30: i64, + #[serde(deserialize_with = "identity_i64")] + f31: i64, + #[serde(deserialize_with = "identity_i64")] + f32: i64, + #[serde(deserialize_with = "identity_i64")] + f33: i64, + #[serde(deserialize_with = "identity_i64")] + f34: i64, + #[serde(deserialize_with = "identity_i64")] + f35: i64, + #[serde(deserialize_with = "identity_i64")] + f36: i64, + #[serde(deserialize_with = "identity_i64")] + f37: i64, + #[serde(deserialize_with = "identity_i64")] + f38: i64, + #[serde(deserialize_with = "identity_i64")] + f39: i64, + #[serde(deserialize_with = "identity_i64")] + f40: i64, + #[serde(deserialize_with = "identity_i64")] + f41: i64, + #[serde(deserialize_with = "identity_i64")] + f42: i64, + #[serde(deserialize_with = "identity_i64")] + f43: i64, + #[serde(deserialize_with = "identity_i64")] + f44: i64, + #[serde(deserialize_with = "identity_i64")] + f45: i64, + #[serde(deserialize_with = "identity_i64")] + f46: i64, + #[serde(deserialize_with = "identity_i64")] + f47: i64, + #[serde(deserialize_with = "identity_i64")] + f48: i64, + #[serde(deserialize_with = "identity_i64")] + f49: i64, + #[serde(deserialize_with = "identity_i64")] + f50: i64, + #[serde(deserialize_with = "identity_i64")] + f51: i64, + #[serde(deserialize_with = "identity_i64")] + f52: i64, + #[serde(deserialize_with = "identity_i64")] + f53: i64, + #[serde(deserialize_with = "identity_i64")] + f54: i64, + #[serde(deserialize_with = "identity_i64")] + f55: i64, + #[serde(deserialize_with = "identity_i64")] + f56: i64, + #[serde(deserialize_with = "identity_i64")] + f57: i64, + #[serde(deserialize_with = "identity_i64")] + f58: i64, + #[serde(deserialize_with = "identity_i64")] + f59: i64, + #[serde(deserialize_with = "identity_i64")] + f60: i64, + #[serde(deserialize_with = "identity_i64")] + f61: i64, + #[serde(deserialize_with = "identity_i64")] + f62: i64, + #[serde(deserialize_with = "identity_i64")] + f63: i64, + #[serde(deserialize_with = "identity_i64")] + f64: i64, + } + }; +} + +define_plain!(PlainSerde, Deserialize); +define_plain!(PlainSonic, SonicDeserialize); +define_with!(WithSerde, Deserialize); +define_with!(WithSonic, SonicDeserialize); + +fn payload() -> String { + let mut json = String::from("{"); + for index in 1..=64 { + if index > 1 { + json.push(','); + } + write!(&mut json, "\"f{index:02}\":{index}").unwrap(); + } + json.push('}'); + json +} + +fn measure(input: &str, iterations: usize) -> f64 +where + T: serde::de::DeserializeOwned, +{ + let start = Instant::now(); + for _ in 0..iterations { + let value: T = sonic_rs::from_str(black_box(input)).unwrap(); + black_box(value); + } + start.elapsed().as_nanos() as f64 / iterations as f64 +} + +#[test] +#[ignore] +fn benchmark_deserialize_with_overhead() { + let input = payload(); + let iterations = std::env::var("SONIC_DERIVE_BENCH_ITERS") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(100_000usize); + + let plain_serde: PlainSerde = sonic_rs::from_str(&input).unwrap(); + let plain_sonic: PlainSonic = sonic_rs::from_str(&input).unwrap(); + let with_serde: WithSerde = sonic_rs::from_str(&input).unwrap(); + let with_sonic: WithSonic = sonic_rs::from_str(&input).unwrap(); + assert_eq!( + serde_json::to_value(&plain_serde).unwrap(), + serde_json::to_value(&plain_sonic).unwrap() + ); + assert_eq!( + serde_json::to_value(&with_serde).unwrap(), + serde_json::to_value(&with_sonic).unwrap() + ); + + for sample in 0..6 { + let plain_serde = measure::(&input, iterations); + let plain_sonic = measure::(&input, iterations); + let with_serde = measure::(&input, iterations); + let with_sonic = measure::(&input, iterations); + + println!( + "sample={sample} plain_serde_ns_op={plain_serde:.2} \ + plain_sonic_ns_op={plain_sonic:.2} with_serde_ns_op={with_serde:.2} \ + with_sonic_ns_op={with_sonic:.2}" + ); + } +} diff --git a/tests/sonic_deserialize.rs b/tests/sonic_deserialize.rs new file mode 100644 index 00000000..8a3e4491 --- /dev/null +++ b/tests/sonic_deserialize.rs @@ -0,0 +1,461 @@ +#![cfg(feature = "derive")] + +use std::{fmt::Write, hint::black_box, time::Instant}; + +use serde::{Deserialize, Deserializer, Serialize}; +use sonic_rs::SonicDeserialize; + +fn default_tag() -> i64 { + 77 +} + +fn number_or_string<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum Repr { + Number(i64), + String(String), + } + + match Repr::deserialize(deserializer)? { + Repr::Number(value) => Ok(value), + Repr::String(value) => value.parse().map_err(serde::de::Error::custom), + } +} + +macro_rules! define_wide { + ($name:ident, $deserialize:path) => { + #[derive(Debug, Default, PartialEq, Serialize, $deserialize)] + #[serde(default)] + struct $name { + #[serde(alias = "uid")] + user_id: i64, + #[serde(default, deserialize_with = "number_or_string")] + coerced: i64, + #[serde(default = "default_tag")] + tag: i64, + #[serde(skip_deserializing)] + skipped: i64, + #[serde(rename(deserialize = "wire_name", serialize = "wire_out"), default)] + renamed: i64, + f01: i64, + f02: i64, + f03: i64, + f04: i64, + f05: i64, + f06: i64, + f07: i64, + f08: i64, + f09: i64, + f10: i64, + f11: i64, + f12: i64, + f13: i64, + f14: i64, + f15: i64, + f16: i64, + f17: i64, + f18: i64, + f19: i64, + f20: i64, + f21: i64, + f22: i64, + f23: i64, + f24: i64, + f25: i64, + f26: i64, + f27: i64, + f28: i64, + f29: i64, + f30: i64, + f31: i64, + f32: i64, + f33: i64, + f34: i64, + f35: i64, + f36: i64, + f37: i64, + f38: i64, + f39: i64, + f40: i64, + f41: i64, + f42: i64, + f43: i64, + f44: i64, + f45: i64, + f46: i64, + f47: i64, + f48: i64, + f49: i64, + f50: i64, + f51: i64, + f52: i64, + f53: i64, + f54: i64, + f55: i64, + f56: i64, + f57: i64, + f58: i64, + f59: i64, + f60: i64, + f61: i64, + f62: i64, + f63: i64, + f64: i64, + f65: i64, + f66: i64, + f67: i64, + f68: i64, + f69: i64, + f70: i64, + f71: i64, + f72: i64, + f73: i64, + f74: i64, + f75: i64, + f76: i64, + f77: i64, + f78: i64, + f79: i64, + f80: i64, + f81: i64, + f82: i64, + f83: i64, + f84: i64, + f85: i64, + f86: i64, + f87: i64, + f88: i64, + f89: i64, + f90: i64, + f91: i64, + f92: i64, + f93: i64, + f94: i64, + f95: i64, + f96: i64, + f97: i64, + f98: i64, + f99: i64, + f100: i64, + f101: i64, + f102: i64, + f103: i64, + f104: i64, + f105: i64, + f106: i64, + f107: i64, + f108: i64, + f109: i64, + f110: i64, + f111: i64, + f112: i64, + f113: i64, + f114: i64, + f115: i64, + f116: i64, + f117: i64, + f118: i64, + f119: i64, + f120: i64, + f121: i64, + f122: i64, + f123: i64, + f124: i64, + f125: i64, + f126: i64, + f127: i64, + } + }; +} + +define_wide!(ControlWide, Deserialize); +define_wide!(FastWide, SonicDeserialize); + +fn wide_payload() -> String { + let mut json = String::from(r#"{"uid":7,"coerced":"19","wire_name":8"#); + for index in 1..=127 { + write!(&mut json, ",\"f{index:02}\":{index}").unwrap(); + } + json.push('}'); + json +} + +fn assert_same(control: &ControlWide, fast: &FastWide) { + let control = serde_json::to_value(control).unwrap(); + let fast = serde_json::to_value(fast).unwrap(); + assert_eq!(fast, control); +} + +#[test] +fn phf_path_matches_serde_for_alias_rename_default_skip_and_custom_decoder() { + let json = r#"{ + "uid": 42, + "coerced": "19", + "wire_name": 8, + "skipped": 999, + "f01": 1, + "f31": 31, + "unknown": {"ignored": true} + }"#; + + let control: ControlWide = sonic_rs::from_str(json).unwrap(); + let fast: FastWide = sonic_rs::from_str(json).unwrap(); + assert_same(&control, &fast); + assert_eq!(fast.user_id, 42); + assert_eq!(fast.coerced, 19); + assert_eq!(fast.tag, 77); + assert_eq!(fast.skipped, 0); + assert_eq!(fast.renamed, 8); + + let via_serde_json: FastWide = serde_json::from_str(json).unwrap(); + assert_eq!(via_serde_json, fast); +} + +#[test] +fn canonical_plus_alias_is_a_duplicate() { + let json = r#"{"user_id":1,"uid":2}"#; + let control = sonic_rs::from_str::(json) + .unwrap_err() + .to_string(); + let fast = sonic_rs::from_str::(json) + .unwrap_err() + .to_string(); + assert!(control.contains("duplicate field"), "{control}"); + assert!(fast.contains("duplicate field"), "{fast}"); +} + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] +struct ControlStrict { + id: i64, +} + +#[derive(Debug, PartialEq, SonicDeserialize)] +#[serde(deny_unknown_fields)] +#[sonic(force_phf)] +struct FastStrict { + id: i64, +} + +#[test] +fn deny_unknown_fields_matches_serde() { + let json = r#"{"id":1,"extra":2}"#; + let control = sonic_rs::from_str::(json) + .unwrap_err() + .to_string(); + let fast = sonic_rs::from_str::(json) + .unwrap_err() + .to_string(); + assert!(control.contains("unknown field"), "{control}"); + assert!(fast.contains("unknown field"), "{fast}"); +} + +#[derive(Debug, PartialEq, Deserialize)] +struct ControlSmall { + #[serde(alias = "identifier")] + id: i64, +} + +#[derive(Debug, PartialEq, SonicDeserialize)] +struct FastSmall { + #[serde(alias = "identifier")] + id: i64, +} + +#[test] +fn small_struct_uses_the_direct_match_fallback() { + let control: ControlSmall = sonic_rs::from_str(r#"{"identifier":3}"#).unwrap(); + let fast: FastSmall = sonic_rs::from_str(r#"{"identifier":3}"#).unwrap(); + assert_eq!(control.id, fast.id); +} + +#[derive(Debug, PartialEq, Deserialize)] +struct ControlRequired { + optional: Option, + required: i64, +} + +#[derive(Debug, PartialEq, SonicDeserialize)] +#[sonic(force_phf)] +struct FastRequired { + optional: Option, + required: i64, +} + +#[test] +fn missing_option_and_required_field_match_serde() { + let control: ControlRequired = sonic_rs::from_str(r#"{"required":9}"#).unwrap(); + let fast: FastRequired = sonic_rs::from_str(r#"{"required":9}"#).unwrap(); + assert_eq!(control.optional, fast.optional); + assert_eq!(control.required, fast.required); + + let control = sonic_rs::from_str::("{}") + .unwrap_err() + .to_string(); + let fast = sonic_rs::from_str::("{}") + .unwrap_err() + .to_string(); + assert!(control.contains("missing field"), "{control}"); + assert!(fast.contains("missing field"), "{fast}"); +} + +#[test] +fn all_sonic_entrypoints_keep_their_existing_interface() { + let json = br#"{"user_id":7,"f31":31}"#; + let from_slice: FastWide = sonic_rs::from_slice(json).unwrap(); + let from_reader: FastWide = sonic_rs::from_reader(&json[..]).unwrap(); + let value: sonic_rs::Value = sonic_rs::from_slice(json).unwrap(); + let from_value: FastWide = sonic_rs::from_value(&value).unwrap(); + assert_eq!(from_slice, from_reader); + assert_eq!(from_slice, from_value); + + let escaped: FastWide = sonic_rs::from_str(r#"{"\u0075id":11}"#).unwrap(); + assert_eq!(escaped.user_id, 11); + + let unchecked: FastWide = unsafe { sonic_rs::from_slice_unchecked(json) }.unwrap(); + assert_eq!(unchecked, from_slice); + + let mut stream = + sonic_rs::Deserializer::from_json(r#"{"user_id":1} {"uid":2}"#).into_stream::(); + assert_eq!(stream.next().unwrap().unwrap().user_id, 1); + assert_eq!(stream.next().unwrap().unwrap().user_id, 2); +} + +#[test] +fn sequence_representation_matches_serde() { + let control: ControlRequired = sonic_rs::from_str("[null,9]").unwrap(); + let fast: FastRequired = sonic_rs::from_str("[null,9]").unwrap(); + assert_eq!(control.optional, fast.optional); + assert_eq!(control.required, fast.required); +} + +#[derive(Debug, Default, PartialEq, SonicDeserialize)] +#[serde(default)] +#[sonic(force_phf)] +struct FastNumericFields { + #[serde(alias = "alias")] + first: i64, + second: i64, +} + +#[test] +fn numeric_field_indexes_include_alias_entries() { + let input = vec![(2_u64, 20_i64)].into_iter(); + let fast = FastNumericFields::deserialize(serde::de::value::MapDeserializer::< + _, + serde::de::value::Error, + >::new(input)) + .unwrap(); + + assert_eq!( + fast, + FastNumericFields { + first: 0, + second: 20 + } + ); +} + +#[derive(Debug, Deserialize)] +struct ControlCustomRequired { + #[serde(deserialize_with = "number_or_string")] + value: i64, +} + +#[derive(Debug, SonicDeserialize)] +#[sonic(force_phf)] +struct FastCustomRequired { + #[serde(deserialize_with = "number_or_string")] + value: i64, +} + +#[test] +fn required_custom_decoder_is_not_called_for_a_missing_field() { + let control = sonic_rs::from_str::("{}") + .unwrap_err() + .to_string(); + let fast = sonic_rs::from_str::("{}") + .unwrap_err() + .to_string(); + assert!(control.contains("missing field"), "{control}"); + assert!(fast.contains("missing field"), "{fast}"); + + let control: ControlCustomRequired = sonic_rs::from_str(r#"{"value":"5"}"#).unwrap(); + let fast: FastCustomRequired = sonic_rs::from_str(r#"{"value":"5"}"#).unwrap(); + assert_eq!(control.value, fast.value); +} + +fn measure(input: &str, iterations: usize) -> f64 +where + T: serde::de::DeserializeOwned, +{ + let start = Instant::now(); + for _ in 0..iterations { + let value: T = sonic_rs::from_str(black_box(input)).unwrap(); + black_box(value); + } + start.elapsed().as_nanos() as f64 / iterations as f64 +} + +fn measure_serde_json(input: &str, iterations: usize) -> f64 +where + T: serde::de::DeserializeOwned, +{ + let start = Instant::now(); + for _ in 0..iterations { + let value: T = serde_json::from_str(black_box(input)).unwrap(); + black_box(value); + } + start.elapsed().as_nanos() as f64 / iterations as f64 +} + +/// Local, dependency-free benchmark. Run in release mode with one pinned CPU: +/// +/// `taskset -c 2 cargo test --release --features derive --test sonic_deserialize benchmark_wide -- +/// --ignored --nocapture` +#[test] +#[ignore] +fn benchmark_wide_struct_field_dispatch() { + let input = wide_payload(); + let iterations = std::env::var("SONIC_DERIVE_BENCH_ITERS") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(100_000usize); + + let control: ControlWide = sonic_rs::from_str(&input).unwrap(); + let fast: FastWide = sonic_rs::from_str(&input).unwrap(); + assert_same(&control, &fast); + + for sample in 0..6 { + let (sonic_baseline, sonic_optimized, serde_json_baseline, serde_json_optimized) = + if sample % 2 == 0 { + ( + measure::(&input, iterations), + measure::(&input, iterations), + measure_serde_json::(&input, iterations), + measure_serde_json::(&input, iterations), + ) + } else { + let serde_json_optimized = measure_serde_json::(&input, iterations); + let serde_json_baseline = measure_serde_json::(&input, iterations); + let sonic_optimized = measure::(&input, iterations); + let sonic_baseline = measure::(&input, iterations); + ( + sonic_baseline, + sonic_optimized, + serde_json_baseline, + serde_json_optimized, + ) + }; + println!( + "sample={sample} sonic_baseline_ns_op={sonic_baseline:.2} \ + sonic_optimized_ns_op={sonic_optimized:.2} \ + serde_json_baseline_ns_op={serde_json_baseline:.2} \ + serde_json_optimized_ns_op={serde_json_optimized:.2}" + ); + } +}