Skip to content

Commit

Permalink
quicklog: derive Serialize
Browse files Browse the repository at this point in the history
  • Loading branch information
thog92 committed Mar 8, 2024
1 parent d5781e5 commit 914cb6d
Show file tree
Hide file tree
Showing 11 changed files with 346 additions and 4 deletions.
176 changes: 176 additions & 0 deletions quicklog-macros/src/derive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use proc_macro::TokenStream;
use proc_macro2::{Ident, TokenStream as TokenStream2};
use quote::quote;
use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Type};

/// Generates a `quicklog` `Serialize` implementation for a user-defined struct.
///
/// There is no new real logic in the generated `encode` and `decode` functions
/// for the struct. The macro simply walks every field of the struct and
/// sequentially calls `encode` or `decode` corresponding to the `Serialize`
/// implementation for the type of the field.
///
/// For instance:
/// ```ignore
/// use quicklog::Serialize;
///
/// #[derive(Serialize)]
/// struct TestStruct {
/// a: usize,
/// b: i32,
/// c: u32,
/// }
///
/// // Generated code
/// impl quicklog::serialize::Serialize for TestStruct {
/// fn encode<'buf>(
/// &self,
/// write_buf: &'buf mut [u8],
/// ) -> quicklog::serialize::Store<'buf> {
/// let (chunk, rest) = write_buf.split_at_mut(self.buffer_size_required());
/// let (_, chunk_rest) = self.a.encode(chunk);
/// let (_, chunk_rest) = self.b.encode(chunk_rest);
/// let (_, chunk_rest) = self.c.encode(chunk_rest);
/// assert!(chunk_rest.is_empty());
/// (quicklog::serialize::Store::new(Self::decode, chunk), rest)
/// }
/// fn decode(read_buf: &[u8]) -> (String, &[u8]) {
/// let (a, read_buf) = <usize as quicklog::serialize::Serialize>::decode(read_buf);
/// let (b, read_buf) = <i32 as quicklog::serialize::Serialize>::decode(read_buf);
/// let (c, read_buf) = <u32 as quicklog::serialize::Serialize>::decode(read_buf);
/// (
/// {
/// let res = ::alloc::fmt::format(format_args!("{0} {1} {2}", a, b, c));
/// res
/// },
/// read_buf,
/// )
/// }
/// fn buffer_size_required(&self) -> usize {
/// self.a.buffer_size_required() + self.b.buffer_size_required()
/// + self.c.buffer_size_required()
/// }
/// }
/// ```
pub(crate) fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

let Data::Struct(DataStruct { fields, .. }) = input.data else {
todo!("Deriving Serialize only supported for structs currently")
};

if fields.is_empty() {
return quote! {}.into();
}

let field_names: Vec<_> = fields
.iter()
.filter_map(|field| field.ident.as_ref())
.collect();

// If we have > 1 field, then we split once at the top-level to get the
// single chunk that has enough capacity to encode all the fields.
// From there, each field will just encode into this single chunk.
//
// Otherwise, if we only have 1 field, we can simply let the single field
// directly read off the main `write_buf` chunk and return the remainder
// unread.
let (initial_chunk_split, chunk_encode_and_store): (TokenStream2, TokenStream2) =
if field_names.len() > 1 {
// Split off just large enough chunk to be kept in final Store
let initial_split = quote! {
let (chunk, rest) = write_buf.split_at_mut(self.buffer_size_required());
};

// Sequentially encode
let encode: Vec<_> = field_names
.iter()
.enumerate()
.map(|(idx, name)| {
if idx == 0 {
quote! {
let (_, chunk_rest) = self.#name.encode(chunk);
}
} else {
quote! {
let (_, chunk_rest) = self.#name.encode(chunk_rest);
}
}
})
.collect();

let encode_and_store = quote! {
#(#encode)*

assert!(chunk_rest.is_empty());
(quicklog::serialize::Store::new(Self::decode, chunk), rest)
};

(initial_split, encode_and_store)
} else {
let initial_split = quote! {
let chunk = write_buf;
};

// Only one field, so can directly encode in main chunk
let field_name = &field_names[0];
let encode_and_store = quote! {
self.#field_name.encode(chunk)
};

(initial_split, encode_and_store)
};

