Skip to main content

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}