use std::{collections::HashMap, fs::OpenOptions};
use prebindgen::{get_prebindgen_out_dir, Record, RecordKind, SourceLocation, DEFAULT_GROUP_NAME};
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
DeriveInput, Ident, ItemConst, ItemFn, ItemType, LitStr, Result, Token,
};
fn unsupported_item_error(item: Option<syn::Item>) -> TokenStream {
match item {
Some(item) => {
let item_type = match &item {
syn::Item::Static(_) => "Static items",
syn::Item::Mod(_) => "Modules",
syn::Item::Trait(_) => "Traits",
syn::Item::Impl(_) => "Impl blocks",
syn::Item::Use(_) => "Use statements",
syn::Item::ExternCrate(_) => "Extern crate declarations",
syn::Item::Macro(_) => "Macro definitions",
syn::Item::Verbatim(_) => "Verbatim items",
_ => "This item type",
};
syn::Error::new_spanned(
item,
format!("{item_type} are not supported by #[prebindgen]"),
)
.to_compile_error()
.into()
}
None => {
syn::Error::new(
proc_macro2::Span::call_site(),
"Invalid syntax for #[prebindgen]",
)
.to_compile_error()
.into()
}
}
}
struct PrebindgenArgs {
group: String,
cfg: Option<String>,
}
impl Parse for PrebindgenArgs {
fn parse(input: ParseStream) -> Result<Self> {
let mut group = DEFAULT_GROUP_NAME.to_string();
let mut cfg = None;
if input.is_empty() {
return Ok(PrebindgenArgs { group, cfg });
}
while !input.is_empty() {
if input.peek(LitStr) {
let lit: LitStr = input.parse()?;
group = lit.value();
} else if input.peek(Ident) {
let ident: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match ident.to_string().as_str() {
"cfg" => {
let cfg_lit: LitStr = input.parse()?;
cfg = Some(cfg_lit.value());
}
_ => {
return Err(syn::Error::new_spanned(ident, "Expected 'cfg'"));
}
}
} else {
return Err(syn::Error::new(input.span(), "Invalid argument format"));
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
} else if !input.is_empty() {
return Err(syn::Error::new(
input.span(),
"Expected comma between arguments",
));
}
}
Ok(PrebindgenArgs { group, cfg })
}
}
thread_local! {
static THREAD_ID: std::cell::RefCell<Option<u64>> = const { std::cell::RefCell::new(None) };
static JSONL_PATHS: std::cell::RefCell<HashMap<String, std::path::PathBuf>> = std::cell::RefCell::new(HashMap::new());
}
fn get_prebindgen_jsonl_path(group: &str) -> std::path::PathBuf {
if let Some(p) = JSONL_PATHS.with(|path| path.borrow().get(group).cloned()) {
return p;
}
let process_id = std::process::id();
let thread_id = if let Some(in_thread_id) = THREAD_ID.with(|id| *id.borrow()) {
in_thread_id
} else {
let new_id = rand::random::<u64>();
THREAD_ID.with(|id| *id.borrow_mut() = Some(new_id));
new_id
};
let mut random_value = None;
let new_path = loop {
let postfix = if let Some(rv) = random_value {
format!("_{rv}")
} else {
"".to_string()
};
let path = get_prebindgen_out_dir()
.join(format!("{group}_{process_id}_{thread_id}{postfix}.jsonl"));
if OpenOptions::new()
.create_new(true)
.write(true)
.open(&path)
.is_ok()
{
break path;
}
random_value = Some(rand::random::<u32>());
};
JSONL_PATHS.with(|path| {
path.borrow_mut()
.insert(group.to_string(), new_path.clone());
});
new_path
}
#[proc_macro_attribute]
pub fn prebindgen(args: TokenStream, input: TokenStream) -> TokenStream {
let input_clone = input.clone();
let parsed_args = syn::parse::<PrebindgenArgs>(args).expect("Invalid #[prebindgen] arguments");
let group = parsed_args.group;
let (kind, name, content, span) = if let Ok(parsed) = syn::parse::<DeriveInput>(input.clone()) {
let kind = match &parsed.data {
syn::Data::Struct(_) => RecordKind::Struct,
syn::Data::Enum(_) => RecordKind::Enum,
syn::Data::Union(_) => RecordKind::Union,
};
let tokens = quote! { #parsed };
(
kind,
parsed.ident.to_string(),
tokens.to_string(),
parsed.span(),
)
} else if let Ok(parsed) = syn::parse::<ItemFn>(input.clone()) {
let mut fn_sig = parsed.clone();
fn_sig.block = syn::parse_quote! {{ }};
let tokens = quote! { #fn_sig };
(
RecordKind::Function,
parsed.sig.ident.to_string(),
tokens.to_string(),
parsed.sig.span(),
)
} else if let Ok(parsed) = syn::parse::<ItemType>(input.clone()) {
let tokens = quote! { #parsed };
(
RecordKind::TypeAlias,
parsed.ident.to_string(),
tokens.to_string(),
parsed.ident.span(),
)
} else if let Ok(parsed) = syn::parse::<ItemConst>(input.clone()) {
let tokens = quote! { #parsed };
(
RecordKind::Const,
parsed.ident.to_string(),
tokens.to_string(),
parsed.ident.span(),
)
} else {
let item = syn::parse::<syn::Item>(input.clone()).ok();
return unsupported_item_error(item);
};
let source_location = SourceLocation::from_span(&span);
let new_record = Record::new(
kind,
name,
content,
source_location,
parsed_args.cfg.clone(),
);
let file_path = get_prebindgen_jsonl_path(&group);
if prebindgen::utils::write_to_jsonl_file(&file_path, &[&new_record]).is_err() {
return TokenStream::from(quote! {
compile_error!("Failed to write prebindgen record");
});
}
if let Some(cfg_value) = &parsed_args.cfg {
let cfg_tokens: proc_macro2::TokenStream = cfg_value
.parse()
.unwrap_or_else(|_| panic!("Invalid cfg condition: {}", cfg_value));
let cfg_attr = quote! { #[cfg(#cfg_tokens)] };
let original_tokens: proc_macro2::TokenStream = input_clone.into();
let result = quote! {
#cfg_attr
#original_tokens
};
result.into()
} else {
input_clone
}
}
#[proc_macro]
pub fn prebindgen_out_dir(_input: TokenStream) -> TokenStream {
let out_dir = std::env::var("OUT_DIR")
.expect("OUT_DIR environment variable not set. Please ensure you have a build.rs file in your project.");
let file_path = std::path::Path::new(&out_dir).join("prebindgen");
let path_str = file_path.to_string_lossy();
let expanded = quote! {
#path_str
};
TokenStream::from(expanded)
}
#[proc_macro]
pub fn features(_input: TokenStream) -> TokenStream {
let features = std::env::var("PREBINDGEN_FEATURES").expect(
"PREBINDGEN_FEATURES environment variable not set. Ensure prebindgen::init_prebindgen_out_dir() is called in build.rs",
);
let lit = syn::LitStr::new(&features, proc_macro2::Span::call_site());
TokenStream::from(quote! { #lit })
}