wasm_split_macro/
lib.rs

1use proc_macro::TokenStream;
2
3use digest::Digest;
4use quote::{format_ident, quote};
5use syn::{parse_macro_input, parse_quote, FnArg, Ident, ItemFn, ReturnType, Signature};
6
7#[proc_macro_attribute]
8pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
9    let module_ident = parse_macro_input!(args as Ident);
10    let item_fn = parse_macro_input!(input as ItemFn);
11
12    if item_fn.sig.asyncness.is_none() {
13        panic!("wasm_split functions must be async. Use a LazyLoader with synchronous functions instead.");
14    }
15
16    let LoaderNames {
17        split_loader_ident,
18        impl_import_ident,
19        impl_export_ident,
20        load_module_ident,
21        ..
22    } = LoaderNames::new(item_fn.sig.ident.clone(), module_ident.to_string());
23
24    let mut desugard_async_sig = item_fn.sig.clone();
25    desugard_async_sig.asyncness = None;
26    desugard_async_sig.output = match &desugard_async_sig.output {
27        ReturnType::Default => {
28            parse_quote! { -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>> }
29        }
30        ReturnType::Type(_, ty) => {
31            parse_quote! { -> std::pin::Pin<Box<dyn std::future::Future<Output = #ty>>> }
32        }
33    };
34
35    let import_sig = Signature {
36        ident: impl_import_ident.clone(),
37        ..desugard_async_sig.clone()
38    };
39
40    let export_sig = Signature {
41        ident: impl_export_ident.clone(),
42        ..desugard_async_sig.clone()
43    };
44
45    let default_item = item_fn.clone();
46
47    let mut wrapper_sig = item_fn.sig;
48    wrapper_sig.asyncness = Some(Default::default());
49
50    let mut args = Vec::new();
51    for (i, param) in wrapper_sig.inputs.iter_mut().enumerate() {
52        match param {
53            syn::FnArg::Receiver(_) => args.push(format_ident!("self")),
54            syn::FnArg::Typed(pat_type) => {
55                let param_ident = format_ident!("__wasm_split_arg_{i}");
56                args.push(param_ident.clone());
57                pat_type.pat = Box::new(syn::Pat::Ident(syn::PatIdent {
58                    attrs: vec![],
59                    by_ref: None,
60                    mutability: None,
61                    ident: param_ident,
62                    subpat: None,
63                }));
64            }
65        }
66    }
67
68    let attrs = &item_fn.attrs;
69    let stmts = &item_fn.block.stmts;
70
71    quote! {
72        #[cfg(target_arch = "wasm32")]
73        #wrapper_sig {
74            #(#attrs)*
75            #[allow(improper_ctypes_definitions)]
76            #[no_mangle]
77            pub extern "C" #export_sig {
78                Box::pin(async move { #(#stmts)* })
79            }
80
81            #[link(wasm_import_module = "./__wasm_split.js")]
82            extern "C" {
83                #[no_mangle]
84                fn #load_module_ident (
85                    callback: unsafe extern "C" fn(*const ::std::ffi::c_void, bool),
86                    data: *const ::std::ffi::c_void
87                );
88
89                #[allow(improper_ctypes)]
90                #[no_mangle]
91                #import_sig;
92            }
93
94            thread_local! {
95                static #split_loader_ident: wasm_split::LazySplitLoader = unsafe {
96                    wasm_split::LazySplitLoader::new(#load_module_ident)
97                };
98            }
99
100            // Initiate the download by calling the load_module_ident function which will kick-off the loader
101            if !wasm_split::LazySplitLoader::ensure_loaded(&#split_loader_ident).await {
102                panic!("Failed to load wasm-split module");
103            }
104
105            unsafe { #impl_import_ident( #(#args),* ) }.await
106        }
107
108        #[cfg(not(target_arch = "wasm32"))]
109        #default_item
110    }
111    .into()
112}
113
114/// Create a lazy loader for a given function. Meant to be used in statics. Designed for libraries to
115/// integrate with.
116///
117/// ```rust, ignore
118/// fn SomeFunction(args: Args) -> Ret {}
119///
120/// static LOADER: wasm_split::LazyLoader<Args, Ret> = lazy_loader!(SomeFunction);
121///
122/// LOADER.load().await.call(args)
123/// ```
124#[proc_macro]
125pub fn lazy_loader(input: TokenStream) -> TokenStream {
126    // We can only accept idents/paths that will be the source function
127    let sig = parse_macro_input!(input as Signature);
128    let params = sig.inputs.clone();
129    let outputs = sig.output.clone();
130    let Some(FnArg::Typed(arg)) = params.first().cloned() else {
131        panic!(
132            "Lazy Loader must define a single input argument to satisfy the LazyLoader signature"
133        )
134    };
135    let arg_ty = arg.ty.clone();
136    let LoaderNames {
137        name,
138        split_loader_ident,
139        impl_import_ident,
140        impl_export_ident,
141        load_module_ident,
142        ..
143    } = LoaderNames::new(
144        sig.ident.clone(),
145        sig.abi
146            .as_ref()
147            .and_then(|abi| abi.name.as_ref().map(|f| f.value()))
148            .expect("abi to be module name")
149            .to_string(),
150    );
151
152    quote! {
153        {
154            #[cfg(target_arch = "wasm32")]
155            {
156                #[link(wasm_import_module = "./__wasm_split.js")]
157                extern "C" {
158                    // The function we'll use to initiate the download of the module
159                    #[no_mangle]
160                    fn #load_module_ident(
161                        callback: unsafe extern "C" fn(*const ::std::ffi::c_void, bool),
162                        data: *const ::std::ffi::c_void,
163                    );
164
165                    #[allow(improper_ctypes)]
166                    #[no_mangle]
167                    fn #impl_import_ident(arg: #arg_ty) #outputs;
168                }
169
170
171                #[allow(improper_ctypes_definitions)]
172                #[no_mangle]
173                pub extern "C" fn #impl_export_ident(arg: #arg_ty) #outputs {
174                    #name(arg)
175                }
176
177                thread_local! {
178                    static #split_loader_ident: wasm_split::LazySplitLoader = unsafe {
179                        wasm_split::LazySplitLoader::new(#load_module_ident)
180                    };
181                };
182
183                unsafe {
184                    wasm_split::LazyLoader::new(#impl_import_ident, &#split_loader_ident)
185                }
186            }
187
188            #[cfg(not(target_arch = "wasm32"))]
189            {
190                wasm_split::LazyLoader::preloaded(#name)
191            }
192        }
193    }
194    .into()
195}
196
197struct LoaderNames {
198    name: Ident,
199    split_loader_ident: Ident,
200    impl_import_ident: Ident,
201    impl_export_ident: Ident,
202    load_module_ident: Ident,
203}
204
205impl LoaderNames {
206    fn new(name: Ident, module: String) -> Self {
207        let unique_identifier = base16::encode_lower(
208            &sha2::Sha256::digest(format!("{name} {span:?}", name = name, span = name.span()))
209                [..16],
210        );
211
212        Self {
213            split_loader_ident: format_ident!("__wasm_split_loader_{module}"),
214            impl_export_ident: format_ident!(
215                "__wasm_split_00___{module}___00_export_{unique_identifier}_{name}"
216            ),
217            impl_import_ident: format_ident!(
218                "__wasm_split_00___{module}___00_import_{unique_identifier}_{name}"
219            ),
220            load_module_ident: format_ident!(
221                "__wasm_split_load_{module}_{unique_identifier}_{name}"
222            ),
223            name,
224        }
225    }
226}