// Combine decode implementations from all field types
let field_tys: Vec<_> = fields
.iter()
.map(|field| {
// Unwrap: safe since we checked that this macro is only for structs
// which always have named fields
let field_name = field.ident.as_ref().unwrap();
let mut field_ty = field.ty.clone();
if let Type::Reference(ty_ref) = &mut field_ty {
_ = ty_ref.lifetime.take();
_ = ty_ref.mutability.take();
}
let decoded_ident = Ident::new(format!("{}", field_name).as_str(), field_name.span());

quote! {
let (#decoded_ident, read_buf) = <#field_ty as quicklog::serialize::Serialize>::decode(read_buf);
}
})
.collect();

// Assuming that each field in the output should just be separated by a space
// TODO: proper field naming?
let mut decode_fmt_str = String::new();
for _ in 0..fields.len() {
decode_fmt_str.push_str("{} ");
}
let decode_fmt_str = decode_fmt_str.trim_end();

quote! {
impl #impl_generics quicklog::serialize::Serialize for #struct_name #ty_generics #where_clause {
fn encode<'buf>(&self, write_buf: &'buf mut [u8]) -> (quicklog::serialize::Store<'buf>, &'buf mut [u8]) {
// Perform initial split to get combined byte buffer that will be
// sufficient for all fields to be encoded in
#initial_chunk_split

#chunk_encode_and_store
}

fn decode(read_buf: &[u8]) -> (String, &[u8]) {
#(#field_tys)*

(format!(#decode_fmt_str, #(#field_names),*), read_buf)
}

fn buffer_size_required(&self) -> usize {
#(self.#field_names.buffer_size_required())+*
}
}
}
.into()
}
9 changes: 9 additions & 0 deletions quicklog-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use proc_macro::TokenStream;

mod args;
mod derive;
mod expand;
mod format_arg;
mod quicklog;

use derive::derive;
use expand::expand;
use quicklog::Level;

Expand Down Expand Up @@ -32,3 +34,10 @@ pub fn warn(input: TokenStream) -> TokenStream {
pub fn error(input: TokenStream) -> TokenStream {
expand(Level::Error, input)
}

/// Derive macro for generating `quicklog` `Serialize`
/// implementations.
#[proc_macro_derive(Serialize)]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
derive(input)
}
6 changes: 5 additions & 1 deletion quicklog/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ name = "quicklog"
path = "src/lib.rs"

[[test]]
name = "tests"
name = "ui"
path = "tests/ui.rs"

[[test]]
name = "derive"
path = "tests/derive/derive.rs"

[dependencies]
lazy_format = "2.0.0"
quicklog-clock = { path = "../quicklog-clock", version = "0.1.3" }
Expand Down
2 changes: 1 addition & 1 deletion quicklog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ include!("constants.rs");
/// `constants.rs` is generated from `build.rs`, should not be modified manually
pub mod constants;

pub use quicklog_macros::{debug, error, info, trace, warn};
pub use quicklog_macros::{debug, error, info, trace, warn, Serialize};

/// Internal API
///
Expand Down
54 changes: 52 additions & 2 deletions quicklog/src/serialize/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,61 @@ use std::{fmt::Display, str::from_utf8};
pub mod buffer;

