Skip to main content

pbench_macros/
lib.rs

1//! Proc macros for pbench benchmark registration.
2//!
3//! Provides `#[bench]` and `#[bench_group]` attribute macros that generate
4//! static registration code using linker sections for pre-main initialization.
5
6mod attr_options;
7mod tokens;
8
9use attr_options::{AttrOptions, Macro};
10use proc_macro::TokenStream;
11use proc_macro2::TokenStream as QuoteStream;
12use quote::{format_ident, quote};
13use syn::{ItemFn, ItemMod, parse_macro_input};
14
15/// Platform-specific linker section attributes for pre-main registration.
16///
17/// Generates `#[used]` + `#[cfg_attr(..., link_section = "...")]` for each
18/// supported platform family (ELF, Mach-O, Windows).
19fn pre_main_attrs() -> QuoteStream {
20    let elf_targets: QuoteStream = quote! {
21        target_os = "linux",
22        target_os = "android",
23        target_os = "dragonfly",
24        target_os = "freebsd",
25        target_os = "fuchsia",
26        target_os = "haiku",
27        target_os = "illumos",
28        target_os = "netbsd",
29        target_os = "openbsd"
30    };
31
32    let mach_o_targets: QuoteStream = quote! {
33        target_os = "macos",
34        target_os = "ios",
35        target_os = "tvos",
36        target_os = "watchos"
37    };
38
39    quote! {
40        #[used]
41        #[cfg_attr(
42            any(#elf_targets),
43            unsafe(link_section = ".init_array")
44        )]
45        #[cfg_attr(
46            any(#mach_o_targets),
47            unsafe(link_section = "__DATA,__mod_init_func")
48        )]
49        #[cfg_attr(target_os = "windows", unsafe(link_section = ".CRT$XCU"))]
50    }
51}
52
53/// Compile error for unsupported platforms.
54fn unsupported_platform_check() -> QuoteStream {
55    quote! {
56        #[cfg(not(any(
57            target_os = "linux",
58            target_os = "android",
59            target_os = "dragonfly",
60            target_os = "freebsd",
61            target_os = "fuchsia",
62            target_os = "haiku",
63            target_os = "illumos",
64            target_os = "netbsd",
65            target_os = "openbsd",
66            target_os = "macos",
67            target_os = "ios",
68            target_os = "tvos",
69            target_os = "watchos",
70            target_os = "windows",
71        )))]
72        compile_error!("pbench: unsupported target OS for benchmark registration");
73    }
74}
75
76/// Generate the `EntryMeta { ... }` expression for a given identifier.
77///
78/// Uses unprefixed compiler built-in macros (`stringify!`, `module_path!`,
79/// `file!`, `line!`, `column!`) which are always available without imports.
80fn entry_meta_expr(private_mod: &QuoteStream, name_ident: &syn::Ident) -> QuoteStream {
81    quote! {
82        #private_mod::EntryMeta {
83            raw_name: stringify!(#name_ident),
84            module_path: module_path!(),
85            location: #private_mod::EntryLocation {
86                file: file!(),
87                line: line!(),
88                col: column!(),
89            },
90        }
91    }
92}
93
94/// Mark a function as a benchmark.
95///
96/// # Supported signatures
97///
98/// ```ignore
99/// #[pbench::bench]
100/// fn no_args() { /* ... */ }
101///
102/// #[pbench::bench]
103/// fn with_bencher(b: &Bencher) { /* ... */ }
104///
105/// #[pbench::bench(args = [1, 2, 4, 8])]
106/// fn with_args(b: &Bencher, arg: &str) { /* ... */ }
107/// ```
108///
109/// # Options
110///
111/// - `sample_count = <expr>` — number of samples
112/// - `sample_size = <expr>` — iterations per sample
113/// - `min_time = <seconds>` — minimum benchmark duration (f64 seconds)
114/// - `max_time = <seconds>` — maximum benchmark duration (f64 seconds)
115/// - `skip_ext_time` — skip external time in min/max accounting
116/// - `args = [<exprs>]` — runtime arguments (requires 2-param fn signature)
117/// - `ignore` — skip this benchmark unless `--include-ignored` is passed
118#[proc_macro_attribute]
119pub fn bench(attr: TokenStream, item: TokenStream) -> TokenStream {
120    let input_fn: ItemFn = parse_macro_input!(item as ItemFn);
121    let fn_name: &syn::Ident = &input_fn.sig.ident;
122    let param_count: usize = input_fn.sig.inputs.len();
123
124    // Check for #[ignore] attribute on the function.
125    let ignore_attr: Option<syn::Path> = input_fn
126        .attrs
127        .iter()
128        .find(|a: &&syn::Attribute| a.path().is_ident("ignore"))
129        .map(|a: &syn::Attribute| a.path().clone());
130
131    let options: AttrOptions = match AttrOptions::parse(attr, Macro::Bench { param_count }) {
132        Ok(opts) => opts,
133        Err(err) => return err,
134    };
135
136    let private_mod: &QuoteStream = &options.private_mod;
137    let pre_main: QuoteStream = pre_main_attrs();
138    let platform_check: QuoteStream = unsupported_platform_check();
139    let meta_expr: QuoteStream = entry_meta_expr(private_mod, fn_name);
140
141    // Generate unique static identifier from function name.
142    let static_name: syn::Ident =
143        format_ident!("__PBENCH_REGISTER_{}", fn_name.to_string().to_uppercase());
144
145    let registration: QuoteStream = if let Some(ref args_array) = options.args_array {
146        // args = [...] variant → GenericBenchEntry.
147        //
148        // Each array element is stringified at compile time via
149        // `stringify!()`. The user function signature must be:
150        //   fn name(b: &Bencher, arg: &str)
151        let args_stringified: Vec<QuoteStream> = args_array
152            .elems
153            .iter()
154            .map(|elem: &syn::Expr| quote! { stringify!(#elem) })
155            .collect();
156
157        quote! {
158            {
159                static ENTRY: #private_mod::GenericBenchEntry =
160                    #private_mod::GenericBenchEntry {
161                        meta: #meta_expr,
162                        bench_fn: #fn_name,
163                        args: &[#(#args_stringified),*],
164                    };
165
166                static ANY_ENTRY: #private_mod::AnyBenchEntry =
167                    #private_mod::AnyBenchEntry::Generic(&ENTRY);
168
169                static NODE: #private_mod::EntryList<
170                    #private_mod::AnyBenchEntry,
171                > = #private_mod::EntryList::new(&ANY_ENTRY);
172
173                extern "C" fn push() {
174                    #private_mod::BENCH_ENTRIES.push(&NODE);
175                }
176
177                #platform_check
178
179                #pre_main
180                static __PBENCH_PUSH: extern "C" fn() = push;
181            }
182        }
183    } else {
184        // Plain benchmark — no args.
185        let bench_options_expr: QuoteStream = options.bench_options_fn(ignore_attr.as_ref());
186
187        let bench_fn_expr: QuoteStream = match param_count {
188            // fn my_bench() { ... }
189            // → wrap in a named function that calls bench_refs
190            0 => quote! {
191                {
192                    fn __pbench_wrap(
193                        __b: &'_ #private_mod::Bencher<'_>,
194                    ) {
195                        __b.bench_refs(#fn_name);
196                    }
197
198                    __pbench_wrap
199                }
200            },
201
202            // fn my_bench(b: &Bencher) { ... }
203            // → use directly as BenchFn
204            1 => quote! { #fn_name },
205
206            _ => {
207                return syn::Error::new_spanned(
208                    &input_fn.sig,
209                    "benchmark function must take 0 or 1 parameters \
210                         (or use `args = [...]` for 2)",
211                )
212                .into_compile_error()
213                .into();
214            }
215        };
216
217        quote! {
218            {
219                static BENCH_ENTRY: #private_mod::BenchEntry =
220                    #private_mod::BenchEntry {
221                        meta: #meta_expr,
222                        bench_fn: #bench_fn_expr,
223                        options: #bench_options_expr,
224                    };
225
226                static ANY_ENTRY: #private_mod::AnyBenchEntry =
227                    #private_mod::AnyBenchEntry::Bench(&BENCH_ENTRY);
228
229                static NODE: #private_mod::EntryList<
230                    #private_mod::AnyBenchEntry,
231                > = #private_mod::EntryList::new(&ANY_ENTRY);
232
233                extern "C" fn push() {
234                    #private_mod::BENCH_ENTRIES.push(&NODE);
235                }
236
237                #platform_check
238
239                #pre_main
240                static __PBENCH_PUSH: extern "C" fn() = push;
241            }
242        }
243    };
244
245    let output: QuoteStream = quote! {
246        #input_fn
247
248        #[doc(hidden)]
249        #[allow(non_upper_case_globals)]
250        const #static_name: () = #registration;
251    };
252
253    output.into()
254}
255
256/// Mark a module as a benchmark group.
257///
258/// Groups provide hierarchical organisation and options inheritance.
259/// A group's options cascade to all child benchmarks during resolution.
260///
261/// # Example
262///
263/// ```ignore
264/// #[pbench::bench_group(sample_count = 500)]
265/// mod my_group {
266///     use super::*;
267///
268///     #[pbench::bench]
269///     fn bench_a(b: &Bencher) { /* ... */ }
270/// }
271/// ```
272#[proc_macro_attribute]
273pub fn bench_group(attr: TokenStream, item: TokenStream) -> TokenStream {
274    let input_mod: ItemMod = parse_macro_input!(item as ItemMod);
275    let mod_name: &syn::Ident = &input_mod.ident;
276
277    let options: AttrOptions = match AttrOptions::parse(attr, Macro::Group) {
278        Ok(opts) => opts,
279        Err(err) => return err,
280    };
281
282    let private_mod: &QuoteStream = &options.private_mod;
283    let pre_main: QuoteStream = pre_main_attrs();
284    let platform_check: QuoteStream = unsupported_platform_check();
285    let meta_expr: QuoteStream = entry_meta_expr(private_mod, mod_name);
286    let bench_options_expr: QuoteStream = options.bench_options_fn(None);
287
288    let static_name: syn::Ident =
289        format_ident!("__PBENCH_GROUP_{}", mod_name.to_string().to_uppercase());
290
291    let registration: QuoteStream = quote! {
292        {
293            static GROUP_ENTRY: #private_mod::GroupEntry =
294                #private_mod::GroupEntry {
295                    meta: #meta_expr,
296                    options: #bench_options_expr,
297                };
298
299            static ANY_ENTRY: #private_mod::AnyBenchEntry =
300                #private_mod::AnyBenchEntry::Group(&GROUP_ENTRY);
301
302            static NODE: #private_mod::EntryList<
303                #private_mod::AnyBenchEntry,
304            > = #private_mod::EntryList::new(&ANY_ENTRY);
305
306            extern "C" fn push() {
307                #private_mod::BENCH_ENTRIES.push(&NODE);
308            }
309
310            #platform_check
311
312            #pre_main
313            static __PBENCH_PUSH: extern "C" fn() = push;
314        }
315    };
316
317    let output: QuoteStream = quote! {
318        #input_mod
319
320        #[doc(hidden)]
321        #[allow(non_upper_case_globals)]
322        const #static_name: () = #registration;
323    };
324
325    output.into()
326}