prebindgen_proc_macro/
lib.rs1use 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
21fn 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 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
56struct 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 if input.peek(LitStr) {
73 let group_lit: LitStr = input.parse()?;
75 group = group_lit.value();
76
77 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 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
105fn 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 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]
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#[proc_macro_attribute]
199pub fn prebindgen(args: TokenStream, input: TokenStream) -> TokenStream {
200 let input_clone = input.clone();
201
202 let parsed_args = syn::parse::<PrebindgenArgs>(args).expect("Invalid #[prebindgen] arguments");
204
205 let group = parsed_args.group;
206
207 let file_path = get_prebindgen_jsonl_path(&group);
209 let dest_path = Path::new(&file_path);
210
211 let (kind, name, content, span) = if let Ok(parsed) = syn::parse::<DeriveInput>(input.clone()) {
213 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 let mut fn_sig = parsed.clone();
230 fn_sig.block = syn::parse_quote! {{ }};
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 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 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 let item = syn::parse::<syn::Item>(input.clone()).ok();
259 return unsupported_item_error(item);
260 };
261
262 let source_location = SourceLocation {
265 file: span.unwrap().file(),
266 line: span.unwrap().line(),
267 column: span.unwrap().column(),
268 };
269
270 let new_record = Record::new(kind, name, content, source_location);
272
273 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 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 let _ = writeln!(file, "{json_content}");
286 let _ = file.flush();
287 }
288 }
289
290 if parsed_args.skip {
291 TokenStream::new()
293 } else {
294 input_clone
296 }
297}