Skip to main content

wasm_split_macros/
lib.rs

1use proc_macro::{Span, TokenStream};
2
3use quote::{format_ident, quote, quote_spanned};
4use sha2::Digest;
5use syn::ext::IdentExt;
6use syn::parse::{Parse, ParseStream};
7use syn::{parenthesized, parse_quote, Attribute, Block, Pat, Path, ReturnType};
8use syn::{parse_macro_input, spanned::Spanned, Ident, ItemFn, LitStr, Signature, Token};
9
10mod magic_constants;
11use magic_constants::PLACEHOLDER_IMPORT_MODULE;
12
13struct ReturnWrapper {
14    pattern: Pat,
15    output: ReturnType,
16    postlude: Block,
17}
18
19struct PreloadDefinition {
20    attrs: Vec<Attribute>,
21    name: Ident,
22}
23
24struct Args {
25    module_ident: Ident,
26    link_name: Option<(Ident, LitStr)>,
27    wasm_split_path: Option<Path>,
28    return_wrapper: Option<ReturnWrapper>,
29    preload_def: Option<PreloadDefinition>,
30}
31
32impl Parse for Args {
33    fn parse(input: ParseStream) -> syn::Result<Self> {
34        let module_ident = input.call(Ident::parse_any)?;
35        let mut link_name = None;
36        let mut wasm_split_path = None;
37        let mut return_wrapper = None;
38        let mut preload_def = None;
39        while !input.is_empty() {
40            let _: Token![,] = input.parse()?;
41            if input.is_empty() {
42                break;
43            }
44            let option: Ident = input.call(Ident::parse_any)?;
45            match () {
46                _ if option == "wasm_import_module" => {
47                    let _: Token![=] = input.parse()?;
48                    link_name = Some((option, input.parse()?));
49                }
50                _ if option == "wasm_split_path" => {
51                    let _: Token![=] = input.parse()?;
52                    wasm_split_path = Some(input.parse()?);
53                }
54                _ if option == "return_wrapper" => {
55                    let wrap_spec;
56                    let _parens = parenthesized!(wrap_spec in input);
57                    let _: Token![let] = wrap_spec.parse()?;
58                    let pattern = Pat::parse_multi_with_leading_vert(&wrap_spec)?;
59                    let _: Token![=] = wrap_spec.parse()?;
60                    let _: Token![_] = wrap_spec.parse()?;
61                    let _: Token![;] = wrap_spec.parse()?;
62                    return_wrapper = Some(ReturnWrapper {
63                        pattern,
64                        postlude: wrap_spec.parse()?,
65                        output: wrap_spec.parse()?,
66                    });
67                }
68                _ if option == "preload" => {
69                    let wrap_spec;
70                    let _parens = parenthesized!(wrap_spec in input);
71                    let attrs = wrap_spec.call(Attribute::parse_outer)?;
72                    let name = wrap_spec.parse()?;
73                    preload_def = Some(PreloadDefinition { attrs, name });
74                }
75                _ => {
76                    return Err(syn::Error::new(
77                        option.span(),
78                        "No such option for the `split` macro.",
79                    ))
80                }
81            }
82        }
83        Ok(Self {
84            module_ident,
85            link_name,
86            wasm_split_path,
87            return_wrapper,
88            preload_def,
89        })
90    }
91}
92
93/// Indicate a function as a split point.
94///
95/// The macro emits a function with the same signature, except that it is `async`. Calls to this function will first load
96/// the module into which the input function was split into before forwarding the arguments and the result. On non-`wasm`
97/// targets, the function will be called directly.
98///
99/// The annotated function must fulfill the requirements a typical `extern` declared function must fufill:
100/// - It can not be `async`. If you want to support this, you must `Box` or otherwise wrap the `Future` into a `dyn` object.
101///   Also see the `return_wrapper` option for some further hints.
102/// - It can not be `const`.
103/// - It can not make use of a receiver argument, generics or an `impl` return type.
104/// - The only extern linkage on wasm is `#[wasm_import_module]` which implies `extern "C"`, see [this blog post]. The macro
105///   allows you to specify a different linkage but that is only used for the generated wrapper function. The forward call
106///   will happen with `"C"` ABI.
107///   This means in particular that panicking is forbidden across the call. As of now, a panic on wasm leads to an abort, and
108///   this doc serves only as a warning.
109///
110/// ## Syntax
111///
112/// ```text
113/// wasm_split($module:ident (, $option ),* ) => { ... };
114/// ```
115///
116/// All functions with the same specified `$module` end up in one split off WASM chunk.
117///
118/// The following options are supported:
119/// - `wasm_split_path = $this:path` changes the path at which the runtime support crate is expected.
120///   As a framework, you might want to reexport this from some hidden module path.
121///   Default: `::wasm_split_helpers`.
122/// - `return_wrapper( let $bindings:pat = _ ; $compute:block -> $ret:ty )`. A rather low-level option to support
123///   rewriting the result of the wrapped function. The generated wrapper will, rather than directly return the result
124///   from the user-given function, bind this to `$bindings` and emit the statements in `$compute` to generate the
125///   return value of the wrapper with the return type indicated by `$ret`.
126///
127///   Example use case: `return_wrapper( let future = _ ; { future.await } -> Output)` to `await` a future directly in
128///   the wrapper.
129/// - `preload( $( #[$attr] )* $preload_name:ident )` generates an additional preload function `$preload_name` with the
130///   signature `async fn()` which can be used to fetch the module in which the wrapped function is contained without
131///   calling it.
132///
133/// [this blog post]: https://blog.rust-lang.org/2026/04/04/changes-to-webassembly-targets-and-handling-undefined-symbols/#what-is-going-to-break-and-how-to-fix
134#[proc_macro_attribute]
135pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
136    let Args {
137        module_ident,
138        link_name,
139        wasm_split_path,
140        return_wrapper,
141        preload_def,
142    } = parse_macro_input!(args as Args);
143    let (deprecated_link_opt, link_name) = if let Some((option, link_name)) = link_name {
144        (Some(option), link_name)
145    } else {
146        (
147            None,
148            LitStr::new(PLACEHOLDER_IMPORT_MODULE, Span::call_site().into()),
149        )
150    };
151    let wasm_split_path = wasm_split_path.unwrap_or(parse_quote!(::wasm_split_helpers));
152
153    let mut item_fn: ItemFn = parse_macro_input!(input as ItemFn);
154    let mut declared_abi = item_fn.sig.abi.take();
155    declared_abi.get_or_insert(parse_quote!( extern "Rust" ));
156    let declared_async = item_fn.sig.asyncness.take();
157
158    let mut wrapper_sig = Signature {
159        asyncness: Some(Default::default()),
160        ..item_fn.sig.clone()
161    };
162
163    if let Some(not_sync) = declared_async {
164        return quote_spanned! {not_sync.span()=>
165            ::core::compile_error!("Split functions can not be `async`");
166
167            #wrapper_sig {
168                ::core::todo!()
169            }
170        }
171        .into();
172    }
173
174    let name = &item_fn.sig.ident;
175    let PreloadDefinition {
176        attrs: preload_attrs,
177        name: preload_name,
178    } = preload_def.unwrap_or_else(|| PreloadDefinition {
179        attrs: vec![
180            parse_quote!(#[automatically_derived]),
181            parse_quote!(#[doc(hidden)]),
182        ],
183        name: format_ident!("__wasm_split_preload_{name}"),
184    });
185    let vis = item_fn.vis;
186
187    let unique_identifier = base16::encode_lower(
188        &sha2::Sha256::digest(format!("{name} {span:?}", span = name.span()))[..16],
189    );
190
191    let load_module_ident = format_ident!("__wasm_split_load_{module_ident}");
192    let impl_import_ident =
193        format_ident!("__wasm_split_00{module_ident}00_import_{unique_identifier}_{name}");
194    let impl_export_ident =
195        format_ident!("__wasm_split_00{module_ident}00_export_{unique_identifier}_{name}");
196
197    let export_sig = Signature {
198        abi: declared_abi.clone(),
199        ident: impl_export_ident.clone(),
200        ..item_fn.sig.clone()
201    };
202
203    // On WASM targets, we must use extern "C" for the import/export pair.
204    // #[link(wasm_import_module)] only creates proper WASM imports for
205    // non-Rust ABIs. Previously this worked because rustc passed
206    // --allow-undefined to wasm-ld by default, but rust-lang/rust#149868
207    // removed that. Using extern "C" ensures the import is a real WASM
208    // import and the export has a matching ABI for wasm-split to link.
209    let wasm_export_sig = Signature {
210        abi: parse_quote!(extern "C"),
211        ident: impl_export_ident.clone(),
212        ..item_fn.sig.clone()
213    };
214
215    let mut args = Vec::new();
216    for (i, param) in wrapper_sig.inputs.iter_mut().enumerate() {
217        match param {
218            syn::FnArg::Typed(pat_type) => {
219                let param_ident = format_ident!("__wasm_split_arg_{i}");
220                args.push(param_ident.clone());
221                *pat_type.pat = syn::Pat::Ident(syn::PatIdent {
222                    attrs: vec![],
223                    by_ref: None,
224                    mutability: None,
225                    ident: param_ident,
226                    subpat: None,
227                });
228            }
229            // receiver arguments can not used in `extern` functions (and we can't name the `Self` type in the arguments to work arount that)
230            syn::FnArg::Receiver(_) => {
231                return quote_spanned! {param.span()=>
232                    ::core::compile_error!("Split functions can not have a receiver argument");
233
234                    #wrapper_sig {
235                        ::core::todo!()
236                    }
237                }
238                .into();
239            }
240        }
241    }
242    let import_sig = Signature {
243        //abi: declared_abi.clone(), // already in an extern block
244        asyncness: None,
245        ident: impl_import_ident.clone(),
246        ..wrapper_sig.clone()
247    };
248
249    let attrs = item_fn.attrs;
250    let stmts = &item_fn.block.stmts;
251
252    let mut compute_result = quote! {
253        #[cfg(target_family = "wasm")]
254        use #impl_import_ident as callee;
255        #[cfg(not(target_family = "wasm"))]
256        use #impl_export_ident as callee;
257        callee( #(#args),* )
258    };
259
260    if let Some(ReturnWrapper {
261        output,
262        pattern: output_pat,
263        postlude,
264    }) = return_wrapper
265    {
266        wrapper_sig.output = output;
267        let postlude = postlude.stmts;
268        compute_result = quote! {{
269            let #output_pat = { #compute_result };
270            #( #postlude )*
271        }};
272    }
273
274    let mut extra_code = quote! {};
275    if let Some(deprecated_opt) = deprecated_link_opt {
276        let deprecation_note = format!("The `{deprecated_opt}` option should not be used, since the wasm_split_cli fixes the import path with improved target knowledge.");
277        extra_code.extend(quote! {
278            const _: () = {
279                #[allow(nonstandard_style)]
280                #[deprecated(note = #deprecation_note)]
281                const #deprecated_opt: () = ();
282                let _ = #deprecated_opt;
283            };
284        });
285    }
286
287    quote! {
288        // This could have weak linkage to unify all mentions of the same module
289        #( #preload_attrs )*
290        #vis async fn #preload_name () {
291            #[cfg(target_family = "wasm")]
292            #[link(wasm_import_module = #link_name)]
293            unsafe extern "C" {
294                #[unsafe(no_mangle)]
295                fn #load_module_ident (callback: #wasm_split_path::rt::LoadCallbackFn, data: *const ::std::ffi::c_void) -> ();
296            }
297            #[cfg(target_family = "wasm")]
298            {
299                #wasm_split_path::rt::ensure_loaded(::core::pin::Pin::static_ref({
300                    // SAFETY: the imported c function correctly implements the callback
301                    static LOADER: #wasm_split_path::rt::LazySplitLoader = unsafe { #wasm_split_path::rt::LazySplitLoader::new(#load_module_ident) };
302                    &LOADER
303                })).await;
304            }
305        }
306        #(#attrs)*
307        #vis #wrapper_sig {
308            // On WASM, use extern "C" so #[link(wasm_import_module)] creates
309            // a real WASM import (it is ignored on extern "Rust" blocks).
310            #[cfg(target_family = "wasm")]
311            #[link(wasm_import_module = #link_name)]
312            #[allow(improper_ctypes)]
313            unsafe extern "C" {
314                // We rewrite calls to this function instead of actually calling it. We just need to link to it. The name is unique by hashing.
315                #[unsafe(no_mangle)]
316                safe #import_sig;
317            }
318
319            // On WASM, the export must use extern "C" to match the import ABI.
320            #[cfg(target_family = "wasm")]
321            #(#attrs)*
322            #[allow(improper_ctypes_definitions)]
323            #[unsafe(no_mangle)]
324            #wasm_export_sig {
325                #(#stmts)*
326            }
327
328            // On non-WASM targets, use the declared ABI (no import needed).
329            #[cfg(not(target_family = "wasm"))]
330            #(#attrs)*
331            #export_sig {
332                #(#stmts)*
333            }
334
335            #preload_name ().await;
336            #compute_result
337        }
338        #extra_code
339    }
340    .into()
341}
342
343/// Generates a unique name for the invoking crate, as a string literal.
344///
345/// This is used as a work-around to guarantee no collisions with `unsafe(no_mangle)`.
346#[doc(hidden)]
347#[proc_macro]
348pub fn version_stamp(_args: TokenStream) -> TokenStream {
349    let unique_path = std::env::var_os("CARGO_MANIFEST_PATH").unwrap();
350    let unique_id = base16::encode_lower(&sha2::Sha256::digest(unique_path.as_encoded_bytes()));
351    let id = format!("_WASM_SPLIT_MARKER_{}", &unique_id[0..16]);
352    let id = syn::LitStr::new(&id, Span::call_site().into());
353    quote! { #id }.into()
354}