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}