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//! These macros are typically imported from the main `prebindgen` crate rather than
10//! used directly from this crate.
11
12use prebindgen::{get_prebindgen_out_dir, trace, Record, RecordKind, DEFAULT_GROUP_NAME};
13use proc_macro::TokenStream;
14use quote::quote;
15use syn::LitStr;
16use std::fs::{OpenOptions, metadata};
17use std::io::Write;
18use std::path::Path;
19use syn::{DeriveInput, ItemFn};
20
21/// Get the full path to `{group}_{pid}_{thread_id}.jsonl` generated in OUT_DIR.
22fn get_prebindgen_jsonl_path(name: &str) -> std::path::PathBuf {
23    let thread_id = std::thread::current().id();
24    let process_id = std::process::id();
25    // Extract numeric thread ID from ThreadId debug representation
26    let thread_id_str = format!("{:?}", thread_id);
27    let thread_id_num = thread_id_str
28        .strip_prefix("ThreadId(")
29        .and_then(|s| s.strip_suffix(")"))
30        .unwrap_or("0");
31    get_prebindgen_out_dir().join(format!("{}_{}_{}.jsonl", name, process_id, thread_id_num))
32}
33
34/// Proc macro that returns the prebindgen output directory path as a string literal.
35/// 
36/// This macro generates a string literal containing the full path to the prebindgen
37/// output directory. It should be used to create a public constant that can be
38/// consumed by language-specific binding crates.
39/// 
40/// # Returns
41/// 
42/// A string literal with the path to the prebindgen output directory.
43/// 
44/// # Example
45/// 
46/// ```rust,ignore
47/// use prebindgen_proc_macro::prebindgen_out_dir;
48/// 
49/// // Create a public constant for use by binding crates
50/// pub const PREBINDGEN_OUT_DIR: &str = prebindgen_out_dir!();
51/// ```
52#[proc_macro]
53pub fn prebindgen_out_dir(_input: TokenStream) -> TokenStream {
54    let file_path = get_prebindgen_out_dir();
55    let path_str = file_path.to_string_lossy();
56
57    // Return just the string literal
58    let expanded = quote! {
59        #path_str
60    };
61
62    TokenStream::from(expanded)
63}
64
65/// Attribute macro that exports FFI definitions for use in language-specific binding crates.
66/// 
67/// All types and functions marked with this attribute can be made available in dependent 
68/// crates as Rust source code for both binding generator processing (cbindgen, csbindgen, etc.) 
69/// and for including into projects to make the compiler generate `#[no_mangle]` FFI exports 
70/// for cdylib/staticlib targets.
71/// 
72/// # Usage
73/// 
74/// ```rust,ignore
75/// // Use with explicit group name
76/// #[prebindgen("group_name")]
77/// #[repr(C)]
78/// pub struct Point {
79///     pub x: f64,
80///     pub y: f64,
81/// }
82/// 
83/// // Use with default group name "default"
84/// #[prebindgen]
85/// pub fn calculate_distance(p1: &Point, p2: &Point) -> f64 {
86///     ((p2.x - p1.x).powi(2) + (p2.y - p1.y).powi(2)).sqrt()
87/// }
88/// 
89/// // Or specify a group name
90/// #[prebindgen("functions")]
91/// pub fn another_function() -> i32 {
92///     42
93/// }
94/// ```
95/// 
96/// # Requirements
97/// 
98/// - Must call `prebindgen::init_prebindgen_out_dir()` in your crate's `build.rs`
99/// - Optionally takes a string literal group name for organization (defaults to "default")
100#[proc_macro_attribute]
101pub fn prebindgen(args: TokenStream, input: TokenStream) -> TokenStream {
102    let input_clone = input.clone();
103    
104    // Parse optional group name literal - use default if not provided
105    let group = if args.is_empty() {
106        DEFAULT_GROUP_NAME.to_string()
107    } else {
108        let group_lit = syn::parse::<LitStr>(args)
109            .expect("`#[prebindgen]` group name must be a string literal");
110        group_lit.value()
111    };
112    
113    // Get the full path to the JSONL file
114    let file_path = get_prebindgen_jsonl_path(&group);
115    let dest_path = Path::new(&file_path);
116
117    // Try to parse as different item types
118    let (kind, name, content) = if let Ok(parsed) = syn::parse::<DeriveInput>(input.clone()) {
119        // Handle struct, enum, union
120        let kind = match &parsed.data {
121            syn::Data::Struct(_) => RecordKind::Struct,
122            syn::Data::Enum(_) => RecordKind::Enum,
123            syn::Data::Union(_) => RecordKind::Union,
124        };
125        let tokens = quote! { #parsed };
126        (kind, parsed.ident.to_string(), tokens.to_string())
127    } else if let Ok(parsed) = syn::parse::<ItemFn>(input.clone()) {
128        // Handle function
129        // For functions, we want to store only the signature without the body
130        let mut fn_sig = parsed.clone();
131        fn_sig.block = syn::parse_quote! {{ /* placeholder */ }};
132        let tokens = quote! { #fn_sig };
133        (
134            RecordKind::Function,
135            parsed.sig.ident.to_string(),
136            tokens.to_string(),
137        )
138    } else {
139        // If we can't parse it, return the original input and skip processing
140        return input_clone;
141    };
142
143    // Create the new record
144    let new_record = Record::new(kind, name, content);
145
146    // Convert record to JSON and append to file in JSON-lines format
147    if let Ok(json_content) = serde_json::to_string(&new_record) {
148        if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(dest_path) {
149            // Check if file is empty (just created or was deleted)
150            let is_empty = metadata(dest_path).map(|m| m.len() == 0).unwrap_or(true);
151
152            if is_empty {
153                // Create new JSONL file
154                trace!("Creating jsonl file: {}", dest_path.display());
155            }
156            
157            // Write the record as a single line (JSON-lines format)
158            let _ = writeln!(file, "{}", json_content);
159            let _ = file.flush();
160        }
161    }
162
163    // Return the original input unchanged
164    input_clone
165}