pinocchio_log_macro/
lib.rs

1#![no_std]
2
3extern crate alloc;
4
5use alloc::{format, string::ToString, vec::Vec};
6use proc_macro::TokenStream;
7use quote::quote;
8use regex::Regex;
9use syn::{
10    parse::{Parse, ParseStream},
11    parse_macro_input, parse_str,
12    punctuated::Punctuated,
13    Error, Expr, ItemFn, LitInt, LitStr, Token,
14};
15
16/// The default buffer size for the logger.
17const DEFAULT_BUFFER_SIZE: &str = "200";
18
19/// Represents the input arguments to the `log!` macro.
20struct LogArgs {
21    /// The length of the buffer to use for the logger.
22    ///
23    /// This does not have effect when the literal `str` does
24    /// not have value placeholders.
25    buffer_len: LitInt,
26
27    /// The literal formatting string passed to the macro.
28    ///
29    /// The `str` might have value placeholders. While this is
30    /// not a requirement, the number of placeholders must
31    /// match the number of args.
32    format_string: LitStr,
33
34    /// The arguments passed to the macro.
35    ///
36    /// The arguments represent the values to replace the
37    /// placeholders on the format `str`. Valid values must implement
38    /// the [`Log`] trait.
39    args: Punctuated<Expr, Token![,]>,
40}
41
42impl Parse for LogArgs {
43    fn parse(input: ParseStream) -> syn::Result<Self> {
44        // Optional buffer length.
45        let buffer_len = if input.peek(LitInt) {
46            let literal = input.parse()?;
47            // Parse the comma after the buffer length.
48            input.parse::<Token![,]>()?;
49            literal
50        } else {
51            parse_str::<LitInt>(DEFAULT_BUFFER_SIZE)?
52        };
53
54        let format_string = input.parse()?;
55        // Check if there are any arguments passed to the macro.
56        let args = if input.is_empty() {
57            Punctuated::new()
58        } else {
59            input.parse::<Token![,]>()?;
60            Punctuated::parse_terminated(input)?
61        };
62
63        Ok(LogArgs {
64            buffer_len,
65            format_string,
66            args,
67        })
68    }
69}
70
71/// Companion `log!` macro for `pinocchio-log`.
72///
73/// The macro automates the creation of a `Logger` object to log a message.
74/// It support a limited subset of the [`format!`](https://doc.rust-lang.org/std/fmt/) syntax.
75/// The macro parses the format string at compile time and generates the calls to a `Logger`
76/// object to generate the corresponding formatted message.
77///
78/// # Arguments
79///
80/// - `buffer_len`: The length of the buffer to use for the logger (default to `200`). This is an optional argument.
81/// - `format_string`: The literal string to log. This string can contain placeholders `{}` to be replaced by the arguments.
82/// - `args`: The arguments to replace the placeholders in the format string. The arguments must implement the `Log` trait.
83#[proc_macro]
84pub fn log(input: TokenStream) -> TokenStream {
85    // Parse the input into a `LogArgs`.
86    let LogArgs {
87        buffer_len,
88        format_string,
89        args,
90    } = parse_macro_input!(input as LogArgs);
91    let parsed_string = format_string.value();
92
93    // Regex pattern to match placeholders in the format string.
94    let placeholder_regex = Regex::new(r"\{.*?\}").unwrap();
95
96    let placeholders: Vec<_> = placeholder_regex
97        .find_iter(&parsed_string)
98        .map(|m| m.as_str())
99        .collect();
100
101    // Check if there is an argument for each `{}` placeholder.
102    if placeholders.len() != args.len() {
103        let arg_message = if args.is_empty() {
104            "but no arguments were given".to_string()
105        } else {
106            format!(
107                "but there is {} {}",
108                args.len(),
109                if args.len() == 1 {
110                    "argument"
111                } else {
112                    "arguments"
113                }
114            )
115        };
116
117        return Error::new_spanned(
118            format_string,
119            format!(
120                "{} positional arguments in format string, {}",
121                placeholders.len(),
122                arg_message
123            ),
124        )
125        .to_compile_error()
126        .into();
127    }
128
129    if !placeholders.is_empty() {
130        // The parts of the format string with the placeholders replaced by arguments.
131        let mut replaced_parts = Vec::new();
132
133        let parts: Vec<&str> = placeholder_regex.split(&parsed_string).collect();
134        let part_iter = parts.iter();
135
136        let mut arg_iter = args.iter();
137        let mut ph_iter = placeholders.iter();
138
139        // Replace each occurrence of `{}` with their corresponding argument value.
140        for part in part_iter {
141            if !part.is_empty() {
142                replaced_parts.push(quote! { logger.append(#part) });
143            }
144
145            if let Some(arg) = arg_iter.next() {
146                // The number of placeholders was validated to be the same as
147                // the number of arguments, so this should never panic.
148                let placeholder = ph_iter.next().unwrap();
149
150                match *placeholder {
151                    "{}" => {
152                        replaced_parts.push(quote! { logger.append(#arg) });
153                    }
154                    value if value.starts_with("{:.") => {
155                        let precision =
156                            if let Ok(precision) = value[3..value.len() - 1].parse::<u8>() {
157                                precision
158                            } else {
159                                return Error::new_spanned(
160                                    format_string,
161                                    format!("invalid precision format: {}", value),
162                                )
163                                .to_compile_error()
164                                .into();
165                            };
166
167                        replaced_parts.push(quote! {
168                            logger.append_with_args(
169                                #arg,
170                                &[pinocchio_log::logger::Argument::Precision(#precision)]
171                            )
172                        });
173                    }
174                    value if value.starts_with("{:<.") || value.starts_with("{:>.") => {
175                        let size = if let Ok(size) = value[4..value.len() - 1].parse::<usize>() {
176                            size
177                        } else {
178                            return Error::new_spanned(
179                                format_string,
180                                format!("invalid truncate size format: {}", value),
181                            )
182                            .to_compile_error()
183                            .into();
184                        };
185
186                        match value.chars().nth(2) {
187                            Some('<') => {
188                                replaced_parts.push(quote! {
189                                    logger.append_with_args(
190                                        #arg,
191                                        &[pinocchio_log::logger::Argument::TruncateStart(#size)]
192                                    )
193                                });
194                            }
195                            Some('>') => {
196                                replaced_parts.push(quote! {
197                                    logger.append_with_args(
198                                        #arg,
199                                        &[pinocchio_log::logger::Argument::TruncateEnd(#size)]
200                                    )
201                                });
202                            }
203                            _ => {
204                                // This should not happen since we already checked the format.
205                                return Error::new_spanned(
206                                    format_string,
207                                    format!("invalid truncate format: {}", value),
208                                )
209                                .to_compile_error()
210                                .into();
211                            }
212                        }
213                    }
214                    _ => {
215                        return Error::new_spanned(
216                            format_string,
217                            format!("invalid placeholder: {}", placeholder),
218                        )
219                        .to_compile_error()
220                        .into();
221                    }
222                }
223            }
224        }
225
226        // Generate the output string as a compile-time constant
227        TokenStream::from(quote! {
228            {
229                let mut logger = pinocchio_log::logger::Logger::<#buffer_len>::default();
230                #(#replaced_parts;)*
231                logger.log();
232            }
233        })
234    } else {
235        TokenStream::from(quote! {pinocchio_log::logger::log_message(#format_string.as_bytes());})
236    }
237}
238
239/// Attribute macro for instrumenting functions with compute unit logging.
240///
241/// This macro wraps the decorated function with additional logging statements
242/// that print the function name and the number of compute units used before and after
243/// the function execution.
244///
245/// # Effects
246///
247/// - Adds a log message with the function name at the end of execution with amount of CU consumed.
248///
249/// # Note
250///
251/// This macro consumes an additional compute units per call due to the logging operations.
252///
253///  # Example
254///
255/// ```rust,ignore
256/// #[pinocchio_log::log_cu_usage]
257/// fn my_function() {
258///     // Function body
259/// }
260/// ```
261///
262/// logging output will look like:
263///
264/// "Program log: Function `my_function` consumed 36 compute units"
265///
266/// # References
267///
268/// * [Logging syscall](https://github.com/anza-xyz/agave/blob/d88050cda335f87e872eddbdf8506bc063f039d3/programs/bpf_loader/src/syscalls/logging.rs#L70)
269/// * [Compute budget](https://github.com/anza-xyz/agave/blob/d88050cda335f87e872eddbdf8506bc063f039d3/program-runtime/src/compute_budget.rs#L150)
270///
271#[proc_macro_attribute]
272pub fn log_cu_usage(_attr: TokenStream, item: TokenStream) -> TokenStream {
273    let mut input = parse_macro_input!(item as ItemFn);
274    let fn_name = &input.sig.ident;
275    let block = &input.block;
276
277    input.block = syn::parse_quote!({
278        let cu_before = unsafe { ::pinocchio_log::logger::remaining_compute_units() };
279
280        let __result = (|| #block)();
281
282        let cu_after = unsafe { ::pinocchio_log::logger::remaining_compute_units() };
283        let introspection_cost = 102; // 100 - compute budget syscall_base_cost,  2 - extra calculations
284
285        let consumed = cu_before - cu_after - introspection_cost;
286
287        ::pinocchio_log::log!("Function {} consumed {} compute units", stringify!(#fn_name), consumed);
288
289        __result
290    });
291
292    quote!(#input).into()
293}