wasm_splitter/
lib.rs

1use std::{
2    cell::Cell,
3    ffi::c_void,
4    future::Future,
5    pin::Pin,
6    rc::Rc,
7    task::{Context, Poll, Waker},
8    thread::LocalKey,
9};
10
11pub use wasm_split_macro::{lazy_loader, wasm_split};
12
13pub type Result<T> = std::result::Result<T, SplitLoaderError>;
14
15#[non_exhaustive]
16#[derive(Debug, Clone)]
17pub enum SplitLoaderError {
18    FailedToLoad,
19}
20impl std::fmt::Display for SplitLoaderError {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            SplitLoaderError::FailedToLoad => write!(f, "Failed to load wasm-split module"),
24        }
25    }
26}
27
28/// A lazy loader that can be used to load a function from a split out `.wasm` file.
29///
30/// # Example
31///
32/// To use the split loader, you must first create the loader using the `lazy_loader` macro. This macro
33/// requires the complete signature of the function you want to load. The extern abi string denotes
34/// which module the function should be loaded from. If you don't know which module to use, use `auto`
35/// and wasm-split will automatically combine all the modules into one.
36///
37/// ```rust, ignore
38/// static LOADER: wasm_split::LazyLoader<Args, Ret> = wasm_split::lazy_loader!(extern "auto" fn SomeFunction(args: Args) -> Ret);
39///
40/// fn SomeFunction(args: Args) -> Ret {
41///     // Implementation
42/// }
43/// ```
44///
45/// ## The `#[component(lazy)]` macro
46///
47/// If you're using wasm-split with Dioxus, the `#[component(lazy)]` macro is provided that wraps
48/// the lazy loader with suspense. This means that the component will suspense until its body has
49/// been loaded.
50///
51/// ```rust, ignore
52/// fn app() -> Element {
53///     rsx! {
54///         Suspense {
55///             fallback: rsx! { "Loading..." },
56///             LazyComponent { abc: 0 }
57///         }
58///     }
59/// }
60///
61/// #[component(lazy)]
62/// fn LazyComponent(abc: i32) -> Element {
63///     rsx! {
64///         div {
65///             "This is a lazy component! {abc}"
66///         }
67///     }
68/// }
69/// ```
70pub struct LazyLoader<Args, Ret> {
71    imported: unsafe extern "C" fn(arg: Args) -> Ret,
72    key: &'static LocalKey<LazySplitLoader>,
73}
74
75impl<Args, Ret> LazyLoader<Args, Ret> {
76    /// Create a new lazy loader from a lazy imported function and a LazySplitLoader
77    ///
78    /// # Safety
79    /// This is unsafe because we're taking an arbitrary function pointer and using it as the loader.
80    /// This function is likely not instantiated when passed here, so it should never be called directly.
81    #[doc(hidden)]
82    pub const unsafe fn new(
83        imported: unsafe extern "C" fn(arg: Args) -> Ret,
84        key: &'static LocalKey<LazySplitLoader>,
85    ) -> Self {
86        Self { imported, key }
87    }
88
89    /// Create a new lazy loader that is already resolved.
90    pub const fn preloaded(f: fn(Args) -> Ret) -> Self {
91        let imported =
92            unsafe { std::mem::transmute::<fn(Args) -> Ret, unsafe extern "C" fn(Args) -> Ret>(f) };
93
94        thread_local! {
95            static LAZY: LazySplitLoader = LazySplitLoader::preloaded();
96        };
97
98        Self {
99            imported,
100            key: &LAZY,
101        }
102    }
103
104    /// Load the lazy loader, returning an boolean indicating whether it loaded successfully
105    pub async fn load(&'static self) -> bool {
106        *self.key.with(|inner| inner.lazy.clone()).as_ref().await
107    }
108
109    /// Call the lazy loader with the given arguments
110    pub fn call(&'static self, args: Args) -> Result<Ret> {
111        let Some(true) = self.key.with(|inner| inner.lazy.try_get().copied()) else {
112            return Err(SplitLoaderError::FailedToLoad);
113        };
114
115        Ok(unsafe { (self.imported)(args) })
116    }
117}
118
119type Lazy = async_once_cell::Lazy<bool, SplitLoaderFuture>;
120type LoadCallbackFn = unsafe extern "C" fn(*const c_void, bool) -> ();
121type LoadFn = unsafe extern "C" fn(LoadCallbackFn, *const c_void) -> ();
122
123pub struct LazySplitLoader {
124    lazy: Pin<Rc<Lazy>>,
125}
126
127impl LazySplitLoader {
128    /// Create a new lazy split loader from a load function that is generated by the wasm-split macro
129    ///
130    /// # Safety
131    ///
132    /// This is unsafe because we're taking an arbitrary function pointer and using it as the loader.
133    /// It is likely not instantiated when passed here, so it should never be called directly.
134    #[doc(hidden)]
135    pub unsafe fn new(load: LoadFn) -> Self {
136        Self {
137            lazy: Rc::pin(Lazy::new({
138                SplitLoaderFuture {
139                    loader: Rc::new(SplitLoader {
140                        state: Cell::new(SplitLoaderState::Deferred(load)),
141                        waker: Cell::new(None),
142                    }),
143                }
144            })),
145        }
146    }
147
148    fn preloaded() -> Self {
149        Self {
150            lazy: Rc::pin(Lazy::new({
151                SplitLoaderFuture {
152                    loader: Rc::new(SplitLoader {
153                        state: Cell::new(SplitLoaderState::Completed(true)),
154                        waker: Cell::new(None),
155                    }),
156                }
157            })),
158        }
159    }
160
161    /// Wait for the lazy loader to load
162    pub async fn ensure_loaded(loader: &'static std::thread::LocalKey<LazySplitLoader>) -> bool {
163        *loader.with(|inner| inner.lazy.clone()).as_ref().await
164    }
165}
166
167struct SplitLoader {
168    state: Cell<SplitLoaderState>,
169    waker: Cell<Option<Waker>>,
170}
171
172#[derive(Clone, Copy)]
173enum SplitLoaderState {
174    Deferred(LoadFn),
175    Pending,
176    Completed(bool),
177}
178
179struct SplitLoaderFuture {
180    loader: Rc<SplitLoader>,
181}
182
183impl Future for SplitLoaderFuture {
184    type Output = bool;
185
186    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<bool> {
187        unsafe extern "C" fn load_callback(loader: *const c_void, success: bool) {
188            let loader = unsafe { Rc::from_raw(loader as *const SplitLoader) };
189            loader.state.set(SplitLoaderState::Completed(success));
190            if let Some(waker) = loader.waker.take() {
191                waker.wake()
192            }
193        }
194
195        match self.loader.state.get() {
196            SplitLoaderState::Deferred(load) => {
197                self.loader.state.set(SplitLoaderState::Pending);
198                self.loader.waker.set(Some(cx.waker().clone()));
199                unsafe {
200                    load(
201                        load_callback,
202                        Rc::<SplitLoader>::into_raw(self.loader.clone()) as *const c_void,
203                    )
204                };
205                Poll::Pending
206            }
207            SplitLoaderState::Pending => {
208                self.loader.waker.set(Some(cx.waker().clone()));
209                Poll::Pending
210            }
211            SplitLoaderState::Completed(value) => Poll::Ready(value),
212        }
213    }
214}