wasm_split_macros 0.2.2

Split a WASM module into lazily loadable chunks
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
use proc_macro::{Span, TokenStream};

use quote::{format_ident, quote, quote_spanned};
use sha2::Digest;
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream};
use syn::{parenthesized, parse_quote, Attribute, Block, Pat, Path, ReturnType};
use syn::{parse_macro_input, spanned::Spanned, Ident, ItemFn, LitStr, Signature, Token};

mod magic_constants;
use magic_constants::PLACEHOLDER_IMPORT_MODULE;

struct ReturnWrapper {
    pattern: Pat,
    output: ReturnType,
    postlude: Block,
}

struct PreloadDefinition {
    attrs: Vec<Attribute>,
    name: Ident,
}

struct Args {
    module_ident: Ident,
    link_name: Option<(Ident, LitStr)>,
    wasm_split_path: Option<Path>,
    return_wrapper: Option<ReturnWrapper>,
    preload_def: Option<PreloadDefinition>,
    fallible: bool,
}

impl Parse for Args {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let module_ident = input.call(Ident::parse_any)?;
        let mut link_name = None;
        let mut wasm_split_path = None;
        let mut return_wrapper = None;
        let mut preload_def = None;
        let mut fallible = false;
        while !input.is_empty() {
            let _: Token![,] = input.parse()?;
            if input.is_empty() {
                break;
            }
            let option: Ident = input.call(Ident::parse_any)?;
            match () {
                _ if option == "wasm_import_module" => {
                    let _: Token![=] = input.parse()?;
                    link_name = Some((option, input.parse()?));
                }
                _ if option == "wasm_split_path" => {
                    let _: Token![=] = input.parse()?;
                    wasm_split_path = Some(input.parse()?);
                }
                _ if option == "return_wrapper" => {
                    let wrap_spec;
                    let _parens = parenthesized!(wrap_spec in input);
                    let _: Token![let] = wrap_spec.parse()?;
                    let pattern = Pat::parse_multi_with_leading_vert(&wrap_spec)?;
                    let _: Token![=] = wrap_spec.parse()?;
                    let _: Token![_] = wrap_spec.parse()?;
                    let _: Token![;] = wrap_spec.parse()?;
                    return_wrapper = Some(ReturnWrapper {
                        pattern,
                        postlude: wrap_spec.parse()?,
                        output: wrap_spec.parse()?,
                    });
                }
                _ if option == "preload" => {
                    let wrap_spec;
                    let _parens = parenthesized!(wrap_spec in input);
                    let attrs = wrap_spec.call(Attribute::parse_outer)?;
                    let name = wrap_spec.parse()?;
                    preload_def = Some(PreloadDefinition { attrs, name });
                }
                _ if option == "fallible" => {
                    fallible = true;
                }
                _ => {
                    return Err(syn::Error::new(
                        option.span(),
                        "No such option for the `split` macro.",
                    ))
                }
            }
        }
        Ok(Self {
            module_ident,
            link_name,
            wasm_split_path,
            return_wrapper,
            preload_def,
            fallible,
        })
    }
}

