rquickjs_core/value/
module.rs

1//! Types for loading and handling JS modules.
2
3use std::{
4    ffi::{CStr, CString},
5    marker::PhantomData,
6    mem::MaybeUninit,
7    ptr::{self, NonNull},
8    slice,
9};
10
11use crate::{
12    atom::PredefinedAtom, qjs, Atom, Ctx, Error, FromAtom, FromJs, IntoAtom, IntoJs, Object,
13    Promise, Result, Value,
14};
15
16/// Helper macro to provide module init function.
17/// Use for exporting module definitions to be loaded as part of a dynamic library.
18/// ```
19/// use rquickjs::{module::ModuleDef, module_init};
20///
21/// struct MyModule;
22/// impl ModuleDef for MyModule {}
23///
24/// module_init!(MyModule);
25/// // or
26/// module_init!(js_init_my_module: MyModule);
27/// ```
28#[macro_export]
29macro_rules! module_init {
30    ($type:ty) => {
31        $crate::module_init!(js_init_module: $type);
32    };
33
34    ($name:ident: $type:ty) => {
35        #[no_mangle]
36        pub unsafe extern "C" fn $name(
37            ctx: *mut $crate::qjs::JSContext,
38            module_name: *const $crate::qjs::c_char,
39        ) -> *mut $crate::qjs::JSModuleDef {
40            $crate::Module::init_raw::<$type>(ctx, module_name)
41        }
42    };
43}
44
45/// The raw module load function (`js_module_init`)
46pub type ModuleLoadFn =
47    unsafe extern "C" fn(*mut qjs::JSContext, *const qjs::c_char) -> *mut qjs::JSModuleDef;
48
49/// A class which can be used to declare rust-native JavaScript modules.
50pub trait ModuleDef {
51    fn declare<'js>(decl: &Declarations<'js>) -> Result<()> {
52        let _ = decl;
53        Ok(())
54    }
55
56    fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> {
57        let _ = (exports, ctx);
58        Ok(())
59    }
60}
61
62/// A struct used for setting declarations on a module.
63pub struct Declarations<'js>(Module<'js, Declared>);
64
65impl<'js> Declarations<'js> {
66    /// Define a new export in a module.
67    pub fn declare<N>(&self, name: N) -> Result<&Self>
68    where
69        N: Into<Vec<u8>>,
70    {
71        let name = CString::new(name)?;
72        self.declare_c_str(name.as_c_str())
73    }
74
75    /// Define a new export in a module.
76    ///
77    /// This function avoids an extra allocation, having to convert from a rust string into a
78    /// null-terminated CStr.
79    pub fn declare_c_str(&self, name: &CStr) -> Result<&Self> {
80        unsafe { qjs::JS_AddModuleExport(self.0.ctx.as_ptr(), self.0.as_ptr(), name.as_ptr()) };
81        Ok(self)
82    }
83}
84
85/// A struct used for setting the value of previously declared exporsts of a module.
86pub struct Exports<'js>(Module<'js, Declared>);
87
88impl<'js> Exports<'js> {
89    /// Set the value of an exported entry.
90    pub fn export<N: Into<Vec<u8>>, T: IntoJs<'js>>(&self, name: N, value: T) -> Result<&Self> {
91        let name = CString::new(name.into())?;
92        self.export_c_str(name.as_c_str(), value)
93    }
94
95    /// Set the value of an exported entry.
96    ///
97    /// This function avoids a possible conversion from a rust string into a CStr
98    pub fn export_c_str<T: IntoJs<'js>>(&self, name: &CStr, value: T) -> Result<&Self> {
99        let value = value.into_js(&self.0.ctx)?;
100        let res = unsafe {
101            qjs::JS_SetModuleExport(
102                self.0.ctx.as_ptr(),
103                self.0.as_ptr(),
104                name.as_ptr(),
105                value.into_js_value(),
106            )
107        };
108        if res < 0 {
109            return Err(Error::InvalidExport);
110        }
111
112        Ok(self)
113    }
114}
115
116/// A marker struct used to indicate that a module is possibly not yet evaluated.
117#[derive(Clone, Copy, Debug)]
118pub struct Declared;
119/// A marker struct used to indicate that a module was evaluated.
120#[derive(Clone, Copy, Debug)]
121pub struct Evaluated;
122
123/// A JavaScript module.
124#[derive(Clone, Debug)]
125pub struct Module<'js, T = Declared> {
126    ptr: NonNull<qjs::JSModuleDef>,
127    ctx: Ctx<'js>,
128    _type_marker: PhantomData<T>,
129}
130
131impl<'js, T> Module<'js, T> {
132    pub(crate) fn as_ptr(&self) -> *mut qjs::JSModuleDef {
133        self.ptr.as_ptr()
134    }
135
136    pub(crate) unsafe fn from_ptr(ctx: Ctx<'js>, ptr: NonNull<qjs::JSModuleDef>) -> Module<'js, T> {
137        Module {
138            ptr,
139            ctx,
140            _type_marker: PhantomData,
141        }
142    }
143
144    unsafe extern "C" fn eval_fn<D>(
145        ctx: *mut qjs::JSContext,
146        ptr: *mut qjs::JSModuleDef,
147    ) -> qjs::c_int
148    where
149        D: ModuleDef,
150    {
151        let ctx = Ctx::from_ptr(ctx);
152        // Should never be null
153        let ptr = NonNull::new(ptr).unwrap();
154        let module = unsafe { Module::from_ptr(ctx.clone(), ptr) };
155        let exports = Exports(module);
156        match D::evaluate(&ctx, &exports) {
157            Ok(_) => 0,
158            Err(error) => {
159                error.throw(&ctx);
160                -1
161            }
162        }
163    }
164
165    /// Returns the name of the module
166    pub fn name<N>(&self) -> Result<N>
167    where
168        N: FromAtom<'js>,
169    {
170        let name = unsafe {
171            Atom::from_atom_val(
172                self.ctx.clone(),
173                qjs::JS_GetModuleName(self.ctx.as_ptr(), self.as_ptr()),
174            )
175        };
176        N::from_atom(name)
177    }
178}
179
180impl<'js> Module<'js, Declared> {
181    /// Declare a module but don't evaluate it.
182    pub fn declare<N, S>(ctx: Ctx<'js>, name: N, source: S) -> Result<Module<'js, Declared>>
183    where
184        N: Into<Vec<u8>>,
185        S: Into<Vec<u8>>,
186    {
187        let name = CString::new(name)?;
188        let flag =
189            qjs::JS_EVAL_TYPE_MODULE | qjs::JS_EVAL_FLAG_STRICT | qjs::JS_EVAL_FLAG_COMPILE_ONLY;
190
191        let module_val = unsafe { ctx.eval_raw(source, name.as_c_str(), flag as i32)? };
192        let module_val = unsafe { ctx.handle_exception(module_val)? };
193        debug_assert_eq!(qjs::JS_TAG_MODULE, unsafe {
194            qjs::JS_VALUE_GET_TAG(module_val)
195        });
196        let module_ptr = unsafe {
197            NonNull::new(qjs::JS_VALUE_GET_PTR(module_val).cast()).ok_or(Error::Unknown)?
198        };
199        unsafe { Ok(Module::from_ptr(ctx, module_ptr)) }
200    }
201
202    /// Declare a rust native module but don't evaluate it.
203    pub fn declare_def<D, N>(ctx: Ctx<'js>, name: N) -> Result<Module<'js, Declared>>
204    where
205        N: Into<Vec<u8>>,
206        D: ModuleDef,
207    {
208        let name = CString::new(name)?;
209        let ptr =
210            unsafe { qjs::JS_NewCModule(ctx.as_ptr(), name.as_ptr(), Some(Self::eval_fn::<D>)) };
211        let ptr = NonNull::new(ptr).ok_or(Error::Unknown)?;
212        let m = unsafe { Module::from_ptr(ctx, ptr) };
213
214        let decl = Declarations(m);
215        D::declare(&decl)?;
216
217        Ok(decl.0)
218        //Ok(())
219    }
220
221    /// Evaluate the source of a module.
222    ///
223    /// This function returns a promise which resolved when the modules was fully compiled and
224    /// returns undefined.
225    ///
226    /// Since QuickJS doesn't give us a way to retrieve the module if we immediately evaluate a
227    /// modules source this function doesn't return a module object.
228    /// If the module is required, you should first declare it with [`Module::declare`] and then call [`Module::eval`] on the
229    /// returned module.
230    pub fn evaluate<N, S>(ctx: Ctx<'js>, name: N, source: S) -> Result<Promise<'js>>
231    where
232        N: Into<Vec<u8>>,
233        S: Into<Vec<u8>>,
234    {
235        let name = CString::new(name)?;
236        let flag = qjs::JS_EVAL_TYPE_MODULE | qjs::JS_EVAL_FLAG_STRICT;
237
238        let module_val = unsafe { ctx.eval_raw(source, name.as_c_str(), flag as i32)? };
239        let module_val = unsafe { ctx.handle_exception(module_val)? };
240        let v = unsafe { Value::from_js_value(ctx, module_val) };
241        Ok(v.into_promise().expect("evaluate should return a promise"))
242    }
243
244    /// Declares a module in the runtime and evaluates it.
245    pub fn evaluate_def<D, N>(
246        ctx: Ctx<'js>,
247        name: N,
248    ) -> Result<(Module<'js, Evaluated>, Promise<'js>)>
249    where
250        N: Into<Vec<u8>>,
251        D: ModuleDef,
252    {
253        let module = Self::declare_def::<D, N>(ctx, name)?;
254        module.eval()
255    }
256
257    /// Load a module from quickjs bytecode.
258    ///
259    /// # Safety
260    /// User must ensure that bytes handed to this function contain valid bytecode.
261    pub unsafe fn load(ctx: Ctx<'js>, bytes: &[u8]) -> Result<Module<'js, Declared>> {
262        let module = unsafe {
263            qjs::JS_ReadObject(
264                ctx.as_ptr(),
265                bytes.as_ptr(),
266                bytes.len() as _,
267                (qjs::JS_READ_OBJ_BYTECODE | qjs::JS_READ_OBJ_ROM_DATA) as i32,
268            )
269        };
270        let module = ctx.handle_exception(module)?;
271        debug_assert_eq!(qjs::JS_TAG_MODULE, unsafe { qjs::JS_VALUE_GET_TAG(module) });
272        let module_ptr =
273            unsafe { NonNull::new(qjs::JS_VALUE_GET_PTR(module).cast()).ok_or(Error::Unknown)? };
274        unsafe { Ok(Module::from_ptr(ctx, module_ptr)) }
275    }
276
277    /// Load a module from a raw module loading function.
278    ///
279    /// # Safety
280    /// The soundness of this function depends completely on if load_fn is implemented correctly.
281    /// THe load_fn function must return a pointer to a valid module loaded with the given context.
282    pub unsafe fn from_load_fn<N>(
283        ctx: Ctx<'js>,
284        name: N,
285        load_fn: ModuleLoadFn,
286    ) -> Result<Module<'js, Declared>>
287    where
288        N: Into<Vec<u8>>,
289    {
290        let name = CString::new(name)?;
291        let ptr = (load_fn)(ctx.as_ptr(), name.as_ptr().cast());
292        let ptr = NonNull::new(ptr).ok_or(Error::Exception)?;
293        unsafe { Ok(Module::from_ptr(ctx, ptr)) }
294    }
295
296    /// Evaluate the module.
297    ///
298    /// Returns the module as being evaluated and a promise which resolves when the module has finished evaluating.
299    /// The return value of the promise is the JavaScript value undefined.
300    pub fn eval(self) -> Result<(Module<'js, Evaluated>, Promise<'js>)> {
301        let ret = unsafe {
302            // JS_EvalFunction `free's` the module so we should dup first
303            let v = qjs::JS_MKPTR(qjs::JS_TAG_MODULE, self.ptr.as_ptr().cast());
304            qjs::JS_DupValue(self.ctx.as_ptr(), v);
305            qjs::JS_EvalFunction(self.ctx.as_ptr(), v)
306        };
307        let ret = unsafe { self.ctx.handle_exception(ret)? };
308        let promise = unsafe { Promise::from_js_value(self.ctx.clone(), ret) };
309        Ok((
310            Module {
311                ptr: self.ptr,
312                ctx: self.ctx,
313                _type_marker: PhantomData,
314            },
315            promise,
316        ))
317    }
318
319    /// A function for loading a Rust module from C.
320    ///
321    /// # Safety
322    /// This function should only be called when the module is loaded as part of a dynamically
323    /// loaded library.
324    pub unsafe extern "C" fn init_raw<D>(
325        ctx: *mut qjs::JSContext,
326        name: *const qjs::c_char,
327    ) -> *mut qjs::JSModuleDef
328    where
329        D: ModuleDef,
330    {
331        let ctx = Ctx::from_ptr(ctx);
332        let name = CStr::from_ptr(name).to_bytes();
333        match Self::declare_def::<D, _>(ctx.clone(), name) {
334            Ok(module) => module.as_ptr(),
335            Err(error) => {
336                error.throw(&ctx);
337                ptr::null_mut()
338            }
339        }
340    }
341
342    /// Import and evaluate a module
343    ///
344    /// This will work similar to an `import(specifier)` statement in JavaScript returning a promise with the result of the imported module.
345    pub fn import<S: Into<Vec<u8>>>(ctx: &Ctx<'js>, specifier: S) -> Result<Promise<'js>> {
346        let specifier = CString::new(specifier)?;
347        unsafe {
348            let base_name = ctx
349                .script_or_module_name(1)
350                .unwrap_or_else(|| Atom::from_predefined(ctx.clone(), PredefinedAtom::Empty));
351
352            let base_name_c_str = qjs::JS_AtomToCString(ctx.as_ptr(), base_name.atom);
353
354            let res = qjs::JS_LoadModule(ctx.as_ptr(), base_name_c_str, specifier.as_ptr());
355
356            qjs::JS_FreeCString(ctx.as_ptr(), base_name_c_str);
357
358            let res = ctx.handle_exception(res)?;
359
360            Ok(Promise::from_js_value(ctx.clone(), res))
361        }
362    }
363}
364
365impl<'js, Evaluated> Module<'js, Evaluated> {
366    /// Write object bytecode for the module in little endian format.
367    pub fn write_le(&self) -> Result<Vec<u8>> {
368        let swap = cfg!(target_endian = "big");
369        self.write(swap)
370    }
371
372    /// Write object bytecode for the module in big endian format.
373    pub fn write_be(&self) -> Result<Vec<u8>> {
374        let swap = cfg!(target_endian = "little");
375        self.write(swap)
376    }
377
378    /// Write object bytecode for the module.
379    ///
380    /// `swap_endianess` swaps the endianness of the bytecode, if true, from native to the other
381    /// kind. Use if the bytecode is meant for a target with a different endianness than the
382    /// current.
383    pub fn write(&self, swap_endianess: bool) -> Result<Vec<u8>> {
384        let ctx = &self.ctx;
385        let mut len = MaybeUninit::uninit();
386        // TODO: Allow inclusion of other flags?
387        let mut flags = qjs::JS_WRITE_OBJ_BYTECODE;
388        if swap_endianess {
389            flags |= qjs::JS_WRITE_OBJ_BSWAP;
390        }
391        let buf = unsafe {
392            qjs::JS_WriteObject(
393                ctx.as_ptr(),
394                len.as_mut_ptr(),
395                qjs::JS_MKPTR(qjs::JS_TAG_MODULE, self.ptr.as_ptr().cast()),
396                flags as i32,
397            )
398        };
399        if buf.is_null() {
400            return Err(ctx.raise_exception());
401        }
402        let len = unsafe { len.assume_init() };
403        let obj = unsafe { slice::from_raw_parts(buf, len as _) };
404        let obj = Vec::from(obj);
405        unsafe { qjs::js_free(ctx.as_ptr(), buf as _) };
406        Ok(obj)
407    }
408
409    /// Return the `import.meta` object of a module
410    pub fn meta(&self) -> Result<Object<'js>> {
411        unsafe {
412            Ok(Object::from_js_value(
413                self.ctx.clone(),
414                self.ctx
415                    .handle_exception(qjs::JS_GetImportMeta(self.ctx.as_ptr(), self.as_ptr()))?,
416            ))
417        }
418    }
419
420    /// Returns the module namespace, an object containing all the module exported values.
421    pub fn namespace(&self) -> Result<Object<'js>> {
422        unsafe {
423            let v = qjs::JS_GetModuleNamespace(self.ctx.as_ptr(), self.as_ptr());
424            let v = self.ctx.handle_exception(v)?;
425            Ok(Object::from_js_value(self.ctx.clone(), v))
426        }
427    }
428
429    /// Return exported value by name
430    pub fn get<N, T>(&self, name: N) -> Result<T>
431    where
432        N: IntoAtom<'js>,
433        T: FromJs<'js>,
434    {
435        self.namespace()?.get(name)
436    }
437
438    /// Change the module back to being only declared.
439    ///
440    /// This is always safe to do since calling eval again on an already evaluated module is safe.
441    pub fn into_declared(self) -> Module<'js, Declared> {
442        Module {
443            ptr: self.ptr,
444            ctx: self.ctx,
445            _type_marker: PhantomData,
446        }
447    }
448}
449
450#[cfg(test)]
451mod test {
452
453    use super::*;
454    use crate::*;
455
456    pub struct RustModule;
457
458    impl ModuleDef for RustModule {
459        fn declare(define: &Declarations) -> Result<()> {
460            define.declare_c_str(CStr::from_bytes_with_nul(b"hello\0")?)?;
461            Ok(())
462        }
463
464        fn evaluate<'js>(_ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> {
465            exports.export_c_str(CStr::from_bytes_with_nul(b"hello\0")?, "world")?;
466            Ok(())
467        }
468    }
469
470    pub struct CrashingRustModule;
471
472    impl ModuleDef for CrashingRustModule {
473        fn declare(_: &Declarations) -> Result<()> {
474            Ok(())
475        }
476
477        fn evaluate<'js>(ctx: &Ctx<'js>, _exports: &Exports<'js>) -> Result<()> {
478            ctx.eval::<(), _>(r#"throw new Error("kaboom")"#)?;
479            Ok(())
480        }
481    }
482
483    #[test]
484    fn from_rust_def() {
485        test_with(|ctx| {
486            Module::declare_def::<RustModule, _>(ctx, "rust_mod").unwrap();
487        })
488    }
489
490    #[test]
491    fn from_rust_def_eval() {
492        test_with(|ctx| {
493            let _ = Module::evaluate_def::<RustModule, _>(ctx, "rust_mod").unwrap();
494        })
495    }
496
497    #[test]
498    fn import_native() {
499        test_with(|ctx| {
500            Module::declare_def::<RustModule, _>(ctx.clone(), "rust_mod").unwrap();
501            Module::evaluate(
502                ctx.clone(),
503                "test",
504                r#"
505                import { hello } from "rust_mod";
506
507                globalThis.hello = hello;
508            "#,
509            )
510            .unwrap()
511            .finish::<()>()
512            .unwrap();
513            let text = ctx
514                .globals()
515                .get::<_, String>("hello")
516                .unwrap()
517                .to_string()
518                .unwrap();
519            assert_eq!(text.as_str(), "world");
520        })
521    }
522
523    #[test]
524    fn import_async() {
525        test_with(|ctx| {
526            Module::declare(
527                ctx.clone(),
528                "rust_mod",
529                "
530                async function foo(){
531                    return 'world';
532                };
533                export let hello = await foo();
534            ",
535            )
536            .unwrap();
537            Module::evaluate(
538                ctx.clone(),
539                "test",
540                r#"
541                import { hello } from "rust_mod";
542                globalThis.hello = hello;
543            "#,
544            )
545            .unwrap()
546            .finish::<()>()
547            .unwrap();
548            let text = ctx
549                .globals()
550                .get::<_, String>("hello")
551                .unwrap()
552                .to_string()
553                .unwrap();
554            assert_eq!(text.as_str(), "world");
555        })
556    }
557
558    #[test]
559    fn import() {
560        test_with(|ctx| {
561            Module::declare_def::<RustModule, _>(ctx.clone(), "rust_mod").unwrap();
562            let val: Object = Module::import(&ctx, "rust_mod").unwrap().finish().unwrap();
563            let hello: StdString = val.get("hello").unwrap();
564
565            assert_eq!(&hello, "world");
566        })
567    }
568
569    #[test]
570    #[should_panic(expected = "kaboom")]
571    fn import_crashing() {
572        use crate::{CatchResultExt, Context, Runtime};
573
574        let runtime = Runtime::new().unwrap();
575        let ctx = Context::full(&runtime).unwrap();
576        ctx.with(|ctx| {
577            Module::declare_def::<CrashingRustModule, _>(ctx.clone(), "bad_rust_mod").unwrap();
578            let _: Value = Module::import(&ctx, "bad_rust_mod")
579                .catch(&ctx)
580                .unwrap()
581                .finish()
582                .catch(&ctx)
583                .unwrap();
584        });
585    }
586
587    #[test]
588    fn eval_crashing_module_inside_module() {
589        let runtime = Runtime::new().unwrap();
590        let ctx = Context::full(&runtime).unwrap();
591
592        ctx.with(|ctx| {
593            let globals = ctx.globals();
594            let eval_crashing = |ctx: Ctx| {
595                Module::evaluate(ctx, "test2", "throw new Error(1)").map(|x| x.finish::<()>())
596            };
597            let function = Function::new(ctx.clone(), eval_crashing).unwrap();
598            globals.set("eval_crashing", function).unwrap();
599
600            let res = Module::evaluate(ctx, "test", " eval_crashing(); ")
601                .unwrap()
602                .finish::<()>();
603            assert!(res.is_err())
604        });
605    }
606
607    #[test]
608    fn access_before_fully_evaluating_module() {
609        let runtime = Runtime::new().unwrap();
610        let ctx = Context::full(&runtime).unwrap();
611
612        ctx.with(|ctx| {
613            let decl = Module::declare(
614                ctx,
615                "test",
616                r#"
617                async function async_res(){
618                    return await (async () => {
619                        return "OK"
620                    })()
621                };
622
623                export let res = await async_res()
624            "#,
625            )
626            .unwrap();
627
628            let (decl, promise) = decl.eval().unwrap();
629
630            let ns = decl.namespace().unwrap();
631            ns.get::<_, ()>("res").unwrap_err();
632
633            promise.finish::<()>().unwrap();
634
635            assert_eq!(ns.get::<_, std::string::String>("res").unwrap(), "OK");
636        });
637    }
638
639    #[test]
640    fn from_javascript() {
641        test_with(|ctx| {
642            let (module, promise) = Module::declare(
643                ctx.clone(),
644                "Test",
645                r#"
646            export var a = 2;
647            export function foo(){ return "bar"}
648            export class Baz{
649                quel = 3;
650                constructor(){
651                }
652            }
653                "#,
654            )
655            .unwrap()
656            .eval()
657            .unwrap();
658
659            promise.finish::<()>().unwrap();
660
661            assert_eq!(module.name::<StdString>().unwrap(), "Test");
662            let _ = module.meta().unwrap();
663
664            let ns = module.namespace().unwrap();
665
666            assert!(ns.contains_key("a").unwrap());
667            assert!(ns.contains_key("foo").unwrap());
668            assert!(ns.contains_key("Baz").unwrap());
669
670            assert_eq!(ns.get::<_, u32>("a").unwrap(), 2u32);
671        });
672    }
673}