/// Allows specification of a custom way to serialize the Struct.
/// Additionally, this stores the contents serialized into a buffer, which does
/// not require allocation and could speed things up.
///
/// This is the key trait to implement to improve logging performance. While
/// `Debug` and `Display` usages are eagerly formatted on the hot path,
/// `Serialize` usages copy the minimal required bytes to a separate buffer,
/// and then allow for formatting when flushing elsewhere. Consider ensuring
/// that all logging arguments implement `Serialize` for best performance.
///
/// Furthermore, you would usually not be required to implement `Serialize` by
/// hand for most types. The option that would work for most use cases would be
/// [deriving `Serialize`](crate::Serialize), similar to how `Debug` is
/// derived on user-defined types. Although, do note that all fields on the user
/// struct must also derive/implement `Serialize` (similar to `Debug` again).
///
/// For instance, this would work since all fields have a `Serialize`
/// implementation:
/// ```
/// use quicklog::Serialize;
///
/// #[derive(Serialize)]
/// struct SerializeStruct {
/// a: usize,
/// b: i32,
/// c: &'static str,
/// }
/// ```
///
/// But a field with a type that does not implement `Serialize` will fail to compile:
/// ```compile_fail
/// use quicklog::Serialize;
///
/// struct NoSerializeStruct {
/// a: &'static str,
/// b: &'static str,
/// }
///
/// #[derive(Serialize)]
/// struct SerializeStruct {
/// a: usize,
/// b: i32,
/// // doesn't implement `Serialize`!
/// c: NoSerializeStruct,
/// }
/// ```
pub trait Serialize {
/// Describes how to encode the implementing type into a byte buffer.
///
/// Returns a [Store](crate::serialize::Store) and the remainder of `write_buf`
/// passed in that was not written to.
fn encode<'buf>(&self, write_buf: &'buf mut [u8]) -> (Store<'buf>, &'buf mut [u8]);
/// Describes how to decode the implementing type from a byte buffer.
///
/// Returns a formatted String after parsing the byte buffer, as well as
/// the remainder of `read_buf` pass in that was not read.
fn decode(read_buf: &[u8]) -> (String, &[u8]);
/// The number of bytes required to `encode` the type into a byte buffer.
fn buffer_size_required(&self) -> usize;
}

Expand Down
9 changes: 9 additions & 0 deletions quicklog/tests/derive/derive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#[test]
fn derive() {
let t = trybuild::TestCases::new();
t.pass("tests/derive/derive_00.rs");
t.pass("tests/derive/derive_01.rs");
t.pass("tests/derive/derive_02.rs");
t.pass("tests/derive/derive_03.rs");
t.pass("tests/derive/derive_04.rs");
}
7 changes: 7 additions & 0 deletions quicklog/tests/derive/derive_00.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Testing structs with no fields (should be a no-op).
use quicklog::Serialize;

#[derive(Serialize)]
struct TestStruct;

fn main() {}
16 changes: 16 additions & 0 deletions quicklog/tests/derive/derive_01.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Testing structs with a simple primitive field.
use quicklog::serialize::Serialize as _;
use quicklog::Serialize;

#[derive(Serialize)]
struct TestStruct {
size: usize,
}

fn main() {
let s = TestStruct { size: 0 };
let mut buf = [0; 128];

let (store, _) = s.encode(&mut buf);
assert_eq!(format!("{}", s.size), format!("{}", store))
}
22 changes: 22 additions & 0 deletions quicklog/tests/derive/derive_02.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Testing structs with multiple primitive fields.
use quicklog::serialize::Serialize as _;
use quicklog::Serialize;

#[derive(Serialize)]
struct TestStruct {
a: usize,
b: i32,
c: u32,
}

fn main() {
let s = TestStruct {
a: 0,
b: -999,
c: 2,
};
let mut buf = [0; 128];

let (store, _) = s.encode(&mut buf);
assert_eq!(format!("{} {} {}", s.a, s.b, s.c), format!("{}", store))
}
24 changes: 24 additions & 0 deletions quicklog/tests/derive/derive_03.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Testing structs with &str types of different lifetimes.
use quicklog::serialize::Serialize as _;
use quicklog::Serialize;

#[derive(Serialize)]
struct TestStruct<'a> {
some_str: &'static str,
another_str: &'a str,
}

fn main() {
let another_string = "Hello".to_string() + "there";
let s = TestStruct {
some_str: "hello world",
another_str: another_string.as_str(),
};
let mut buf = [0; 128];

let (store, _) = s.encode(&mut buf);
assert_eq!(
format!("{} {}", s.some_str, s.another_str),
format!("{}", store)
)
}
25 changes: 25 additions & 0 deletions quicklog/tests/derive/derive_04.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Testing structs with combination of primitives and &str.
use quicklog::serialize::Serialize as _;
use quicklog::Serialize;

#[derive(Serialize)]
struct TestStruct {
a: usize,
some_str: &'static str,
b: i32,
}

fn main() {
let s = TestStruct {
a: 999,
some_str: "hello world",
b: -32,
};
let mut buf = [0; 128];

let (store, _) = s.encode(&mut buf);
assert_eq!(
format!("{} {} {}", s.a, s.some_str, s.b),
format!("{}", store)
)
}

0 comments on commit 914cb6d

Please sign in to comment.