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//!
9use prebindgen::{DEFAULT_GROUP_NAME, Record, RecordKind, SourceLocation, get_prebindgen_out_dir};
10use proc_macro::TokenStream;
11use quote::quote;
12use std::fs::{OpenOptions, metadata};
13use std::io::Write;
14use std::path::Path;
15use syn::parse::{Parse, ParseStream};
16use syn::spanned::Spanned;
17use syn::{DeriveInput, ItemConst, ItemFn, ItemType};
18use syn::{Ident, LitBool, LitStr};
19use syn::{Result, Token};
20
21/// Helper function to generate consistent error messages for unsupported or unparseable items.
22fn unsupported_item_error(item: Option<syn::Item>) -> TokenStream {
23    match item {
24        Some(item) => {
25            let item_type = match &item {
26                syn::Item::Static(_) => "Static items",
27                syn::Item::Mod(_) => "Modules",
28                syn::Item::Trait(_) => "Traits",
29                syn::Item::Impl(_) => "Impl blocks",
30                syn::Item::Use(_) => "Use statements",
31                syn::Item::ExternCrate(_) => "Extern crate declarations",
32                syn::Item::Macro(_) => "Macro definitions",
33                syn::Item::Verbatim(_) => "Verbatim items",
34                _ => "This item type",
35            };
36
37            syn::Error::new_spanned(
38                item,
39                format!("{item_type} are not supported by #[prebindgen]"),
40            )
41            .to_compile_error()
42            .into()
43        }
44        None => {
45            // If we can't even parse it as an Item, return a generic error
46            syn::Error::new(
47                proc_macro2::Span::call_site(),
48                "Invalid syntax for #[prebindgen]",
49            )
50            .to_compile_error()
51            .into()
52        }
53    }
54}
55
56/// Arguments for the prebindgen macro
57struct PrebindgenArgs {
58    group: String,
59    skip: bool,
60}
61
62impl Parse for PrebindgenArgs {
63    fn parse(input: ParseStream) -> Result<Self> {
64        let mut group = DEFAULT_GROUP_NAME.to_string();
65        let mut skip = false;
66
67        if input.is_empty() {
68            return Ok(PrebindgenArgs { group, skip });
69        }
70
71        // Parse first argument
72        if input.peek(LitStr) {
73            // First argument is a string literal (group name)
74            let group_lit: LitStr = input.parse()?;
75            group = group_lit.value();
76
77            // Check for comma and skip attribute
78            if input.peek(Token![,]) {
79                input.parse::<Token![,]>()?;
80                let skip_ident: Ident = input.parse()?;
81                if skip_ident != "skip" {
82                    return Err(syn::Error::new_spanned(skip_ident, "Expected 'skip'"));
83                }
84                input.parse::<Token![=]>()?;
85                let skip_lit: LitBool = input.parse()?;
86                skip = skip_lit.value();
87            }
88        } else if input.peek(Ident) {
89            // First argument is an identifier (should be 'skip')
90            let skip_ident: Ident = input.parse()?;
91            if skip_ident != "skip" {
92                return Err(syn::Error::new_spanned(skip_ident, "Expected 'skip'"));
93            }
94            input.parse::<Token![=]>()?;
95            let skip_lit: LitBool = input.parse()?;
96            skip = skip_lit.value();
97        } else {
98            return Err(syn::Error::new(input.span(), "Invalid argument format"));
99        }
100
101        Ok(PrebindgenArgs { group, skip })
102    }
103}
104
105/// Get the full path to `{group}_{pid}_{thread_id}.jsonl` generated in OUT_DIR.
106fn get_prebindgen_jsonl_path(name: &str) -> std::path::PathBuf {
107    let thread_id = std::thread::current().id();
108    let process_id = std::process::id();
109    // Extract numeric thread ID from ThreadId debug representation
110    let thread_id_str = format!("{thread_id:?}");
111    let thread_id_num = thread_id_str
112        .strip_prefix("ThreadId(")
113        .and_then(|s| s.strip_suffix(")"))
114        .unwrap_or("0");
115
116    get_prebindgen_out_dir().join(format!("{name}_{process_id}_{thread_id_num}.jsonl"))
117}
118
119/// Proc macro that returns the prebindgen output directory path as a string literal.
120///
121/// This macro generates a string literal containing the full path to the prebindgen
122/// output directory. It should be used to create a public constant that can be
123/// consumed by language-specific binding crates.
124///
125/// # Panics
126///
127/// Panics if OUT_DIR environment variable is not set. This indicates that the macro
128/// is being used outside of a build.rs context.
129///
130/// # Returns
131///
132/// A string literal with the path to the prebindgen output directory.
133///
134/// # Example
135///
136/// ```rust,ignore
137/// use prebindgen_proc_macro::prebindgen_out_dir;
138///
139/// // Create a public constant for use by binding crates
140/// pub const PREBINDGEN_OUT_DIR: &str = prebindgen_out_dir!();
141/// ```
142#[proc_macro]
143pub fn prebindgen_out_dir(_input: TokenStream) -> TokenStream {
144    let out_dir = std::env::var("OUT_DIR")
145        .expect("OUT_DIR environment variable not set. Please ensure you have a build.rs file in your project.");
146    let file_path = std::path::Path::new(&out_dir).join("prebindgen");
147    let path_str = file_path.to_string_lossy();
148
149    let expanded = quote! {
150        #path_str
151    };
152
153    TokenStream::from(expanded)
154}
155
156/// Attribute macro that exports FFI definitions for use in language-specific binding crates.
157///
158/// All types and functions marked with this attribute can be made available in dependent
159/// crates as Rust source code for both binding generator processing (cbindgen, csbindgen, etc.)
160/// and for including into projects to make the compiler generate `#[no_mangle]` FFI exports
161/// for cdylib/staticlib targets.
162///
163/// # Usage
164///
165/// ```rust,ignore
166/// // Use with explicit group name
167/// #[prebindgen("group_name")]
168/// #[repr(C)]
169/// pub struct Point {
170///     pub x: f64,
171///     pub y: f64,
172/// }
173///
174/// // Use with default group name "default"
175/// #[prebindgen]
176/// pub fn calculate_distance(p1: &Point, p2: &Point) -> f64 {
177///     ((p2.x - p1.x).powi(2) + (p2.y - p1.y).powi(2)).sqrt()
178/// }
179///
180/// // Generate all the output for further processing but do not pass code to the compiler
181/// #[prebindgen(skip = true)]
182/// pub fn internal_function() -> i32 {
183///     42
184/// }
185///
186/// // Combine group name with skip
187/// #[prebindgen("functions", skip = true)]
188/// pub fn another_function() -> i32 {
189///     42
190/// }
191/// ```
192///
193/// # Requirements
194///
195/// - Must call `prebindgen::init_prebindgen_out_dir()` in your crate's `build.rs`
196/// - Optionally takes a string literal group name for organization (defaults to "default")
197/// - Optionally takes `skip = true` to remove the item from compilation output
198#[proc_macro_attribute]
199pub fn prebindgen(args: TokenStream, input: TokenStream) -> TokenStream {
200    let input_clone = input.clone();
201
202    // Parse arguments
203    let parsed_args = syn::parse::<PrebindgenArgs>(args).expect("Invalid #[prebindgen] arguments");
204
205    let group = parsed_args.group;
206
207    // Get the full path to the JSONL file
208    let file_path = get_prebindgen_jsonl_path(&group);
209    let dest_path = Path::new(&file_path);
210
211    // Try to parse as different item types
212    let (kind, name, content, span) = if let Ok(parsed) = syn::parse::<DeriveInput>(input.clone()) {
213        // Handle struct, enum, union
214        let kind = match &parsed.data {
215            syn::Data::Struct(_) => RecordKind::Struct,
216            syn::Data::Enum(_) => RecordKind::Enum,
217            syn::Data::Union(_) => RecordKind::Union,
218        };
219        let tokens = quote! { #parsed };
220        (
221            kind,
222            parsed.ident.to_string(),
223            tokens.to_string(),
224            parsed.span(),
225        )
226    } else if let Ok(parsed) = syn::parse::<ItemFn>(input.clone()) {
227        // Handle function
228        // For functions, we want to store only the signature without the body
229        let mut fn_sig = parsed.clone();
230        fn_sig.block = syn::parse_quote! {{ /* placeholder */ }};
231        let tokens = quote! { #fn_sig };
232        (
233            RecordKind::Function,
234            parsed.sig.ident.to_string(),
235            tokens.to_string(),
236            parsed.sig.span(),
237        )
238    } else if let Ok(parsed) = syn::parse::<ItemType>(input.clone()) {
239        // Handle type alias
240        let tokens = quote! { #parsed };
241        (
242            RecordKind::TypeAlias,
243            parsed.ident.to_string(),
244            tokens.to_string(),
245            parsed.ident.span(),
246        )
247    } else if let Ok(parsed) = syn::parse::<ItemConst>(input.clone()) {
248        // Handle constant
249        let tokens = quote! { #parsed };
250        (
251            RecordKind::Const,
252            parsed.ident.to_string(),
253            tokens.to_string(),
254            parsed.ident.span(),
255        )
256    } else {
257        // Try to parse as any item to provide better error messages
258        let item = syn::parse::<syn::Item>(input.clone()).ok();
259        return unsupported_item_error(item);
260    };
261
262    // Extract basic source location information available during compilation
263    // Convert proc_macro2::Span to proc_macro::Span to access file() method
264    let source_location = SourceLocation {
265        file: span.unwrap().file(),
266        line: span.unwrap().line(),
267        column: span.unwrap().column(),
268    };
269
270    // Create the new record
271    let new_record = Record::new(kind, name, content, source_location);
272
273    // Convert record to JSON and append to file in JSON-lines format
274    if let Ok(json_content) = serde_json::to_string(&new_record) {
275        if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(dest_path) {
276            // Check if file is empty (just created or was deleted)
277            let is_empty = metadata(dest_path).map(|m| m.len() == 0).unwrap_or(true);
278
279            if is_empty {
280                #[cfg(feature = "debug")]
281                println!("Creating jsonl file: {}", dest_path.display());
282            }
283
284            // Write the record as a single line (JSON-lines format)
285            let _ = writeln!(file, "{json_content}");
286            let _ = file.flush();
287        }
288    }
289
290    if parsed_args.skip {
291        // If skip is true, return empty token stream (eat the input)
292        TokenStream::new()
293    } else {
294        // Otherwise return the original input unchanged
295        input_clone
296    }
297}