rquickjs_core/
loader.rs

1//! Loaders and resolvers for loading JS modules.
2
3use std::{ffi::CStr, ptr};
4
5use crate::{module::Declared, qjs, Ctx, Module, Result};
6
7mod builtin_loader;
8mod builtin_resolver;
9pub mod bundle;
10mod compile;
11mod file_resolver;
12mod module_loader;
13mod script_loader;
14mod util;
15
16#[cfg(feature = "dyn-load")]
17mod native_loader;
18
19pub use builtin_loader::BuiltinLoader;
20pub use builtin_resolver::BuiltinResolver;
21pub use compile::Compile;
22pub use file_resolver::FileResolver;
23pub use module_loader::ModuleLoader;
24pub use script_loader::ScriptLoader;
25
26#[cfg(feature = "dyn-load")]
27#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "dyn-load")))]
28pub use native_loader::NativeLoader;
29
30#[cfg(feature = "phf")]
31/// The type of bundle that the `embed!` macro returns
32pub type Bundle = bundle::Bundle<bundle::PhfBundleData<&'static [u8]>>;
33
34#[cfg(not(feature = "phf"))]
35/// The type of bundle that the `embed!` macro returns
36pub type Bundle = bundle::Bundle<bundle::ScaBundleData<&'static [u8]>>;
37
38/// Module resolver interface
39#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "loader")))]
40pub trait Resolver {
41    /// Normalize module name
42    ///
43    /// The resolving may looks like:
44    ///
45    /// ```no_run
46    /// # use rquickjs::{Ctx, Result, Error};
47    /// # fn default_resolve<'js>(_ctx: &Ctx<'js>, base: &str, name: &str) -> Result<String> {
48    /// Ok(if !name.starts_with('.') {
49    ///     name.into()
50    /// } else {
51    ///     let mut split = base.rsplitn(2, '/');
52    ///     let path = match (split.next(), split.next()) {
53    ///         (_, Some(path)) => path,
54    ///         _ => "",
55    ///     };
56    ///     format!("{path}/{name}")
57    /// })
58    /// # }
59    /// ```
60    fn resolve<'js>(&mut self, ctx: &Ctx<'js>, base: &str, name: &str) -> Result<String>;
61}
62
63/// Module loader interface
64#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "loader")))]
65pub trait Loader {
66    /// Load module by name
67    fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> Result<Module<'js, Declared>>;
68}
69
70struct LoaderOpaque {
71    resolver: Box<dyn Resolver>,
72    loader: Box<dyn Loader>,
73}
74
75#[derive(Debug)]
76#[repr(transparent)]
77pub(crate) struct LoaderHolder(*mut LoaderOpaque);
78
79impl Drop for LoaderHolder {
80    fn drop(&mut self) {
81        let _opaque = unsafe { Box::from_raw(self.0) };
82    }
83}
84
85impl LoaderHolder {
86    pub fn new<R, L>(resolver: R, loader: L) -> Self
87    where
88        R: Resolver + 'static,
89        L: Loader + 'static,
90    {
91        Self(Box::into_raw(Box::new(LoaderOpaque {
92            resolver: Box::new(resolver),
93            loader: Box::new(loader),
94        })))
95    }
96
97    pub(crate) fn set_to_runtime(&self, rt: *mut qjs::JSRuntime) {
98        unsafe {
99            qjs::JS_SetModuleLoaderFunc(
100                rt,
101                Some(Self::normalize_raw),
102                Some(Self::load_raw),
103                self.0 as _,
104            );
105        }
106    }
107
108    #[inline]
109    fn normalize<'js>(
110        opaque: &mut LoaderOpaque,
111        ctx: &Ctx<'js>,
112        base: &CStr,
113        name: &CStr,
114    ) -> Result<*mut qjs::c_char> {
115        let base = base.to_str()?;
116        let name = name.to_str()?;
117
118        let name = opaque.resolver.resolve(ctx, base, name)?;
119
120        // We should transfer ownership of this string to QuickJS
121        Ok(
122            unsafe {
123                qjs::js_strndup(ctx.as_ptr(), name.as_ptr() as _, name.as_bytes().len() as _)
124            },
125        )
126    }
127
128    unsafe extern "C" fn normalize_raw(
129        ctx: *mut qjs::JSContext,
130        base: *const qjs::c_char,
131        name: *const qjs::c_char,
132        opaque: *mut qjs::c_void,
133    ) -> *mut qjs::c_char {
134        let ctx = Ctx::from_ptr(ctx);
135        let base = CStr::from_ptr(base);
136        let name = CStr::from_ptr(name);
137        let loader = &mut *(opaque as *mut LoaderOpaque);
138
139        Self::normalize(loader, &ctx, base, name).unwrap_or_else(|error| {
140            error.throw(&ctx);
141            ptr::null_mut()
142        })
143    }
144
145    #[inline]
146    unsafe fn load<'js>(
147        opaque: &mut LoaderOpaque,
148        ctx: &Ctx<'js>,
149        name: &CStr,
150    ) -> Result<*mut qjs::JSModuleDef> {
151        let name = name.to_str()?;
152
153        Ok(opaque.loader.load(ctx, name)?.as_ptr())
154    }
155
156    unsafe extern "C" fn load_raw(
157        ctx: *mut qjs::JSContext,
158        name: *const qjs::c_char,
159        opaque: *mut qjs::c_void,
160    ) -> *mut qjs::JSModuleDef {
161        let ctx = Ctx::from_ptr(ctx);
162        let name = CStr::from_ptr(name);
163        let loader = &mut *(opaque as *mut LoaderOpaque);
164
165        Self::load(loader, &ctx, name).unwrap_or_else(|error| {
166            error.throw(&ctx);
167            ptr::null_mut()
168        })
169    }
170}
171
172macro_rules! loader_impls {
173    ($($t:ident)*) => {
174        loader_impls!(@sub @mark $($t)*);
175    };
176    (@sub $($lead:ident)* @mark $head:ident $($rest:ident)*) => {
177        loader_impls!(@impl $($lead)*);
178        loader_impls!(@sub $($lead)* $head @mark $($rest)*);
179    };
180    (@sub $($lead:ident)* @mark) => {
181        loader_impls!(@impl $($lead)*);
182    };
183    (@impl $($t:ident)*) => {
184            impl<$($t,)*> Resolver for ($($t,)*)
185            where
186                $($t: Resolver,)*
187            {
188                #[allow(non_snake_case)]
189                #[allow(unused_mut)]
190                fn resolve<'js>(&mut self, _ctx: &Ctx<'js>, base: &str, name: &str) -> Result<String> {
191                    let mut messages = Vec::<std::string::String>::new();
192                    let ($($t,)*) = self;
193                    $(
194                        match $t.resolve(_ctx, base, name) {
195                            // Still could try the next resolver
196                            Err($crate::Error::Resolving { message, .. }) => {
197                                message.map(|message| messages.push(message));
198                            },
199                            result => return result,
200                        }
201                    )*
202                    // Unable to resolve module name
203                    Err(if messages.is_empty() {
204                        $crate::Error::new_resolving(base, name)
205                    } else {
206                        $crate::Error::new_resolving_message(base, name, messages.join("\n"))
207                    })
208                }
209            }
210
211            impl< $($t,)*> $crate::loader::Loader for ($($t,)*)
212            where
213                $($t: $crate::loader::Loader,)*
214            {
215                #[allow(non_snake_case)]
216                #[allow(unused_mut)]
217                fn load<'js>(&mut self, _ctx: &Ctx<'js>, name: &str) -> Result<Module<'js, Declared>> {
218                    let mut messages = Vec::<std::string::String>::new();
219                    let ($($t,)*) = self;
220                    $(
221                        match $t.load(_ctx, name) {
222                            // Still could try the next loader
223                            Err($crate::Error::Loading { message, .. }) => {
224                                message.map(|message| messages.push(message));
225                            },
226                            result => return result,
227                        }
228                    )*
229                    // Unable to load module
230                    Err(if messages.is_empty() {
231                        $crate::Error::new_loading(name)
232                    } else {
233                        $crate::Error::new_loading_message(name, messages.join("\n"))
234                    })
235                }
236            }
237    };
238}
239loader_impls!(A B C D E F G H);
240
241#[cfg(test)]
242mod test {
243    use crate::{CatchResultExt, Context, Ctx, Error, Module, Result, Runtime};
244
245    use super::{Loader, Resolver};
246
247    struct TestResolver;
248
249    impl Resolver for TestResolver {
250        fn resolve<'js>(&mut self, _ctx: &Ctx<'js>, base: &str, name: &str) -> Result<String> {
251            if base == "loader" && name == "test" {
252                Ok(name.into())
253            } else {
254                Err(Error::new_resolving_message(
255                    base,
256                    name,
257                    "unable to resolve",
258                ))
259            }
260        }
261    }
262
263    struct TestLoader;
264
265    impl Loader for TestLoader {
266        fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> Result<Module<'js>> {
267            if name == "test" {
268                Module::declare(
269                    ctx.clone(),
270                    "test",
271                    r#"
272                      export const n = 123;
273                      export const s = "abc";
274                    "#,
275                )
276            } else {
277                Err(Error::new_loading_message(name, "unable to load"))
278            }
279        }
280    }
281
282    #[test]
283    fn custom_loader() {
284        let rt = Runtime::new().unwrap();
285        let ctx = Context::full(&rt).unwrap();
286        rt.set_loader(TestResolver, TestLoader);
287        ctx.with(|ctx| {
288            Module::evaluate(
289                ctx,
290                "loader",
291                r#"
292                      import { n, s } from "test";
293                      export default [n, s];
294                    "#,
295            )
296            .unwrap()
297            .finish::<()>()
298            .unwrap();
299        })
300    }
301
302    #[test]
303    #[should_panic(expected = "Error resolving module")]
304    fn resolving_error() {
305        let rt = Runtime::new().unwrap();
306        let ctx = Context::full(&rt).unwrap();
307        rt.set_loader(TestResolver, TestLoader);
308        ctx.with(|ctx| {
309            Module::evaluate(
310                ctx.clone(),
311                "loader",
312                r#"
313                      import { n, s } from "test_";
314                    "#,
315            )
316            .catch(&ctx)
317            .unwrap()
318            .finish::<()>()
319            .catch(&ctx)
320            .expect("Unable to resolve");
321        })
322    }
323}