prebindgen_proc_macro/
lib.rs

1//! # prebindgen-proc-macro
2//!
3//! Procedural macros for the prebindgen system.
4//!
5//! This crate provides the procedural macros used by the prebindgen system:
6//! - `#[prebindgen]` or `#[prebindgen("group")]` - Attribute macro for marking FFI definitions
7//! - `prebindgen_out_dir!()` - Macro that returns the prebindgen output directory path
8//! - `features!()` - Macro that returns the list of features enabled for the crate
9//!
10//! See also: [`prebindgen`](https://docs.rs/prebindgen) for the main processing library.
11//!
12use std::{collections::HashMap, fs::OpenOptions};
13
14use prebindgen::{get_prebindgen_out_dir, Record, RecordKind, SourceLocation, DEFAULT_GROUP_NAME};
15use proc_macro::TokenStream;
16use quote::quote;
17use syn::{
18    parse::{Parse, ParseStream},
19    spanned::Spanned,
20    DeriveInput, Ident, ItemConst, ItemFn, ItemType, LitStr, Result, Token,
21};
22
23/// Helper function to generate consistent error messages for unsupported or unparseable items.
24fn unsupported_item_error(item: Option<syn::Item>) -> TokenStream {
25    match item {
26        Some(item) => {
27            let item_type = match &item {
28                syn::Item::Static(_) => "Static items",
29                syn::Item::Mod(_) => "Modules",
30                syn::Item::Trait(_) => "Traits",
31                syn::Item::Impl(_) => "Impl blocks",
32                syn::Item::Use(_) => "Use statements",
33                syn::Item::ExternCrate(_) => "Extern crate declarations",
34                syn::Item::Macro(_) => "Macro definitions",
35                syn::Item::Verbatim(_) => "Verbatim items",
36                _ => "This item type",
37            };
38
39            syn::Error::new_spanned(
40                item,
41                format!("{item_type} are not supported by #[prebindgen]"),
42            )
43            .to_compile_error()
44            .into()
45        }
46        None => {
47            // If we can't even parse it as an Item, return a generic error
48            syn::Error::new(
49                proc_macro2::Span::call_site(),
50                "Invalid syntax for #[prebindgen]",
51            )
52            .to_compile_error()
53            .into()
54        }
55    }
56}
57
58/// Arguments for the prebindgen macro
59struct PrebindgenArgs {
60    group: String,
61    cfg: Option<String>,
62}
63
64impl Parse for PrebindgenArgs {
65    fn parse(input: ParseStream) -> Result<Self> {
66        let mut group = DEFAULT_GROUP_NAME.to_string();
67        let mut cfg = None;
68
69        if input.is_empty() {
70            return Ok(PrebindgenArgs { group, cfg });
71        }
72
73        // Parse arguments in any order
74        while !input.is_empty() {
75            if input.peek(LitStr) {
76                // String literal - could be group name
77                let lit: LitStr = input.parse()?;
78                group = lit.value();
79            } else if input.peek(Ident) {
80                let ident: Ident = input.parse()?;
81                input.parse::<Token![=]>()?;
82
83                match ident.to_string().as_str() {
84                    "cfg" => {
85                        let cfg_lit: LitStr = input.parse()?;
86                        cfg = Some(cfg_lit.value());
87                    }
88                    _ => {
89                        return Err(syn::Error::new_spanned(ident, "Expected 'cfg'"));
90                    }
91                }
92            } else {
93                return Err(syn::Error::new(input.span(), "Invalid argument format"));
94            }
95
96            // Parse optional comma
97            if input.peek(Token![,]) {
98                input.parse::<Token![,]>()?;
99            } else if !input.is_empty() {
100                return Err(syn::Error::new(
101                    input.span(),
102                    "Expected comma between arguments",
103                ));
104            }
105        }
106
107        Ok(PrebindgenArgs { group, cfg })
108    }
109}
110
111thread_local! {
112    static THREAD_ID: std::cell::RefCell<Option<u64>> = const { std::cell::RefCell::new(None) };
113    static JSONL_PATHS: std::cell::RefCell<HashMap<String, std::path::PathBuf>> = std::cell::RefCell::new(HashMap::new());
114}
115
116/// Get the full path to `{group}_{pid}_{thread_id}.jsonl` generated in OUT_DIR.
117fn get_prebindgen_jsonl_path(group: &str) -> std::path::PathBuf {
118    if let Some(p) = JSONL_PATHS.with(|path| path.borrow().get(group).cloned()) {
119        return p;
120    }
121    let process_id = std::process::id();
122    let thread_id = if let Some(in_thread_id) = THREAD_ID.with(|id| *id.borrow()) {
123        in_thread_id
124    } else {
125        let new_id = rand::random::<u64>();
126        THREAD_ID.with(|id| *id.borrow_mut() = Some(new_id));
127        new_id
128    };
129    let mut random_value = None;
130    // Try to really create file and repeat until success
131    // to avoid collisions in extremely rare case when two threads got
132    // the same random value
133    let new_path = loop {
134        let postfix = if let Some(rv) = random_value {
135            format!("_{rv}")
136        } else {
137            "".to_string()
138        };
139        let path = get_prebindgen_out_dir()
140            .join(format!("{group}_{process_id}_{thread_id}{postfix}.jsonl"));
141        if OpenOptions::new()
142            .create_new(true)
143            .write(true)
144            .open(&path)
145            .is_ok()
146        {
147            break path;
148        }
149        random_value = Some(rand::random::<u32>());
150    };
151    JSONL_PATHS.with(|path| {
152        path.borrow_mut()
153            .insert(group.to_string(), new_path.clone());
154    });
155    new_path
156}
157
158/// Attribute macro that exports FFI definitions for use in language-specific binding crates.
159///
160/// All types and functions marked with this attribute can be made available in dependent
161/// crates as Rust source code for both binding generator processing (cbindgen, csbindgen, etc.)
162/// and for including into projects to make the compiler generate `#[no_mangle]` FFI exports
163/// for cdylib/staticlib targets.
164///
165/// # Usage
166///
167/// ```rust,ignore
168/// // Use with explicit group name
169/// #[prebindgen("group_name")]
170/// #[repr(C)]
171/// pub struct Point {
172///     pub x: f64,
173///     pub y: f64,
174/// }
175///
176/// // Use with default group name "default"
177/// #[prebindgen]
178/// pub fn calculate_distance(p1: &Point, p2: &Point) -> f64 {
179///     ((p2.x - p1.x).powi(2) + (p2.y - p1.y).powi(2)).sqrt()
180/// }
181///
182/// // Add cfg attribute to generated code
183/// #[prebindgen(cfg = "feature = \"experimental\"")]
184/// pub fn experimental_function() -> i32 {
185///     42
186/// }
187///
188/// // Combine group name with cfg
189/// #[prebindgen("functions", cfg = "unix")]
190/// pub fn another_function() -> i32 {
191///     42
192/// }
193/// ```
194///
195/// # Requirements
196///
197/// - Must call `prebindgen::init_prebindgen_out_dir()` in your crate's `build.rs`
198/// - Optionally takes a string literal group name for organization (defaults to "default")
199/// - Optionally takes `cfg = "condition"` to add `#[cfg(condition)]` to generated code
200#[proc_macro_attribute]
201pub fn prebindgen(args: TokenStream, input: TokenStream) -> TokenStream {
202    let input_clone = input.clone();
203
204    // Parse arguments
205    let parsed_args = syn::parse::<PrebindgenArgs>(args).expect("Invalid #[prebindgen] arguments");
206
207    let group = parsed_args.group;
208
209    // Try to parse as different item types
210    let (kind, name, content, span) = if let Ok(parsed) = syn::parse::<DeriveInput>(input.clone()) {
211        // Handle struct, enum, union
212        let kind = match &parsed.data {
213            syn::Data::Struct(_) => RecordKind::Struct,
214            syn::Data::Enum(_) => RecordKind::Enum,
215            syn::Data::Union(_) => RecordKind::Union,
216        };
217        let tokens = quote! { #parsed };
218        (
219            kind,
220            parsed.ident.to_string(),
221            tokens.to_string(),
222            parsed.span(),
223        )
224    } else if let Ok(parsed) = syn::parse::<ItemFn>(input.clone()) {
225        // Handle function
226        // For functions, we want to store only the signature without the body
227        let mut fn_sig = parsed.clone();
228        fn_sig.block = syn::parse_quote! {{ /* placeholder */ }};
229        let tokens = quote! { #fn_sig };
230        (
231            RecordKind::Function,
232            parsed.sig.ident.to_string(),
233            tokens.to_string(),
234            parsed.sig.span(),
235        )
236    } else if let Ok(parsed) = syn::parse::<ItemType>(input.clone()) {
237        // Handle type alias
238        let tokens = quote! { #parsed };
239        (
240            RecordKind::TypeAlias,
241            parsed.ident.to_string(),
242            tokens.to_string(),
243            parsed.ident.span(),
244        )
245    } else if let Ok(parsed) = syn::parse::<ItemConst>(input.clone()) {
246        // Handle constant
247        let tokens = quote! { #parsed };
248        (
249            RecordKind::Const,
250            parsed.ident.to_string(),
251            tokens.to_string(),
252            parsed.ident.span(),
253        )
254    } else {
255        // Try to parse as any item to provide better error messages
256        let item = syn::parse::<syn::Item>(input.clone()).ok();
257        return unsupported_item_error(item);
258    };
259
260    // Extract basic source location information available during compilation
261    let source_location = SourceLocation::from_span(&span);
262
263    // Create the new record
264    let new_record = Record::new(
265        kind,
266        name,
267        content,
268        source_location,
269        parsed_args.cfg.clone(),
270    );
271
272    // Get the full path to the JSONL file
273    let file_path = get_prebindgen_jsonl_path(&group);
274    if prebindgen::utils::write_to_jsonl_file(&file_path, &[&new_record]).is_err() {
275        return TokenStream::from(quote! {
276            compile_error!("Failed to write prebindgen record");
277        });
278    }
279
280    // Apply cfg attribute to the original code if specified
281    if let Some(cfg_value) = &parsed_args.cfg {
282        let cfg_tokens: proc_macro2::TokenStream = cfg_value
283            .parse()
284            .unwrap_or_else(|_| panic!("Invalid cfg condition: {}", cfg_value));
285        let cfg_attr = quote! { #[cfg(#cfg_tokens)] };
286        let original_tokens: proc_macro2::TokenStream = input_clone.into();
287        let result = quote! {
288            #cfg_attr
289            #original_tokens
290        };
291        result.into()
292    } else {
293        // Otherwise return the original input unchanged
294        input_clone
295    }
296}
297
298/// Proc macro that returns the prebindgen output directory path as a string literal.
299///
300/// This macro generates a string literal containing the full path to the prebindgen
301/// output directory. It should be used to create a public constant that can be
302/// consumed by language-specific binding crates.
303///
304/// # Panics
305///
306/// Panics if OUT_DIR environment variable is not set. This indicates that the macro
307/// is being used outside of a build.rs context.
308///
309/// # Returns
310///
311/// A string literal with the path to the prebindgen output directory.
312///
313/// # Example
314///
315/// ```rust,ignore
316/// use prebindgen_proc_macro::prebindgen_out_dir;
317///
318/// // Create a public constant for use by binding crates
319/// pub const PREBINDGEN_OUT_DIR: &str = prebindgen_out_dir!();
320/// ```
321#[proc_macro]
322pub fn prebindgen_out_dir(_input: TokenStream) -> TokenStream {
323    let out_dir = std::env::var("OUT_DIR")
324        .expect("OUT_DIR environment variable not set. Please ensure you have a build.rs file in your project.");
325    let file_path = std::path::Path::new(&out_dir).join("prebindgen");
326    let path_str = file_path.to_string_lossy();
327
328    let expanded = quote! {
329        #path_str
330    };
331
332    TokenStream::from(expanded)
333}
334
335/// Proc macro that returns the enabled features, joined by commas, as a string literal.
336///
337/// The value is sourced from the `PREBINDGEN_FEATURES` compile-time environment variable,
338/// which is set by calling `prebindgen::init_prebindgen_out_dir()` in your crate's `build.rs`.
339///
340/// # Panics
341///
342/// Emits a compile-time error if `PREBINDGEN_FEATURES` is not set, which typically means
343/// `prebindgen::init_prebindgen_out_dir()` wasn't called in `build.rs`.
344///
345/// # Returns
346///
347/// A string literal containing the comma-separated list of enabled features.
348/// The string may be empty if no features are enabled.
349///
350/// # Example
351///
352/// ```rust,ignore
353/// use prebindgen_proc_macro::features;
354///
355/// pub const ENABLED_FEATURES: &str = features!();
356/// ```
357#[proc_macro]
358pub fn features(_input: TokenStream) -> TokenStream {
359    let features = std::env::var("PREBINDGEN_FEATURES").expect(
360        "PREBINDGEN_FEATURES environment variable not set. Ensure prebindgen::init_prebindgen_out_dir() is called in build.rs",
361    );
362    let lit = syn::LitStr::new(&features, proc_macro2::Span::call_site());
363    TokenStream::from(quote! { #lit })
364}