Skip to main content

lumen_capability_macros/
lib.rs

1//! The expansion behind `lumen_capability!`. Reach it through
2//! `lumen-capability`, which re-exports it; this crate is not depended on
3//! directly.
4//!
5//! # What it emits
6//!
7//! - `lumen_capability_register_<n>`, an `extern "C"` function that puts the
8//!   capability on the registry, where `<n>` is the declared name with every
9//!   character a symbol cannot carry replaced by `_`. The entry records the
10//!   package the macro expanded in, so a kit can tell which of its files
11//!   carries the capability.
12//! - A `#[used]` pointer to that function in the platform's pre-main
13//!   constructor section (`.init_array`, `__DATA,__mod_init_func`,
14//!   `.CRT$XCU`), so a binary the capability is linked into registers it
15//!   before `main` runs.
16//!
17//! The register symbol is the linker's handle on the capability. A link that
18//! names it (`-u` on ELF and Mach-O, `/INCLUDE:` on Windows) pulls the
19//! object that defines it out of its archive; a link that does not, and that
20//! leaves rustc's `symbols.o` off the line, links nothing of the capability
21//! at all. `core/capability` spells the same name from the declared string
22//! at runtime, which is why the macro takes the name rather than reading the
23//! package's.
24
25use proc_macro::TokenStream;
26use proc_macro2::{Ident, Span};
27use quote::quote;
28use syn::parse::{Parse, ParseStream};
29use syn::{Expr, LitStr, Token, parse_macro_input};
30
31/// `lumen_capability!("<name>", <phase>, <install>)`, with any of
32/// `preflight = <fn>` and `select = <Select>` after the install entry.
33struct CapabilityEntry {
34    name: LitStr,
35    phase: Expr,
36    install: Expr,
37    preflight: Option<Expr>,
38    select: Option<Expr>,
39}
40
41impl Parse for CapabilityEntry {
42    fn parse(input: ParseStream) -> syn::Result<Self> {
43        let name: LitStr = input.parse().map_err(|e| {
44            syn::Error::new(
45                e.span(),
46                "lumen_capability! takes the capability's name first, as a string literal: \
47                 lumen_capability!(\"os-tray\", Phase::Platform, install)",
48            )
49        })?;
50        input.parse::<Token![,]>()?;
51        let phase: Expr = input.parse()?;
52        input.parse::<Token![,]>()?;
53        let install: Expr = input.parse()?;
54        let mut preflight = None;
55        let mut select = None;
56        while input.peek(Token![,]) {
57            input.parse::<Token![,]>()?;
58            if input.is_empty() {
59                break;
60            }
61            let key: Ident = input.parse()?;
62            input.parse::<Token![=]>()?;
63            let slot = match key.to_string().as_str() {
64                "preflight" => &mut preflight,
65                "select" => &mut select,
66                _ => {
67                    return Err(syn::Error::new(
68                        key.span(),
69                        "the options after the install entry are `preflight = <fn>` and \
70                         `select = <Select>`",
71                    ));
72                }
73            };
74            if slot.is_some() {
75                return Err(syn::Error::new(
76                    key.span(),
77                    format!("`{key}` is given twice"),
78                ));
79            }
80            *slot = Some(input.parse()?);
81        }
82        Ok(CapabilityEntry {
83            name,
84            phase,
85            install,
86            preflight,
87            select,
88        })
89    }
90}
91
92/// Declare an optional runtime subsystem.
93///
94/// See the `lumen-capability` crate docs for the authoring shape; see this
95/// crate's docs for what the expansion contains.
96#[proc_macro]
97pub fn lumen_capability(input: TokenStream) -> TokenStream {
98    let CapabilityEntry {
99        name,
100        phase,
101        install,
102        preflight,
103        select,
104    } = parse_macro_input!(input as CapabilityEntry);
105
106    let declared = name.value();
107    if declared.trim().is_empty() {
108        return syn::Error::new(name.span(), "the capability's name must not be empty")
109            .to_compile_error()
110            .into();
111    }
112    let suffix = symbol_suffix(&declared);
113    let register_name = format!("lumen_capability_register_{suffix}");
114    let register_fn = Ident::new(&format!("register_{suffix}"), Span::call_site());
115    let preflight = match preflight {
116        Some(f) => quote! { ::core::option::Option::Some(#f) },
117        None => quote! { ::core::option::Option::None },
118    };
119    let select = match select {
120        Some(s) => quote! { #s },
121        None => quote! { ::lumen_capability::Select::Always },
122    };
123
124    quote! {
125        const _: () = {
126            #[unsafe(export_name = #register_name)]
127            extern "C" fn #register_fn() {
128                ::lumen_capability::register(::lumen_capability::Capability {
129                    name: #name,
130                    phase: #phase,
131                    install: #install,
132                    preflight: #preflight,
133                    select: #select,
134                    // The package this expands in, which is the object the
135                    // register symbol pulls.
136                    crate_name: env!("CARGO_PKG_NAME"),
137                });
138            }
139
140            // The pre-main constructor. `#[used]` keeps the pointer in the
141            // object file; the section is what the platform's startup code
142            // walks. A link that wants this capability names the register
143            // symbol above, which is what pulls this object in.
144            #[used]
145            #[cfg_attr(
146                all(unix, not(target_vendor = "apple")),
147                unsafe(link_section = ".init_array")
148            )]
149            #[cfg_attr(
150                target_vendor = "apple",
151                unsafe(link_section = "__DATA,__mod_init_func")
152            )]
153            #[cfg_attr(windows, unsafe(link_section = ".CRT$XCU"))]
154            static CTOR: extern "C" fn() = #register_fn;
155        };
156    }
157    .into()
158}
159
160/// The declared name as a symbol suffix. Kept in step with
161/// `lumen_capability::register_symbol`, which builds the same name from the
162/// string an app or a link kit declares.
163fn symbol_suffix(name: &str) -> String {
164    name.chars()
165        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
166        .collect()
167}
168
169#[cfg(test)]
170mod tests {
171    use super::symbol_suffix;
172
173    #[test]
174    fn a_hyphenated_name_underscores() {
175        assert_eq!(symbol_suffix("os-tray"), "os_tray");
176    }
177
178    #[test]
179    fn anything_a_symbol_cannot_carry_becomes_an_underscore() {
180        assert_eq!(symbol_suffix("http.fetch+2"), "http_fetch_2");
181    }
182}