/// Indicate a function as a split point.
///
/// The macro emits a function with the same signature, except that it is `async`. Calls to this function will first load
/// the module into which the annotated function was split into before forwarding the arguments and the result. On non-`wasm`
/// targets, the function will be called directly.
///
/// The annotated function must fulfill the requirements a typical `extern` declared function must fufill:
/// - It can not be `async`. If you want to support this, you must `Box` or otherwise wrap the `Future` into a `dyn` object.
///   Also see the `return_wrapper` option for some further hints.
/// - It can not be `const`.
/// - It can not make use of a receiver argument, generics or an `impl` return type.
/// - The only extern linkage on wasm is `#[wasm_import_module]` which implies `extern "C"`, see [this blog post]. The macro
///   allows you to specify a different linkage but that is only used for the generated wrapper function. The forward call
///   will happen with `"C"` ABI.
///   This means in particular that panicking is forbidden across the call. As of now, a panic on wasm leads to an abort, and
///   this doc serves only as a warning.
///
/// ## Syntax
///
/// ```text
/// wasm_split($module:ident (, $option ),* ) => { ... };
/// ```
///
/// All functions with the same specified `$module` end up in one split off WASM chunk.
///
/// The following options are supported:
/// - `wasm_split_path = $this:path` changes the path at which the runtime support crate is expected.
///   As a framework, you might want to reexport this from some hidden module path.
///   Default: `::wasm_split_helpers`.
/// - `return_wrapper( let $bindings:pat = _ ; $compute:block -> $ret:ty )`. A rather low-level option to support
///   rewriting the result of the wrapped function. The generated wrapper will, rather than directly return the result
///   from the user-given function, bind this to `$bindings` and emit the statements in `$compute` to generate the
///   return value of the wrapper with the return type indicated by `$ret`.
///
///   Example use case: `return_wrapper( let future = _ ; { future.await } -> Output)` to `await` a future directly in
///   the wrapper.
/// - `preload( $( #[$attr] )* $preload_name:ident )` generates an additional preload function `$preload_name` that
///   fetches the module in which the wrapped function is contained without calling it. Its signature is `async fn()`
///   and a hard load failure panics, matching the wrapper; with the `fallible` option it instead returns
///   `Result<(), $wasm_split_path::SplitLoaderError>`. See also [`SplitLoaderError`][].
/// - `fallible` surfaces a load failure as `Err` instead of panicking (a panic aborts the whole wasm module). The
///   annotated function must return `Result<_, E>` where `E: From<$wasm_split_path::SplitLoaderError>`; the macro
///   leaves that signature untouched and converts a load failure into your `E` via `?`, so a use-site can fold it
///   straight into a framework-specific error. The generated `preload` function (if any) returns
///   `Result<(), $wasm_split_path::SplitLoaderError>`. Without this option both keep their infallible signatures and
///   a hard load failure panics. The failure is not cached, so a later call retries from scratch. See also
///   [`SplitLoaderError`][].
///
/// [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
/// [`SplitLoaderError`]: https://docs.rs/wasm_split_helpers/0.2.2/wasm_split_helpers/struct.SplitLoaderError.html
#[proc_macro_attribute]
pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
    let Args {
        module_ident,
        link_name,
        wasm_split_path,
        return_wrapper,
        preload_def,
        fallible,
    } = parse_macro_input!(args as Args);
    let (deprecated_link_opt, link_name) = if let Some((option, link_name)) = link_name {
        (Some(option), link_name)
    } else {
        (
            None,
            LitStr::new(PLACEHOLDER_IMPORT_MODULE, Span::call_site().into()),
        )
    };
    let wasm_split_path = wasm_split_path.unwrap_or(parse_quote!(::wasm_split_helpers));

    let mut item_fn: ItemFn = parse_macro_input!(input as ItemFn);
    let mut declared_abi = item_fn.sig.abi.take();
    declared_abi.get_or_insert(parse_quote!( extern "Rust" ));
    let declared_async = item_fn.sig.asyncness.take();

    let mut wrapper_sig = Signature {
        asyncness: Some(Default::default()),
        ..item_fn.sig.clone()
    };

    if let Some(not_sync) = declared_async {
        return quote_spanned! {not_sync.span()=>
            ::core::compile_error!("Split functions can not be `async`");

            #wrapper_sig {
                ::core::todo!()
            }
        }
        .into();
    }

    let name = &item_fn.sig.ident;
    let PreloadDefinition {
        attrs: preload_attrs,
        name: preload_name,
    } = preload_def.unwrap_or_else(|| PreloadDefinition {
        attrs: vec![
            parse_quote!(#[automatically_derived]),
            parse_quote!(#[doc(hidden)]),
        ],
        name: format_ident!("__wasm_split_preload_{name}"),
    });
    let vis = item_fn.vis;

    let unique_identifier = base16::encode_lower(
        &sha2::Sha256::digest(format!("{name} {span:?}", span = name.span()))[..16],
    );

    let load_module_ident = format_ident!("__wasm_split_load_{module_ident}");
    let impl_import_ident =
        format_ident!("__wasm_split_00{module_ident}00_import_{unique_identifier}_{name}");
    let impl_export_ident =
        format_ident!("__wasm_split_00{module_ident}00_export_{unique_identifier}_{name}");

    let export_sig = Signature {
        abi: declared_abi.clone(),
        ident: impl_export_ident.clone(),
        ..item_fn.sig.clone()
    };

    // On WASM targets, we must use extern "C" for the import/export pair.
    // #[link(wasm_import_module)] only creates proper WASM imports for
    // non-Rust ABIs. Previously this worked because rustc passed
    // --allow-undefined to wasm-ld by default, but rust-lang/rust#149868
    // removed that. Using extern "C" ensures the import is a real WASM
    // import and the export has a matching ABI for wasm-split to link.
    let wasm_export_sig = Signature {
        abi: parse_quote!(extern "C"),
        ident: impl_export_ident.clone(),
        ..item_fn.sig.clone()
    };

    let mut args = Vec::new();
    for (i, param) in wrapper_sig.inputs.iter_mut().enumerate() {
        match param {
            syn::FnArg::Typed(pat_type) => {
                let param_ident = format_ident!("__wasm_split_arg_{i}");
                args.push(param_ident.clone());
                *pat_type.pat = syn::Pat::Ident(syn::PatIdent {
                    attrs: vec![],
                    by_ref: None,
                    mutability: None,
                    ident: param_ident,
                    subpat: None,
                });
            }
            // receiver arguments can not used in `extern` functions (and we can't name the `Self` type in the arguments to work arount that)
            syn::FnArg::Receiver(_) => {
                return quote_spanned! {param.span()=>
                    ::core::compile_error!("Split functions can not have a receiver argument");

                    #wrapper_sig {
                        ::core::todo!()
                    }
                }
                .into();
            }
        }
    }
    let import_sig = Signature {
        //abi: declared_abi.clone(), // already in an extern block
        asyncness: None,
        ident: impl_import_ident.clone(),
        ..wrapper_sig.clone()
    };

    let attrs = item_fn.attrs;
    let stmts = &item_fn.block.stmts;

    let mut compute_result = quote! {
        #[cfg(target_family = "wasm")]
        use #impl_import_ident as callee;
        #[cfg(not(target_family = "wasm"))]
        use #impl_export_ident as callee;
        callee( #(#args),* )
    };

    if let Some(ReturnWrapper {
        output,
        pattern: output_pat,
        postlude,
    }) = return_wrapper
    {
        wrapper_sig.output = output;
        let postlude = postlude.stmts;
        compute_result = quote! {{
            let #output_pat = { #compute_result };
            #( #postlude )*
        }};
    }

    let split_loader_error = quote!(#wasm_split_path::SplitLoaderError);
    // The load is awaited inside `preload`. With `fallible`, both the wrapper
    // and `preload` surface a load failure as `Result<_, SplitLoaderError>`;
    // without it they keep their infallible signatures and a hard failure
    // panics, exactly as before. Keeping `preload` infallible by default is
    // load-bearing: existing callers expect `async fn() -> ()` — e.g. leptos's
    // `LazyRoute::preload` is typed `Output = ()` — and changing that
    // unconditionally would break them.
    let ensure_loaded = quote! {
        #wasm_split_path::rt::ensure_loaded(::core::pin::Pin::static_ref({
            // SAFETY: the imported c function correctly implements the callback
            static LOADER: #wasm_split_path::rt::LazySplitLoader = unsafe { #wasm_split_path::rt::LazySplitLoader::new(#load_module_ident) };
            &LOADER
        })).await
    };
    let (preload_output, preload_load, preload_tail, load_and_compute) = if fallible {
        // `fallible` keeps the user's own signature: they must return
        // `Result<_, E>` where `E: From<SplitLoaderError>`. We do NOT synthesize
        // the return type. `#preload_name().await?` converts a load failure into
        // the user's `E` via `?`/`From`, so a use-site can fold it straight into
        // a framework error (e.g. leptos surfacing it to `<ErrorBoundary>`).
        (
            quote!(-> ::core::result::Result<(), #split_loader_error>),
            quote! { return #ensure_loaded; },
            // On non-wasm targets the function is called directly; nothing to load.
            quote! { #[allow(unreachable_code)] ::core::result::Result::Ok(()) },
            quote! {
                #preload_name().await?;
                #compute_result
            },
        )
    } else {
        (
            quote!(),
            quote! { #ensure_loaded.expect("load callback should succeed"); },
            quote!(),
            quote! {
                #preload_name().await;
                #compute_result
            },
        )
    };

    let mut extra_code = quote! {};
    if let Some(deprecated_opt) = deprecated_link_opt {
        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.");
        extra_code.extend(quote! {
            const _: () = {
                #[allow(nonstandard_style)]
                #[deprecated(note = #deprecation_note)]
                const #deprecated_opt: () = ();
                let _ = #deprecated_opt;
            };
        });
    }

    quote! {
        // This could have weak linkage to unify all mentions of the same module
        #( #preload_attrs )*
        #vis async fn #preload_name () #preload_output {
            #[cfg(target_family = "wasm")]
            #[link(wasm_import_module = #link_name)]
            unsafe extern "C" {
                #[unsafe(no_mangle)]
                fn #load_module_ident (callback: #wasm_split_path::rt::LoadCallbackFn, data: *const ::std::ffi::c_void) -> ();
            }
            #[cfg(target_family = "wasm")]
            {
                #preload_load
            }
            #preload_tail
        }
        #(#attrs)*
        #vis #wrapper_sig {
            // On WASM, use extern "C" so #[link(wasm_import_module)] creates
            // a real WASM import (it is ignored on extern "Rust" blocks).
            #[cfg(target_family = "wasm")]
            #[link(wasm_import_module = #link_name)]
            #[allow(improper_ctypes)]
            unsafe extern "C" {
                // We rewrite calls to this function instead of actually calling it. We just need to link to it. The name is unique by hashing.
                #[unsafe(no_mangle)]
                safe #import_sig;
            }

            // On WASM, the export must use extern "C" to match the import ABI.
            #[cfg(target_family = "wasm")]
            #(#attrs)*
            #[allow(improper_ctypes_definitions)]
            #[unsafe(no_mangle)]
            #wasm_export_sig {
                #(#stmts)*
            }

            // On non-WASM targets, use the declared ABI (no import needed).
            #[cfg(not(target_family = "wasm"))]
            #(#attrs)*
            #export_sig {
                #(#stmts)*
            }

            #load_and_compute
        }
        #extra_code
    }
    .into()
}

/// Generates a unique name for the invoking crate, as a string literal.
///
/// This is used as a work-around to guarantee no collisions with `unsafe(no_mangle)`.
#[doc(hidden)]
#[proc_macro]
pub fn version_stamp(_args: TokenStream) -> TokenStream {
    let unique_path = std::env::var_os("CARGO_MANIFEST_PATH").unwrap();
    let unique_id = base16::encode_lower(&sha2::Sha256::digest(unique_path.as_encoded_bytes()));
    let id = format!("_WASM_SPLIT_MARKER_{}", &unique_id[0..16]);
    let id = syn::LitStr::new(&id, Span::call_site().into());
    quote! { #id }.into()
}