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}