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//! - `features!()` - Macro that returns the list of features enabled for the crate
9//!
10//! # Crate features
11//!
12//! - **`inline`** *(off by default)* — inject `#[inline]` onto every function
13//! marked with `#[prebindgen]` (types/consts are unaffected).
14//!
15//! prebindgen wrappers are usually thin shims that forward to a native API. A
16//! non-generic `pub fn` in one crate is **not** inlined into a Rust caller in
17//! another crate unless the function is `#[inline]` *or* the final binary is
18//! built with link-time optimization. Without inlining, every wrapper call
19//! costs an extra cross-crate call (measurable on hot paths — e.g. a per-message
20//! publish loop).
21//!
22//! Two ways to make the wrappers zero-cost:
23//! 1. Build the final artifact with **LTO** (`[profile.release] lto = "fat"`,
24//! `codegen-units = 1`). Cross-crate inlining then happens automatically and
25//! this feature is redundant. This is the recommended setup for an FFI
26//! `cdylib`/`staticlib` and matches how upstream zenoh builds its release
27//! profile.
28//! 2. Enable **`inline`** when you cannot rely on LTO — e.g. the wrapper crate is
29//! consumed as a normal Rust dependency by crates that build *without* LTO.
30//!
31//! It is opt-in because prebindgen can also wrap non-trivial functions where
32//! forcing `#[inline]` would only bloat the consumer; enable it only for genuine
33//! thin-wrapper libraries. The feature affects **only** the Rust function emitted
34//! into the wrapper crate — the recorded definition used for binding generation
35//! (and the resulting C ABI) is unchanged.
36//!
37//! See also: [`prebindgen`](https://docs.rs/prebindgen) for the main processing library.
38//!
39use std::{collections::HashMap, fs::OpenOptions};
40
41use prebindgen::{get_prebindgen_out_dir, Record, RecordKind, SourceLocation, DEFAULT_GROUP_NAME};
42use proc_macro::TokenStream;
43use quote::quote;
44use syn::{
45 parse::{Parse, ParseStream},
46 spanned::Spanned,
47 DeriveInput, Ident, ItemConst, ItemFn, ItemType, LitStr, Result, Token,
48};
49
50/// Helper function to generate consistent error messages for unsupported or unparseable items.
51fn unsupported_item_error(item: Option<syn::Item>) -> TokenStream {
52 match item {
53 Some(item) => {
54 let item_type = match &item {
55 syn::Item::Static(_) => "Static items",
56 syn::Item::Mod(_) => "Modules",
57 syn::Item::Trait(_) => "Traits",
58 syn::Item::Impl(_) => "Impl blocks",
59 syn::Item::Use(_) => "Use statements",
60 syn::Item::ExternCrate(_) => "Extern crate declarations",
61 syn::Item::Macro(_) => "Macro definitions",
62 syn::Item::Verbatim(_) => "Verbatim items",
63 _ => "This item type",
64 };
65
66 syn::Error::new_spanned(
67 item,
68 format!("{item_type} are not supported by #[prebindgen]"),
69 )
70 .to_compile_error()
71 .into()
72 }
73 None => {
74 // If we can't even parse it as an Item, return a generic error
75 syn::Error::new(
76 proc_macro2::Span::call_site(),
77 "Invalid syntax for #[prebindgen]",
78 )
79 .to_compile_error()
80 .into()
81 }
82 }
83}
84
85/// Arguments for the prebindgen macro
86struct PrebindgenArgs {
87 group: String,
88 cfg: Option<String>,
89}
90
91impl Parse for PrebindgenArgs {
92 fn parse(input: ParseStream) -> Result<Self> {
93 let mut group = DEFAULT_GROUP_NAME.to_string();
94 let mut cfg = None;
95
96 if input.is_empty() {
97 return Ok(PrebindgenArgs { group, cfg });
98 }
99
100 // Parse arguments in any order
101 while !input.is_empty() {
102 if input.peek(LitStr) {
103 // String literal - could be group name
104 let lit: LitStr = input.parse()?;
105 group = lit.value();
106 } else if input.peek(Ident) {
107 let ident: Ident = input.parse()?;
108 input.parse::<Token![=]>()?;
109
110 match ident.to_string().as_str() {
111 "cfg" => {
112 let cfg_lit: LitStr = input.parse()?;
113 cfg = Some(cfg_lit.value());
114 }
115 _ => {
116 return Err(syn::Error::new_spanned(ident, "Expected 'cfg'"));
117 }
118 }
119 } else {
120 return Err(syn::Error::new(input.span(), "Invalid argument format"));
121 }
122
123 // Parse optional comma
124 if input.peek(Token![,]) {
125 input.parse::<Token![,]>()?;
126 } else if !input.is_empty() {
127 return Err(syn::Error::new(
128 input.span(),
129 "Expected comma between arguments",
130 ));
131 }
132 }
133
134 Ok(PrebindgenArgs { group, cfg })
135 }
136}
137
138thread_local! {
139 static THREAD_ID: std::cell::RefCell<Option<u64>> = const { std::cell::RefCell::new(None) };
140 static JSONL_PATHS: std::cell::RefCell<HashMap<String, std::path::PathBuf>> = std::cell::RefCell::new(HashMap::new());
141}
142
143/// Get the full path to `{group}_{pid}_{thread_id}.jsonl` generated in OUT_DIR.
144fn get_prebindgen_jsonl_path(group: &str) -> std::path::PathBuf {
145 if let Some(p) = JSONL_PATHS.with(|path| path.borrow().get(group).cloned()) {
146 return p;
147 }
148 let process_id = std::process::id();
149 let thread_id = if let Some(in_thread_id) = THREAD_ID.with(|id| *id.borrow()) {
150 in_thread_id
151 } else {
152 let new_id = rand::random::<u64>();
153 THREAD_ID.with(|id| *id.borrow_mut() = Some(new_id));
154 new_id
155 };
156 let mut random_value = None;
157 // Try to really create file and repeat until success
158 // to avoid collisions in extremely rare case when two threads got
159 // the same random value
160 let new_path = loop {
161 let postfix = if let Some(rv) = random_value {
162 format!("_{rv}")
163 } else {
164 "".to_string()
165 };
166 let path = get_prebindgen_out_dir()
167 .join(format!("{group}_{process_id}_{thread_id}{postfix}.jsonl"));
168 if OpenOptions::new()
169 .create_new(true)
170 .write(true)
171 .open(&path)
172 .is_ok()
173 {
174 break path;
175 }
176 random_value = Some(rand::random::<u32>());
177 };
178 JSONL_PATHS.with(|path| {
179 path.borrow_mut()
180 .insert(group.to_string(), new_path.clone());
181 });
182 new_path
183}
184
185/// Attribute macro that exports FFI definitions for use in language-specific binding crates.
186///
187/// All types and functions marked with this attribute can be made available in dependent
188/// crates as Rust source code for both binding generator processing (cbindgen, csbindgen, etc.)
189/// and for including into projects to make the compiler generate `#[no_mangle]` FFI exports
190/// for cdylib/staticlib targets.
191///
192/// # Usage
193///
194/// ```rust
195/// # use prebindgen_proc_macro::prebindgen;
196/// // Use with explicit group name
197/// #[prebindgen("group_name")]
198/// #[repr(C)]
199/// pub struct Point {
200/// pub x: f64,
201/// pub y: f64,
202/// }
203///
204/// // Use with default group name "default"
205/// #[prebindgen]
206/// pub fn calculate_distance(p1: &Point, p2: &Point) -> f64 {
207/// ((p2.x - p1.x).powi(2) + (p2.y - p1.y).powi(2)).sqrt()
208/// }
209///
210/// // Add cfg attribute to generated code
211/// #[prebindgen(cfg = "feature = \"experimental\"")]
212/// pub fn experimental_function() -> i32 {
213/// 42
214/// }
215///
216/// // Combine group name with cfg
217/// #[prebindgen("functions", cfg = "unix")]
218/// pub fn another_function() -> i32 {
219/// 42
220/// }
221/// ```
222///
223/// # Requirements
224///
225/// - Must call `prebindgen::init_prebindgen_out_dir()` in your crate's `build.rs`
226/// - Optionally takes a string literal group name for organization (defaults to "default")
227/// - Optionally takes `cfg = "condition"` to add `#[cfg(condition)]` to generated code
228///
229/// # The `inline` feature
230///
231/// With the crate `inline` feature enabled, this macro prepends `#[inline]` to the
232/// emitted function so thin wrappers stay zero-cost for Rust consumers that do not
233/// build with LTO. Off by default; see the crate-level documentation.
234#[proc_macro_attribute]
235pub fn prebindgen(args: TokenStream, input: TokenStream) -> TokenStream {
236 let input_clone = input.clone();
237
238 // Parse arguments
239 let parsed_args = syn::parse::<PrebindgenArgs>(args).expect("Invalid #[prebindgen] arguments");
240
241 let group = parsed_args.group;
242
243 // Try to parse as different item types
244 let (kind, name, content, span) = if let Ok(parsed) = syn::parse::<DeriveInput>(input.clone()) {
245 // Handle struct, enum, union
246 let kind = match &parsed.data {
247 syn::Data::Struct(_) => RecordKind::Struct,
248 syn::Data::Enum(_) => RecordKind::Enum,
249 syn::Data::Union(_) => RecordKind::Union,
250 };
251 let tokens = quote! { #parsed };
252 (
253 kind,
254 parsed.ident.to_string(),
255 tokens.to_string(),
256 parsed.span(),
257 )
258 } else if let Ok(parsed) = syn::parse::<ItemFn>(input.clone()) {
259 // Handle function
260 // For functions, we want to store only the signature without the body
261 let mut fn_sig = parsed.clone();
262 fn_sig.block = syn::parse_quote! {{ /* placeholder */ }};
263 let tokens = quote! { #fn_sig };
264 (
265 RecordKind::Function,
266 parsed.sig.ident.to_string(),
267 tokens.to_string(),
268 parsed.sig.span(),
269 )
270 } else if let Ok(parsed) = syn::parse::<ItemType>(input.clone()) {
271 // Handle type alias
272 let tokens = quote! { #parsed };
273 (
274 RecordKind::TypeAlias,
275 parsed.ident.to_string(),
276 tokens.to_string(),
277 parsed.ident.span(),
278 )
279 } else if let Ok(parsed) = syn::parse::<ItemConst>(input.clone()) {
280 // Handle constant
281 let tokens = quote! { #parsed };
282 (
283 RecordKind::Const,
284 parsed.ident.to_string(),
285 tokens.to_string(),
286 parsed.ident.span(),
287 )
288 } else {
289 // Try to parse as any item to provide better error messages
290 let item = syn::parse::<syn::Item>(input.clone()).ok();
291 return unsupported_item_error(item);
292 };
293
294 // The `inline` feature adds `#[inline]` to function wrappers only (not to
295 // structs/enums/types/consts). Captured here before `kind` is moved below.
296 let is_function = matches!(kind, RecordKind::Function);
297
298 // Extract basic source location information available during compilation
299 let source_location = SourceLocation::from_span(&span);
300
301 // Create the new record
302 let new_record = Record::new(
303 kind,
304 name,
305 content,
306 source_location,
307 parsed_args.cfg.clone(),
308 );
309
310 // Get the full path to the JSONL file
311 let file_path = get_prebindgen_jsonl_path(&group);
312 if prebindgen::utils::write_to_jsonl_file(&file_path, &[&new_record]).is_err() {
313 return TokenStream::from(quote! {
314 compile_error!("Failed to write prebindgen record");
315 });
316 }
317
318 // Re-emit the original item, optionally prepending `#[cfg(...)]` (from the
319 // macro argument) and `#[inline]` (when the `inline` feature is on, functions
320 // only). When neither applies, the original tokens are returned unchanged.
321 let add_inline = cfg!(feature = "inline") && is_function;
322 if parsed_args.cfg.is_none() && !add_inline {
323 return input_clone;
324 }
325 let inline_attr = if add_inline {
326 quote! { #[inline] }
327 } else {
328 quote! {}
329 };
330 let cfg_attr = if let Some(cfg_value) = &parsed_args.cfg {
331 let cfg_tokens: proc_macro2::TokenStream = cfg_value
332 .parse()
333 .unwrap_or_else(|_| panic!("Invalid cfg condition: {}", cfg_value));
334 quote! { #[cfg(#cfg_tokens)] }
335 } else {
336 quote! {}
337 };
338 let original_tokens: proc_macro2::TokenStream = input_clone.into();
339 quote! {
340 #cfg_attr
341 #inline_attr
342 #original_tokens
343 }
344 .into()
345}
346
347/// Proc macro that returns the prebindgen output directory path as a string literal.
348///
349/// This macro generates a string literal containing the full path to the prebindgen
350/// output directory. It should be used to create a public constant that can be
351/// consumed by language-specific binding crates.
352///
353/// # Panics
354///
355/// Panics if OUT_DIR environment variable is not set. This indicates that the macro
356/// is being used outside of a build.rs context.
357///
358/// # Returns
359///
360/// A string literal with the path to the prebindgen output directory.
361///
362/// # Example
363///
364/// ```rust
365/// use prebindgen_proc_macro::prebindgen_out_dir;
366///
367/// // Create a public constant for use by binding crates
368/// pub const PREBINDGEN_OUT_DIR: &str = prebindgen_out_dir!();
369/// ```
370#[proc_macro]
371pub fn prebindgen_out_dir(_input: TokenStream) -> TokenStream {
372 let out_dir = std::env::var("OUT_DIR")
373 .expect("OUT_DIR environment variable not set. Please ensure you have a build.rs file in your project.");
374 let file_path = std::path::Path::new(&out_dir).join("prebindgen");
375 let path_str = file_path.to_string_lossy();
376
377 let expanded = quote! {
378 #path_str
379 };
380
381 TokenStream::from(expanded)
382}
383
384/// Proc macro that returns the enabled features, joined by commas, as a string literal.
385///
386/// The value is sourced from the `PREBINDGEN_FEATURES` compile-time environment variable,
387/// which is set by calling `prebindgen::init_prebindgen_out_dir()` in your crate's `build.rs`.
388///
389/// # Panics
390///
391/// Emits a compile-time error if `PREBINDGEN_FEATURES` is not set, which typically means
392/// `prebindgen::init_prebindgen_out_dir()` wasn't called in `build.rs`.
393///
394/// # Returns
395///
396/// A string literal containing the comma-separated list of enabled features.
397/// The string may be empty if no features are enabled.
398///
399/// # Example
400///
401/// ```rust
402/// use prebindgen_proc_macro::features;
403///
404/// pub const ENABLED_FEATURES: &str = features!();
405/// ```
406#[proc_macro]
407pub fn features(_input: TokenStream) -> TokenStream {
408 let features = std::env::var("PREBINDGEN_FEATURES").expect(
409 "PREBINDGEN_FEATURES environment variable not set. Ensure prebindgen::init_prebindgen_out_dir() is called in build.rs",
410 );
411 let lit = syn::LitStr::new(&features, proc_macro2::Span::call_site());
412 TokenStream::from(quote! { #lit })
413}
414
415/// Proc macro that returns the **source crate's** manifest directory as a string
416/// literal (its `CARGO_MANIFEST_DIR` at compile time).
417///
418/// Exposing this lets a downstream binding crate locate the marked source crate
419/// *wherever it lives* (a path/git/registry dependency) without guessing layout —
420/// e.g. to compile a size/alignment probe against it. This complements
421/// [`prebindgen_out_dir!`](macro@prebindgen_out_dir) and [`features!`](macro@features).
422///
423/// # Returns
424///
425/// A string literal with the absolute path to the source crate's manifest dir.
426///
427/// # Example
428///
429/// ```rust
430/// use prebindgen_proc_macro::manifest_dir;
431///
432/// // Create a public constant for use by binding crates
433/// pub const MANIFEST_DIR: &str = manifest_dir!();
434/// ```
435#[proc_macro]
436pub fn manifest_dir(_input: TokenStream) -> TokenStream {
437 let dir = std::env::var("CARGO_MANIFEST_DIR")
438 .expect("CARGO_MANIFEST_DIR environment variable not set");
439 let lit = syn::LitStr::new(&dir, proc_macro2::Span::call_site());
440 TokenStream::from(quote! { #lit })
441}