quickjs_runtime/quickjs_utils/
modules.rs

1//! utils for working with ES6 Modules
2
3use crate::jsutils::{JsError, Script};
4use crate::quickjs_utils::atoms;
5use crate::quickjs_utils::atoms::JSAtomRef;
6use crate::quickjsrealmadapter::QuickJsRealmAdapter;
7use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter;
8use crate::quickjsvalueadapter::QuickJsValueAdapter;
9use core::ptr;
10
11use libquickjs_sys as q;
12use std::ffi::{CStr, CString};
13
14/// compile a module, used for module loading
15/// # Safety
16/// please ensure the corresponding QuickJSContext is still valid
17pub unsafe fn compile_module(
18    context: *mut q::JSContext,
19    script: Script,
20) -> Result<QuickJsValueAdapter, JsError> {
21    let code_str = script.get_runnable_code();
22
23    let code_c = CString::new(code_str).ok().unwrap();
24    let filename_c = CString::new(script.get_path()).ok().unwrap();
25
26    let value_raw = q::JS_Eval(
27        context,
28        code_c.as_ptr(),
29        code_str.len() as _,
30        filename_c.as_ptr(),
31        (q::JS_EVAL_TYPE_MODULE | q::JS_EVAL_FLAG_COMPILE_ONLY) as i32,
32    );
33
34    // check for error
35    let ret = QuickJsValueAdapter::new(
36        context,
37        value_raw,
38        false,
39        true,
40        format!("compile_module result of {}", script.get_path()).as_str(),
41    );
42
43    log::trace!("compile module yielded a {}", ret.borrow_value().tag);
44
45    if ret.is_exception() {
46        let ex_opt = QuickJsRealmAdapter::get_exception(context);
47        if let Some(ex) = ex_opt {
48            Err(ex)
49        } else {
50            Err(JsError::new_str(
51                "compile_module failed and could not get exception",
52            ))
53        }
54    } else {
55        Ok(ret)
56    }
57}
58
59// get the ModuleDef obj from a JSValue, this is used for module loading
60pub fn get_module_def(value: &QuickJsValueAdapter) -> *mut q::JSModuleDef {
61    log::trace!("get_module_def");
62    assert!(value.is_module());
63    log::trace!("get_module_def / 2");
64    unsafe { value.borrow_value().u.ptr as *mut q::JSModuleDef }
65}
66
67#[allow(dead_code)]
68pub fn set_module_loader(q_js_rt: &QuickJsRuntimeAdapter) {
69    log::trace!("setting up module loader");
70
71    let module_normalize: q::JSModuleNormalizeFunc = Some(js_module_normalize);
72    let module_loader: q::JSModuleLoaderFunc = Some(js_module_loader);
73
74    let opaque = std::ptr::null_mut();
75
76    unsafe { q::JS_SetModuleLoaderFunc(q_js_rt.runtime, module_normalize, module_loader, opaque) }
77}
78
79/// detect if a script is module (contains import or export statements)
80pub fn detect_module(source: &str) -> bool {
81    // own impl since detectmodule in quickjs-ng is different since 0.7.0
82    // https://github.com/quickjs-ng/quickjs/issues/767
83
84    // Check for static `import` statements
85
86    #[cfg(feature = "quickjs-ng")]
87    {
88        for line in source.lines() {
89            let trimmed = line.trim();
90            if trimmed.starts_with("import ") && !trimmed.contains("(") {
91                return true;
92            }
93            if trimmed.starts_with("export ") {
94                return true;
95            }
96        }
97        false
98    }
99
100    #[cfg(feature = "bellard")]
101    {
102        let cstr =
103            CString::new(source).expect("could not create CString due to null term in source");
104        let res = unsafe { q::JS_DetectModule(cstr.as_ptr(), source.len() as _) };
105        //println!("res for {} = {}", source, res);
106        res != 0
107    }
108}
109
110/// create new Module (JSModuleDef struct) which can be populated with exports after (and from) the init_func
111/// # Safety
112/// Please ensure the context passed is still valid
113pub unsafe fn new_module(
114    ctx: *mut q::JSContext,
115    name: &str,
116    init_func: q::JSModuleInitFunc,
117) -> Result<*mut q::JSModuleDef, JsError> {
118    let name_cstr = CString::new(name).map_err(|_e| JsError::new_str("CString failed"))?;
119    Ok(q::JS_NewCModule(ctx, name_cstr.as_ptr(), init_func))
120}
121
122/// set an export in a JSModuleDef, this should be called AFTER the init_func(as passed to new_module()) is called
123/// please note that you always need to use this in combination with add_module_export()
124/// # Safety
125/// Please ensure the context passed is still valid
126pub unsafe fn set_module_export(
127    ctx: *mut q::JSContext,
128    module: *mut q::JSModuleDef,
129    export_name: &str,
130    js_val: QuickJsValueAdapter,
131) -> Result<(), JsError> {
132    let name_cstr = CString::new(export_name).map_err(|_e| JsError::new_str("CString failed"))?;
133    let res = q::JS_SetModuleExport(
134        ctx,
135        module,
136        name_cstr.as_ptr(),
137        js_val.clone_value_incr_rc(),
138    );
139    if res == 0 {
140        Ok(())
141    } else {
142        Err(JsError::new_str("JS_SetModuleExport failed"))
143    }
144}
145
146/// set an export in a JSModuleDef, this should be called BEFORE this init_func(as passed to new_module()) is called
147/// # Safety
148/// Please ensure the context passed is still valid
149pub unsafe fn add_module_export(
150    ctx: *mut q::JSContext,
151    module: *mut q::JSModuleDef,
152    export_name: &str,
153) -> Result<(), JsError> {
154    let name_cstr = CString::new(export_name).map_err(|_e| JsError::new_str("CString failed"))?;
155    let res = q::JS_AddModuleExport(ctx, module, name_cstr.as_ptr());
156    if res == 0 {
157        Ok(())
158    } else {
159        Err(JsError::new_str("JS_SetModuleExport failed"))
160    }
161}
162
163/// get the name of an JSModuleDef struct
164/// # Safety
165/// Please ensure the context passed is still valid
166pub unsafe fn get_module_name(
167    ctx: *mut q::JSContext,
168    module: *mut q::JSModuleDef,
169) -> Result<String, JsError> {
170    let atom_raw = q::JS_GetModuleName(ctx, module);
171    let atom_ref = JSAtomRef::new(ctx, atom_raw);
172    atoms::to_string(ctx, &atom_ref)
173}
174
175unsafe extern "C" fn js_module_normalize(
176    ctx: *mut q::JSContext,
177    module_base_name: *const ::std::os::raw::c_char,
178    module_name: *const ::std::os::raw::c_char,
179    _opaque: *mut ::std::os::raw::c_void,
180) -> *mut ::std::os::raw::c_char {
181    log::trace!("js_module_normalize called.");
182
183    let base_c = CStr::from_ptr(module_base_name);
184    let base_str = base_c
185        .to_str()
186        .expect("could not convert module_base_name to str");
187    let name_c = CStr::from_ptr(module_name);
188    let name_str = name_c
189        .to_str()
190        .expect("could not convert module_name to str");
191
192    log::trace!(
193        "js_module_normalize called. base: {}. name: {}",
194        base_str,
195        name_str
196    );
197
198    QuickJsRuntimeAdapter::do_with(|q_js_rt| {
199        let q_ctx = q_js_rt.get_quickjs_context(ctx);
200
201        if let Some(res) = q_js_rt.with_all_module_loaders(|loader| {
202            if let Some(normalized_path) = loader.normalize_path(q_ctx, base_str, name_str) {
203                let c_absolute_path = CString::new(normalized_path.as_str()).expect("fail");
204                Some(c_absolute_path.into_raw())
205            } else {
206                None
207            }
208        }) {
209            res
210        } else {
211            q_ctx.report_ex(format!("Module {name_str} was not found").as_str());
212            ptr::null_mut()
213        }
214    })
215}
216
217unsafe extern "C" fn js_module_loader(
218    ctx: *mut q::JSContext,
219    module_name_raw: *const ::std::os::raw::c_char,
220    _opaque: *mut ::std::os::raw::c_void,
221) -> *mut q::JSModuleDef {
222    log::trace!("js_module_loader called.");
223
224    let module_name_c = CStr::from_ptr(module_name_raw);
225    let module_name = module_name_c.to_str().expect("could not get module name");
226
227    log::trace!("js_module_loader called: {}", module_name);
228
229    QuickJsRuntimeAdapter::do_with(|q_js_rt| {
230        QuickJsRealmAdapter::with_context(ctx, |q_ctx| {
231            if let Some(res) = q_js_rt.with_all_module_loaders(|module_loader| {
232                if module_loader.has_module(q_ctx, module_name) {
233                    let mod_val_res = module_loader.load_module(q_ctx, module_name);
234                    return match mod_val_res {
235                        Ok(mod_val) => Some(mod_val),
236                        Err(e) => {
237                            let err =
238                                format!("Module load failed for {module_name} because of: {e}");
239                            log::error!("{}", err);
240                            q_ctx.report_ex(err.as_str());
241                            Some(std::ptr::null_mut())
242                        }
243                    };
244                }
245                None
246            }) {
247                res
248            } else {
249                std::ptr::null_mut()
250            }
251        })
252    })
253}
254
255#[cfg(test)]
256pub mod tests {
257    use crate::facades::tests::init_test_rt;
258    use crate::jsutils::Script;
259    use crate::quickjs_utils::modules::detect_module;
260    use crate::values::JsValueFacade;
261    use std::time::Duration;
262
263    #[test]
264    fn test_native_modules() {
265        let rt = init_test_rt();
266        let mres = rt.eval_module_sync(None, Script::new(
267            "test.mes",
268            "import {a, b, c} from 'greco://testmodule1';\nconsole.log('testmodule1.a = %s, testmodule1.b = %s, testmodule1.c = %s', a, b, c);",
269        ));
270        match mres {
271            Ok(_module_res) => {}
272            Err(e) => panic!("test_native_modules failed: {}", e),
273        }
274
275        let res_prom = rt.eval_sync(None, Script::new("test_mod_nat_async.es", "(import('greco://someMod').then((module) => {return {a: module.a, b: module.b, c: module.c};}));")).ok().unwrap();
276        assert!(res_prom.is_js_promise());
277
278        match res_prom {
279            JsValueFacade::JsPromise { cached_promise } => {
280                let res = cached_promise
281                    .get_promise_result_sync()
282                    .expect("prom timed out");
283                let obj = res.expect("prom failed");
284                assert!(obj.is_js_object());
285                match obj {
286                    JsValueFacade::JsObject { cached_object } => {
287                        let map = cached_object.get_object_sync().expect("esvf to map failed");
288                        let a = map.get("a").expect("obj did not have a");
289                        assert_eq!(a.get_i32(), 1234);
290                        let b = map.get("b").expect("obj did not have b");
291                        assert_eq!(b.get_i32(), 64834);
292                    }
293                    _ => {}
294                }
295            }
296            _ => {}
297        }
298    }
299
300    #[test]
301    fn test_detect() {
302        assert!(detect_module("import {} from 'foo.js';"));
303        assert!(detect_module("export function a(){};"));
304        assert!(!detect_module("//hi"));
305        assert!(!detect_module("let a = 1;"));
306        assert!(!detect_module("import('foo.js').then((a) = {});"));
307    }
308
309    #[test]
310    fn test_module_sandbox() {
311        log::info!("> test_module_sandbox");
312
313        let rt = init_test_rt();
314        rt.exe_rt_task_in_event_loop(|q_js_rt| {
315            let q_ctx = q_js_rt.get_main_realm();
316            let res = q_ctx.eval_module(Script::new(
317                "test1.mes",
318                "export const name = 'foobar';\nconsole.log('evalling module');",
319            ));
320
321            if res.is_err() {
322                panic!("parse module failed: {}", res.err().unwrap())
323            }
324            res.ok().expect("parse module failed");
325        });
326
327        rt.exe_rt_task_in_event_loop(|q_js_rt| {
328            let q_ctx = q_js_rt.get_main_realm();
329            let res = q_ctx.eval_module(Script::new(
330                "test2.mes",
331                "import {name} from 'test1.mes';\n\nconsole.log('imported name: ' + name);",
332            ));
333
334            if res.is_err() {
335                panic!("parse module2 failed: {}", res.err().unwrap())
336            }
337
338            res.ok().expect("parse module2 failed");
339        });
340
341        rt.exe_rt_task_in_event_loop(|q_js_rt| {
342            let q_ctx = q_js_rt.get_main_realm();
343            let res = q_ctx.eval_module(Script::new(
344                "test3.mes",
345                "import {name} from 'notfound.mes';\n\nconsole.log('imported name: ' + name);",
346            ));
347
348            assert!(res.is_err());
349            assert!(res
350                .err()
351                .unwrap()
352                .get_message()
353                .contains("Module notfound.mes was not found"));
354        });
355
356        rt.exe_rt_task_in_event_loop(|q_js_rt| {
357            let q_ctx = q_js_rt.get_main_realm();
358            let res = q_ctx.eval_module(Script::new(
359                "test4.mes",
360                "import {name} from 'invalid.mes';\n\nconsole.log('imported name: ' + name);",
361            ));
362
363            assert!(res.is_err());
364            assert!(res
365                .err()
366                .unwrap()
367                .get_message()
368                .contains("Module load failed for invalid.mes"));
369        });
370
371        rt.exe_rt_task_in_event_loop(|q_js_rt| {
372            let q_ctx = q_js_rt.get_main_realm();
373            let res = q_ctx.eval_module(Script::new(
374                "test2.mes",
375                "import {name} from 'test1.mes';\n\nconsole.log('imported name: ' + name);",
376            ));
377
378            if res.is_err() {
379                panic!("parse module2 failed: {}", res.err().unwrap())
380            }
381
382            res.ok().expect("parse module2 failed");
383        });
384
385        std::thread::sleep(Duration::from_secs(1));
386
387        log::info!("< test_module_sandbox");
388    }
389}