Skip to main content

solana_program_log_macro/
lib.rs

1#![no_std]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![allow(clippy::arithmetic_side_effects)]
4
5extern crate alloc;
6
7use {
8    alloc::{format, string::ToString, vec::Vec},
9    proc_macro::TokenStream,
10    quote::quote,
11    regex::Regex,
12    syn::{
13        parse::{Parse, ParseStream},
14        parse_macro_input, parse_str,
15        punctuated::Punctuated,
16        Error, Expr, ItemFn, LitInt, LitStr, Path, Token,
17    },
18};
19
20/// The default buffer size for the logger.
21const DEFAULT_BUFFER_SIZE: &str = "200";
22
23/// The default name of the `solana-program-log` package to search for when
24/// discovering the crate path.
25const PROGRAM_LOG_PACKAGE_NAME: &str = "::solana_program_log";
26
27/// Represents the input arguments to the `log!` macro.
28struct LogArgs {
29    /// The path to the crate where the `Logger` struct is defined.
30    crate_path: Path,
31
32    /// The length of the buffer to use for the logger.
33    ///
34    /// This does not have effect when the literal `str` does
35    /// not have value placeholders.
36    buffer_len: LitInt,
37
38    /// The literal formatting string passed to the macro.
39    ///
40    /// The `str` might have value placeholders. While this is
41    /// not a requirement, the number of placeholders must
42    /// match the number of args.
43    format_string: LitStr,
44
45    /// The arguments passed to the macro.
46    ///
47    /// The arguments represent the values to replace the
48    /// placeholders on the format `str`. Valid values must implement
49    /// the [`Log`] trait.
50    args: Punctuated<Expr, Token![,]>,
51}
52
53impl Parse for LogArgs {
54    fn parse(input: ParseStream) -> syn::Result<Self> {
55        // Optional crate path.
56        let crate_path = if input.peek(LitStr) || input.peek(LitInt) {
57            parse_str::<Path>(PROGRAM_LOG_PACKAGE_NAME)?
58        } else {
59            let crate_path = input.parse::<Path>()?;
60            // Parse the comma after the crate path.
61            input.parse::<Token![,]>()?;
62            crate_path
63        };
64
65        // Optional buffer length.
66        let buffer_len = if input.peek(LitInt) {
67            let literal = input.parse()?;
68            // Parse the comma after the buffer length.
69            input.parse::<Token![,]>()?;
70            literal
71        } else {
72            parse_str::<LitInt>(DEFAULT_BUFFER_SIZE)?
73        };
74
75        let format_string = input.parse()?;
76        // Check if there are any arguments passed to the macro.
77        let args = if input.is_empty() {
78            Punctuated::new()
79        } else {
80            input.parse::<Token![,]>()?;
81            Punctuated::parse_terminated(input)?
82        };
83
84        Ok(LogArgs {
85            crate_path,
86            buffer_len,
87            format_string,
88            args,
89        })
90    }
91}
92
93/// Represents the input arguments to the `log_cu_usage` attribute macro.
94struct LogCuUsageArgs {
95    /// Explicitly specify the crate path for the `Logger` struct.
96    crate_path: Path,
97}
98
99impl Parse for LogCuUsageArgs {
100    fn parse(input: ParseStream) -> syn::Result<Self> {
101        if input.is_empty() {
102            return Ok(Self {
103                crate_path: parse_str::<Path>(PROGRAM_LOG_PACKAGE_NAME)?,
104            });
105        }
106
107        // Support for `crate = <PATH>`.
108        if input.peek(Token![crate]) {
109            input.parse::<Token![crate]>()?;
110            input.parse::<Token![=]>()?;
111        }
112
113        // Support for standalone path.
114        let crate_path = input.parse::<Path>()?;
115
116        if !input.is_empty() {
117            return Err(input.error("unexpected tokens after crate path"));
118        }
119
120        Ok(Self { crate_path })
121    }
122}
123
124/// Companion `log!` macro.
125///
126/// The macro automates the creation of a `Logger` object to log a message.
127/// It support a limited subset of the [`format!`](https://doc.rust-lang.org/std/fmt/) syntax.
128/// The macro parses the format string at compile time and generates the calls to a `Logger`
129/// object to generate the corresponding formatted message.
130///
131/// # Arguments
132///
133/// - `crate_path`: The path to the crate where the `Logger` struct is defined. This is an optional argument.
134/// - `buffer_len`: The length of the buffer to use for the logger (default to `200`). This is an optional argument.
135/// - `format_string`: The literal string to log. This string can contain placeholders `{}` to be replaced by the arguments.
136/// - `args`: The arguments to replace the placeholders in the format string. The arguments must implement the `Log` trait.
137#[proc_macro]
138pub fn log(input: TokenStream) -> TokenStream {
139    // Parse the input into a `LogArgs`.
140    let LogArgs {
141        crate_path,
142        buffer_len,
143        format_string,
144        args,
145    } = parse_macro_input!(input as LogArgs);
146    let parsed_string = format_string.value();
147
148    // Regex pattern to match placeholders in the format string.
149    let placeholder_regex = Regex::new(r"\{.*?\}").unwrap();
150
151    let placeholders: Vec<_> = placeholder_regex
152        .find_iter(&parsed_string)
153        .map(|m| m.as_str())
154        .collect();
155
156    // Check if there is an argument for each `{}` placeholder.
157    if placeholders.len() != args.len() {
158        let arg_message = if args.is_empty() {
159            "but no arguments were given".to_string()
160        } else {
161            format!(
162                "but there is {} {}",
163                args.len(),
164                if args.len() == 1 {
165                    "argument"
166                } else {
167                    "arguments"
168                }
169            )
170        };
171
172        return Error::new_spanned(
173            format_string,
174            format!(
175                "{} positional arguments in format string, {}",
176                placeholders.len(),
177                arg_message
178            ),
179        )
180        .to_compile_error()
181        .into();
182    }
183
184    if !placeholders.is_empty() {
185        // The parts of the format string with the placeholders replaced by arguments.
186        let mut replaced_parts = Vec::new();
187
188        let parts: Vec<&str> = placeholder_regex.split(&parsed_string).collect();
189        let part_iter = parts.iter();
190
191        let mut arg_iter = args.iter();
192        let mut ph_iter = placeholders.iter();
193
194        // Replace each occurrence of `{}` with their corresponding argument value.
195        for part in part_iter {
196            if !part.is_empty() {
197                replaced_parts.push(quote! { logger.append(#part) });
198            }
199
200            if let Some(arg) = arg_iter.next() {
201                // The number of placeholders was validated to be the same as
202                // the number of arguments, so this should never panic.
203                let placeholder = ph_iter.next().unwrap();
204
205                match *placeholder {
206                    "{}" => {
207                        replaced_parts.push(quote! { logger.append(#arg) });
208                    }
209                    value if value.starts_with("{:.") => {
210                        let Ok(precision) = value[3..value.len() - 1].parse::<u8>() else {
211                            return Error::new_spanned(
212                                format_string,
213                                format!("invalid precision format: {value}"),
214                            )
215                            .to_compile_error()
216                            .into();
217                        };
218
219                        replaced_parts.push(quote! {
220                            logger.append_with_args(
221                                #arg,
222                                &[#crate_path::logger::Argument::Precision(#precision)]
223                            )
224                        });
225                    }
226                    value if value.starts_with("{:<.") || value.starts_with("{:>.") => {
227                        let Ok(size) = value[4..value.len() - 1].parse::<usize>() else {
228                            return Error::new_spanned(
229                                format_string,
230                                format!("invalid truncate size format: {value}"),
231                            )
232                            .to_compile_error()
233                            .into();
234                        };
235
236                        match value.chars().nth(2) {
237                            Some('<') => {
238                                replaced_parts.push(quote! {
239                                    logger.append_with_args(
240                                        #arg,
241                                        &[#crate_path::logger::Argument::TruncateStart(#size)]
242                                    )
243                                });
244                            }
245                            Some('>') => {
246                                replaced_parts.push(quote! {
247                                    logger.append_with_args(
248                                        #arg,
249                                        &[#crate_path::logger::Argument::TruncateEnd(#size)]
250                                    )
251                                });
252                            }
253                            _ => {
254                                // This should not happen since we already checked the format.
255                                return Error::new_spanned(
256                                    format_string,
257                                    format!("invalid truncate format: {value}"),
258                                )
259                                .to_compile_error()
260                                .into();
261                            }
262                        }
263                    }
264                    _ => {
265                        return Error::new_spanned(
266                            format_string,
267                            format!("invalid placeholder: {placeholder}"),
268                        )
269                        .to_compile_error()
270                        .into();
271                    }
272                }
273            }
274        }
275
276        // Generate the output string as a compile-time constant
277        TokenStream::from(quote! {
278            {
279                let mut logger = #crate_path::logger::Logger::<#buffer_len>::default();
280                #(#replaced_parts;)*
281                logger.log();
282            }
283        })
284    } else {
285        TokenStream::from(quote! {
286            {
287                #crate_path::logger::log_message(#format_string.as_bytes());
288            }
289        })
290    }
291}
292
293/// Attribute macro for instrumenting functions with compute unit logging.
294///
295/// This macro wraps the decorated function with additional logging statements
296/// that print the function name and the number of compute units used before and after
297/// the function execution.
298///
299/// # Effects
300///
301/// - Adds a log message with the function name at the end of execution with amount of CU consumed.
302///
303/// # Note
304///
305/// This macro consumes an additional compute units per call due to the logging operations.
306///
307///  # Example
308///
309/// ```rust,ignore
310/// #[solana_program_log::log_cu_usage]
311/// fn my_function() {
312///     // Function body
313/// }
314/// ```
315///
316/// logging output will look like:
317///
318/// "Program log: Function `my_function` consumed 36 compute units"
319///
320/// # References
321///
322/// * [Logging syscall](https://github.com/anza-xyz/agave/blob/d88050cda335f87e872eddbdf8506bc063f039d3/programs/bpf_loader/src/syscalls/logging.rs#L70)
323/// * [Compute budget](https://github.com/anza-xyz/agave/blob/d88050cda335f87e872eddbdf8506bc063f039d3/program-runtime/src/compute_budget.rs#L150)
324///
325#[proc_macro_attribute]
326pub fn log_cu_usage(attr: TokenStream, item: TokenStream) -> TokenStream {
327    let crate_path = parse_macro_input!(attr as LogCuUsageArgs).crate_path;
328    let mut input = parse_macro_input!(item as ItemFn);
329    let fn_name = &input.sig.ident;
330    let block = &input.block;
331
332    input.block = syn::parse_quote!({
333        let cu_before = unsafe { #crate_path::logger::remaining_compute_units() };
334
335        let __result = (|| #block)();
336
337        let cu_after = unsafe { #crate_path::logger::remaining_compute_units() };
338        // 100 (compute budget syscall_base_cost) + 2 (extra calculations)
339        let introspection_cost = 102;
340
341        let consumed = cu_before - cu_after - introspection_cost;
342
343        #crate_path::log!("Function {} consumed {} compute units", stringify!(#fn_name), consumed);
344
345        __result
346    });
347
348    quote!(#input).into()
349}
350
351#[cfg(test)]
352mod tests {
353    use {
354        super::{LogArgs, LogCuUsageArgs},
355        syn::{parse_quote, parse_str, Path},
356    };
357
358    #[test]
359    fn log_default_crate_path() {
360        let args = parse_str::<LogArgs>("\"a simple log\"").unwrap();
361        let expected: Path = parse_quote!(::solana_program_log);
362        assert_eq!(args.crate_path, expected);
363    }
364
365    #[test]
366    fn log_default_crate_path_with_buffer_len() {
367        let args = parse_str::<LogArgs>("500, \"a simple log\"").unwrap();
368        let expected: Path = parse_quote!(::solana_program_log);
369        assert_eq!(args.crate_path, expected);
370        assert_eq!(args.buffer_len.base10_digits(), "500");
371    }
372
373    #[test]
374    fn log_with_crate_path() {
375        let args = parse_str::<LogArgs>("mylog, \"a simple log\"").unwrap();
376        let expected: Path = parse_quote!(mylog);
377        assert_eq!(args.crate_path, expected);
378    }
379
380    #[test]
381    fn log_with_crate_path_and_buffer_len() {
382        let args = parse_str::<LogArgs>("mylog, 500, \"a simple log\"").unwrap();
383        let expected: Path = parse_quote!(mylog);
384        assert_eq!(args.crate_path, expected);
385        assert_eq!(args.buffer_len.base10_digits(), "500");
386    }
387
388    #[test]
389    fn log_cu_usage() {
390        let args = parse_str::<LogCuUsageArgs>("").unwrap();
391        let expected: Path = parse_quote!(::solana_program_log);
392        assert_eq!(args.crate_path, expected);
393    }
394
395    #[test]
396    fn log_cu_usage_args_support_standalone_path() {
397        let args = parse_str::<LogCuUsageArgs>("mylog").unwrap();
398        let expected: Path = parse_quote!(mylog);
399        assert_eq!(args.crate_path, expected);
400    }
401
402    #[test]
403    fn log_cu_usage_args_support_crate_equals_path() {
404        let args = parse_str::<LogCuUsageArgs>("crate = another_log").unwrap();
405        let expected: Path = parse_quote!(another_log);
406        assert_eq!(args.crate_path, expected);
407    }
408}