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//!
9//! See also: [`prebindgen`](https://docs.rs/prebindgen) for the main processing library.
10//!
11use prebindgen::{DEFAULT_GROUP_NAME, Record, RecordKind, SourceLocation, get_prebindgen_out_dir};
12use proc_macro::TokenStream;
13use quote::quote;
14use std::fs::{OpenOptions, metadata};
15use std::io::Write;
16use std::path::Path;
17use syn::parse::{Parse, ParseStream};
18use syn::spanned::Spanned;
19use syn::{DeriveInput, ItemConst, ItemFn, ItemType};
20use syn::{Ident, LitBool, LitStr};
21use syn::{Result, Token};
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    skip: bool,
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 skip = false;
68
69        if input.is_empty() {
70            return Ok(PrebindgenArgs { group, skip });
71        }
72
73        // Parse first argument
74        if input.peek(LitStr) {
75            // First argument is a string literal (group name)
76            let group_lit: LitStr = input.parse()?;
77            group = group_lit.value();
78
79            // Check for comma and skip attribute
80            if input.peek(Token![,]) {
81                input.parse::<Token![,]>()?;
82                let skip_ident: Ident = input.parse()?;
83                if skip_ident != "skip" {
84                    return Err(syn::Error::new_spanned(skip_ident, "Expected 'skip'"));
85                }
86                input.parse::<Token![=]>()?;
87                let skip_lit: LitBool = input.parse()?;
88                skip = skip_lit.value();
89            }
90        } else if input.peek(Ident) {
91            // First argument is an identifier (should be 'skip')
92            let skip_ident: Ident = input.parse()?;
93            if skip_ident != "skip" {
94                return Err(syn::Error::new_spanned(skip_ident, "Expected 'skip'"));
95            }
96            input.parse::<Token![=]>()?;
97            let skip_lit: LitBool = input.parse()?;
98            skip = skip_lit.value();
99        } else {
100            return Err(syn::Error::new(input.span(), "Invalid argument format"));
101        }
102
103        Ok(PrebindgenArgs { group, skip })
104    }
105}
106
107/// Get the full path to `{group}_{pid}_{thread_id}.jsonl` generated in OUT_DIR.
108fn get_prebindgen_jsonl_path(name: &str) -> std::path::PathBuf {
109    let thread_id = std::thread::current().id();
110    let process_id = std::process::id();
111    // Extract numeric thread ID from ThreadId debug representation
112    let thread_id_str = format!("{thread_id:?}");
113    let thread_id_num = thread_id_str
114        .strip_prefix("ThreadId(")
115        .and_then(|s| s.strip_suffix(")"))
116        .unwrap_or("0");
117
118    get_prebindgen_out_dir().join(format!("{name}_{process_id}_{thread_id_num}.jsonl"))
119}
120
121/// Proc macro that returns the prebindgen output directory path as a string literal.
122///
123/// This macro generates a string literal containing the full path to the prebindgen
124/// output directory. It should be used to create a public constant that can be
125/// consumed by language-specific binding crates.
126///
127/// # Panics
128///
129/// Panics if OUT_DIR environment variable is not set. This indicates that the macro
130/// is being used outside of a build.rs context.
131///
132/// # Returns
133///
134/// A string literal with the path to the prebindgen output directory.
135///
136/// # Example
137///
138/// ```rust,ignore
139/// use prebindgen_proc_macro::prebindgen_out_dir;
140///
141/// // Create a public constant for use by binding crates
142/// pub const PREBINDGEN_OUT_DIR: &str = prebindgen_out_dir!();
143/// ```
144#[proc_macro]
145pub fn prebindgen_out_dir(_input: TokenStream) -> TokenStream {
146    let out_dir = std::env::var("OUT_DIR")
147        .expect("OUT_DIR environment variable not set. Please ensure you have a build.rs file in your project.");
148    let file_path = std::path::Path::new(&out_dir).join("prebindgen");
149    let path_str = file_path.to_string_lossy();
150
151    let expanded = quote! {
152        #path_str
153    };
154
155    TokenStream::from(expanded)
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/// // Generate all the output for further processing but do not pass code to the compiler
183/// #[prebindgen(skip = true)]
184/// pub fn internal_function() -> i32 {
185///     42
186/// }
187///
188/// // Combine group name with skip
189/// #[prebindgen("functions", skip = true)]
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 `skip = true` to remove the item from compilation output
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    // Get the full path to the JSONL file
210    let file_path = get_prebindgen_jsonl_path(&group);
211    let dest_path = Path::new(&file_path);
212
213    // Try to parse as different item types
214    let (kind, name, content, span) = if let Ok(parsed) = syn::parse::<DeriveInput>(input.clone()) {
215        // Handle struct, enum, union
216        let kind = match &parsed.data {
217            syn::Data::Struct(_) => RecordKind::Struct,
218            syn::Data::Enum(_) => RecordKind::Enum,
219            syn::Data::Union(_) => RecordKind::Union,
220        };
221        let tokens = quote! { #parsed };
222        (
223            kind,
224            parsed.ident.to_string(),
225            tokens.to_string(),
226            parsed.span(),
227        )
228    } else if let Ok(parsed) = syn::parse::<ItemFn>(input.clone()) {
229        // Handle function
230        // For functions, we want to store only the signature without the body
231        let mut fn_sig = parsed.clone();
232        fn_sig.block = syn::parse_quote! {{ /* placeholder */ }};
233        let tokens = quote! { #fn_sig };
234        (
235            RecordKind::Function,
236            parsed.sig.ident.to_string(),
237            tokens.to_string(),
238            parsed.sig.span(),
239        )
240    } else if let Ok(parsed) = syn::parse::<ItemType>(input.clone()) {
241        // Handle type alias
242        let tokens = quote! { #parsed };
243        (
244            RecordKind::TypeAlias,
245            parsed.ident.to_string(),
246            tokens.to_string(),
247            parsed.ident.span(),
248        )
249    } else if let Ok(parsed) = syn::parse::<ItemConst>(input.clone()) {
250        // Handle constant
251        let tokens = quote! { #parsed };
252        (
253            RecordKind::Const,
254            parsed.ident.to_string(),
255            tokens.to_string(),
256            parsed.ident.span(),
257        )
258    } else {
259        // Try to parse as any item to provide better error messages
260        let item = syn::parse::<syn::Item>(input.clone()).ok();
261        return unsupported_item_error(item);
262    };
263
264    // Extract basic source location information available during compilation
265    // Convert proc_macro2::Span to proc_macro::Span to access file() method
266    let source_location = SourceLocation {
267        file: span.unwrap().file(),
268        line: span.unwrap().line(),
269        column: span.unwrap().column(),
270    };
271
272    // Create the new record
273    let new_record = Record::new(kind, name, content, source_location);
274
275    // Convert record to JSON and append to file in JSON-lines format
276    if let Ok(json_content) = serde_json::to_string(&new_record) {
277        if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(dest_path) {
278            // Check if file is empty (just created or was deleted)
279            let is_empty = metadata(dest_path).map(|m| m.len() == 0).unwrap_or(true);
280
281            if is_empty {
282                #[cfg(feature = "debug")]
283                println!("Creating jsonl file: {}", dest_path.display());
284            }
285
286            // Write the record as a single line (JSON-lines format)
287            let _ = writeln!(file, "{json_content}");
288            let _ = file.flush();
289        }
290    }
291
292    if parsed_args.skip {
293        // If skip is true, return empty token stream (eat the input)
294        TokenStream::new()
295    } else {
296        // Otherwise return the original input unchanged
297        input_clone
298    }
299}