prebindgen_proc_macro/
lib.rs1use 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
23fn 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 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
58struct 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 if input.peek(LitStr) {
75 let group_lit: LitStr = input.parse()?;
77 group = group_lit.value();
78
79 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 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
107fn 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 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]
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#[proc_macro_attribute]
201pub fn prebindgen(args: TokenStream, input: TokenStream) -> TokenStream {
202 let input_clone = input.clone();
203
204 let parsed_args = syn::parse::<PrebindgenArgs>(args).expect("Invalid #[prebindgen] arguments");
206
207 let group = parsed_args.group;
208
209 let file_path = get_prebindgen_jsonl_path(&group);
211 let dest_path = Path::new(&file_path);
212
213 let (kind, name, content, span) = if let Ok(parsed) = syn::parse::<DeriveInput>(input.clone()) {
215 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 let mut fn_sig = parsed.clone();
232 fn_sig.block = syn::parse_quote! {{ }};
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 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 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 let item = syn::parse::<syn::Item>(input.clone()).ok();
261 return unsupported_item_error(item);
262 };
263
264 let source_location = SourceLocation {
267 file: span.unwrap().file(),
268 line: span.unwrap().line(),
269 column: span.unwrap().column(),
270 };
271
272 let new_record = Record::new(kind, name, content, source_location);
274
275 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 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 let _ = writeln!(file, "{json_content}");
288 let _ = file.flush();
289 }
290 }
291
292 if parsed_args.skip {
293 TokenStream::new()
295 } else {
296 input_clone
298 }
299}