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