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}