prebindgen_proc_macro/
lib.rs1use std::{collections::HashMap, fs::OpenOptions};
13
14use prebindgen::{get_prebindgen_out_dir, Record, RecordKind, SourceLocation, DEFAULT_GROUP_NAME};
15use proc_macro::TokenStream;
16use quote::quote;
17use syn::{
18 parse::{Parse, ParseStream},
19 spanned::Spanned,
20 DeriveInput, Ident, ItemConst, ItemFn, ItemType, LitStr, Result, Token,
21};
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 cfg: Option<String>,
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 cfg = None;
68
69 if input.is_empty() {
70 return Ok(PrebindgenArgs { group, cfg });
71 }
72
73 while !input.is_empty() {
75 if input.peek(LitStr) {
76 let lit: LitStr = input.parse()?;
78 group = lit.value();
79 } else if input.peek(Ident) {
80 let ident: Ident = input.parse()?;
81 input.parse::<Token![=]>()?;
82
83 match ident.to_string().as_str() {
84 "cfg" => {
85 let cfg_lit: LitStr = input.parse()?;
86 cfg = Some(cfg_lit.value());
87 }
88 _ => {
89 return Err(syn::Error::new_spanned(ident, "Expected 'cfg'"));
90 }
91 }
92 } else {
93 return Err(syn::Error::new(input.span(), "Invalid argument format"));
94 }
95
96 if input.peek(Token![,]) {
98 input.parse::<Token![,]>()?;
99 } else if !input.is_empty() {
100 return Err(syn::Error::new(
101 input.span(),
102 "Expected comma between arguments",
103 ));
104 }
105 }
106
107 Ok(PrebindgenArgs { group, cfg })
108 }
109}
110
111thread_local! {
112 static THREAD_ID: std::cell::RefCell<Option<u64>> = const { std::cell::RefCell::new(None) };
113 static JSONL_PATHS: std::cell::RefCell<HashMap<String, std::path::PathBuf>> = std::cell::RefCell::new(HashMap::new());
114}
115
116fn get_prebindgen_jsonl_path(group: &str) -> std::path::PathBuf {
118 if let Some(p) = JSONL_PATHS.with(|path| path.borrow().get(group).cloned()) {
119 return p;
120 }
121 let process_id = std::process::id();
122 let thread_id = if let Some(in_thread_id) = THREAD_ID.with(|id| *id.borrow()) {
123 in_thread_id
124 } else {
125 let new_id = rand::random::<u64>();
126 THREAD_ID.with(|id| *id.borrow_mut() = Some(new_id));
127 new_id
128 };
129 let mut random_value = None;
130 let new_path = loop {
134 let postfix = if let Some(rv) = random_value {
135 format!("_{rv}")
136 } else {
137 "".to_string()
138 };
139 let path = get_prebindgen_out_dir()
140 .join(format!("{group}_{process_id}_{thread_id}{postfix}.jsonl"));
141 if OpenOptions::new()
142 .create_new(true)
143 .write(true)
144 .open(&path)
145 .is_ok()
146 {
147 break path;
148 }
149 random_value = Some(rand::random::<u32>());
150 };
151 JSONL_PATHS.with(|path| {
152 path.borrow_mut()
153 .insert(group.to_string(), new_path.clone());
154 });
155 new_path
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 (kind, name, content, span) = if let Ok(parsed) = syn::parse::<DeriveInput>(input.clone()) {
211 let kind = match &parsed.data {
213 syn::Data::Struct(_) => RecordKind::Struct,
214 syn::Data::Enum(_) => RecordKind::Enum,
215 syn::Data::Union(_) => RecordKind::Union,
216 };
217 let tokens = quote! { #parsed };
218 (
219 kind,
220 parsed.ident.to_string(),
221 tokens.to_string(),
222 parsed.span(),
223 )
224 } else if let Ok(parsed) = syn::parse::<ItemFn>(input.clone()) {
225 let mut fn_sig = parsed.clone();
228 fn_sig.block = syn::parse_quote! {{ }};
229 let tokens = quote! { #fn_sig };
230 (
231 RecordKind::Function,
232 parsed.sig.ident.to_string(),
233 tokens.to_string(),
234 parsed.sig.span(),
235 )
236 } else if let Ok(parsed) = syn::parse::<ItemType>(input.clone()) {
237 let tokens = quote! { #parsed };
239 (
240 RecordKind::TypeAlias,
241 parsed.ident.to_string(),
242 tokens.to_string(),
243 parsed.ident.span(),
244 )
245 } else if let Ok(parsed) = syn::parse::<ItemConst>(input.clone()) {
246 let tokens = quote! { #parsed };
248 (
249 RecordKind::Const,
250 parsed.ident.to_string(),
251 tokens.to_string(),
252 parsed.ident.span(),
253 )
254 } else {
255 let item = syn::parse::<syn::Item>(input.clone()).ok();
257 return unsupported_item_error(item);
258 };
259
260 let source_location = SourceLocation::from_span(&span);
262
263 let new_record = Record::new(
265 kind,
266 name,
267 content,
268 source_location,
269 parsed_args.cfg.clone(),
270 );
271
272 let file_path = get_prebindgen_jsonl_path(&group);
274 if prebindgen::utils::write_to_jsonl_file(&file_path, &[&new_record]).is_err() {
275 return TokenStream::from(quote! {
276 compile_error!("Failed to write prebindgen record");
277 });
278 }
279
280 if let Some(cfg_value) = &parsed_args.cfg {
282 let cfg_tokens: proc_macro2::TokenStream = cfg_value
283 .parse()
284 .unwrap_or_else(|_| panic!("Invalid cfg condition: {}", cfg_value));
285 let cfg_attr = quote! { #[cfg(#cfg_tokens)] };
286 let original_tokens: proc_macro2::TokenStream = input_clone.into();
287 let result = quote! {
288 #cfg_attr
289 #original_tokens
290 };
291 result.into()
292 } else {
293 input_clone
295 }
296}
297
298#[proc_macro]
322pub fn prebindgen_out_dir(_input: TokenStream) -> TokenStream {
323 let out_dir = std::env::var("OUT_DIR")
324 .expect("OUT_DIR environment variable not set. Please ensure you have a build.rs file in your project.");
325 let file_path = std::path::Path::new(&out_dir).join("prebindgen");
326 let path_str = file_path.to_string_lossy();
327
328 let expanded = quote! {
329 #path_str
330 };
331
332 TokenStream::from(expanded)
333}
334
335#[proc_macro]
358pub fn features(_input: TokenStream) -> TokenStream {
359 let features = std::env::var("PREBINDGEN_FEATURES").expect(
360 "PREBINDGEN_FEATURES environment variable not set. Ensure prebindgen::init_prebindgen_out_dir() is called in build.rs",
361 );
362 let lit = syn::LitStr::new(&features, proc_macro2::Span::call_site());
363 TokenStream::from(quote! { #lit })
364}