quickjs_rusty/context/
context.rs

1#![allow(missing_docs)]
2
3use std::{
4    convert::TryFrom,
5    ffi::{c_char, c_int, c_void},
6    sync::Mutex,
7};
8
9use libquickjs_ng_sys::{self as q, JSContext};
10
11use crate::callback::*;
12use crate::console::ConsoleBackend;
13use crate::errors::*;
14use crate::module_loader::*;
15use crate::utils::{create_string, ensure_no_excpetion, get_exception, make_cstring};
16use crate::value::*;
17
18use super::ContextBuilder;
19
20/// Context is a wrapper around a QuickJS Javascript context.
21/// It is the primary way to interact with the runtime.
22///
23/// For each `Context` instance a new instance of QuickJS
24/// runtime is created. It means that it is safe to use
25/// different contexts in different threads, but each
26/// `Context` instance must be used only from a single thread.
27pub struct Context {
28    runtime: *mut q::JSRuntime,
29    pub(crate) context: *mut q::JSContext,
30    /// Stores callback closures and quickjs data pointers.
31    /// This array is write-only and only exists to ensure the lifetime of
32    /// the closure.
33    // A Mutex is used over a RefCell because it needs to be unwind-safe.
34    callbacks: Mutex<Vec<(Box<WrappedCallback>, Box<q::JSValue>)>>,
35    module_loader: Mutex<Option<Box<ModuleLoader>>>,
36}
37
38impl Drop for Context {
39    fn drop(&mut self) {
40        unsafe {
41            q::JS_FreeContext(self.context);
42            q::JS_FreeRuntime(self.runtime);
43
44            // Drop the module loader.
45            let _ = self.module_loader.lock().unwrap().take();
46        }
47    }
48}
49
50impl Context {
51    /// Create a `ContextBuilder` that allows customization of JS Runtime settings.
52    ///
53    /// For details, see the methods on `ContextBuilder`.
54    ///
55    /// ```rust
56    /// let _context = quickjs_rusty::Context::builder()
57    ///     .memory_limit(100_000)
58    ///     .build()
59    ///     .unwrap();
60    /// ```
61    pub fn builder() -> ContextBuilder {
62        ContextBuilder::new()
63    }
64
65    /// Initialize a wrapper by creating a JSRuntime and JSContext.
66    pub fn new(memory_limit: Option<usize>) -> Result<Self, ContextError> {
67        let runtime = unsafe { q::JS_NewRuntime() };
68        if runtime.is_null() {
69            return Err(ContextError::RuntimeCreationFailed);
70        }
71
72        // Configure memory limit if specified.
73        if let Some(limit) = memory_limit {
74            unsafe {
75                q::JS_SetMemoryLimit(runtime, limit as _);
76            }
77        }
78
79        let context = unsafe { q::JS_NewContext(runtime) };
80        if context.is_null() {
81            unsafe {
82                q::JS_FreeRuntime(runtime);
83            }
84            return Err(ContextError::ContextCreationFailed);
85        }
86
87        // Initialize the promise resolver helper code.
88        // This code is needed by Self::resolve_value
89        let wrapper = Self {
90            runtime,
91            context,
92            callbacks: Mutex::new(Vec::new()),
93            module_loader: Mutex::new(None),
94        };
95
96        Ok(wrapper)
97    }
98
99    // See console standard: https://console.spec.whatwg.org
100    pub fn set_console(&self, backend: Box<dyn ConsoleBackend>) -> Result<(), ExecutionError> {
101        use crate::console::Level;
102
103        self.add_callback("__console_write", move |args: Arguments| {
104            let mut args = args.into_vec();
105
106            if args.len() > 1 {
107                let level_raw = args.remove(0);
108
109                let level_opt = level_raw.to_string().ok().and_then(|v| match v.as_str() {
110                    "trace" => Some(Level::Trace),
111                    "debug" => Some(Level::Debug),
112                    "log" => Some(Level::Log),
113                    "info" => Some(Level::Info),
114                    "warn" => Some(Level::Warn),
115                    "error" => Some(Level::Error),
116                    _ => None,
117                });
118
119                if let Some(level) = level_opt {
120                    backend.log(level, args);
121                }
122            }
123        })?;
124
125        self.eval(
126            r#"
127            globalThis.console = {
128                trace: (...args) => {
129                    globalThis.__console_write("trace", ...args);
130                },
131                debug: (...args) => {
132                    globalThis.__console_write("debug", ...args);
133                },
134                log: (...args) => {
135                    globalThis.__console_write("log", ...args);
136                },
137                info: (...args) => {
138                    globalThis.__console_write("info", ...args);
139                },
140                warn: (...args) => {
141                    globalThis.__console_write("warn", ...args);
142                },
143                error: (...args) => {
144                    globalThis.__console_write("error", ...args);
145                },
146            };
147        "#,
148            false,
149        )?;
150
151        Ok(())
152    }
153
154    /// Reset the Javascript engine.
155    ///
156    /// All state and callbacks will be removed.
157    pub fn reset(self) -> Result<Self, ContextError> {
158        unsafe {
159            q::JS_FreeContext(self.context);
160        };
161        self.callbacks.lock().unwrap().clear();
162        let context = unsafe { q::JS_NewContext(self.runtime) };
163        if context.is_null() {
164            return Err(ContextError::ContextCreationFailed);
165        }
166
167        let mut s = self;
168        s.context = context;
169        Ok(s)
170    }
171
172    // Get raw pointer to the underlying QuickJS context.
173    pub unsafe fn context_raw(&self) -> *mut q::JSContext {
174        self.context
175    }
176
177    /// Get the global object.
178    pub fn global(&self) -> Result<OwnedJsObject, ExecutionError> {
179        let global_raw = unsafe { q::JS_GetGlobalObject(self.context) };
180        let global_ref = OwnedJsValue::new(self.context, global_raw);
181        let global = global_ref.try_into_object()?;
182        Ok(global)
183    }
184
185    /// Set a global variable.
186    ///
187    /// ```rust
188    /// use quickjs_rusty::Context;
189    /// let context = Context::builder().build().unwrap();
190    ///
191    /// context.set_global("someGlobalVariable", 42).unwrap();
192    /// let value = context.eval_as::<i32>("someGlobalVariable").unwrap();
193    /// assert_eq!(
194    ///     value,
195    ///     42,
196    /// );
197    /// ```
198    pub fn set_global<T>(&self, name: &str, value: T) -> Result<(), ExecutionError>
199    where
200        T: ToOwnedJsValue,
201    {
202        let global = self.global()?;
203        global.set_property(name, (self.context, value).into())?;
204        Ok(())
205    }
206
207    /// Execute the pending job in the event loop.
208    pub fn execute_pending_job(&self) -> Result<(), ExecutionError> {
209        let mut pctx = Box::new(std::ptr::null_mut::<JSContext>());
210        unsafe {
211            loop {
212                // TODO: is it actually essential to lock the context here?
213                // let _handle = self.context_lock.lock();
214                let err = q::JS_ExecutePendingJob(self.runtime, pctx.as_mut());
215
216                if err <= 0 {
217                    if err < 0 {
218                        ensure_no_excpetion(*pctx)?
219                    }
220                    break;
221                }
222            }
223        }
224        Ok(())
225    }
226
227    /// Check if the given value is an exception, and return the exception if it is.
228    pub fn check_exception(&self, value: &OwnedJsValue) -> Result<(), ExecutionError> {
229        if value.is_exception() {
230            let err = get_exception(self.context)
231                .unwrap_or_else(|| ExecutionError::Internal("Unknown exception".to_string()));
232            Err(err)
233        } else {
234            Ok(())
235        }
236    }
237
238    /// If the given value is a promise, run the event loop until it is
239    /// resolved, and return the final value.
240    pub fn resolve_value(&self, value: OwnedJsValue) -> Result<OwnedJsValue, ExecutionError> {
241        if value.is_object() {
242            let obj = value.try_into_object()?;
243            if obj.is_promise()? {
244                self.eval(
245                    r#"
246                    // Values:
247                    //   - undefined: promise not finished
248                    //   - false: error ocurred, __promiseError is set.
249                    //   - true: finished, __promiseSuccess is set.
250                    var __promiseResult = 0;
251                    var __promiseValue = 0;
252
253                    var __resolvePromise = function(p) {
254                        p
255                            .then(value => {
256                                __promiseResult = true;
257                                __promiseValue = value;
258                            })
259                            .catch(e => {
260                                __promiseResult = false;
261                                __promiseValue = e;
262                            });
263                    }
264                "#,
265                    false,
266                )?;
267
268                let global = self.global()?;
269                let resolver = global
270                    .property_require("__resolvePromise")?
271                    .try_into_function()?;
272
273                // Call the resolver code that sets the result values once
274                // the promise resolves.
275                resolver.call(vec![obj.into_value()])?;
276
277                loop {
278                    let flag = unsafe {
279                        let wrapper_mut = self as *const Self as *mut Self;
280                        let ctx_mut = &mut (*wrapper_mut).context;
281                        q::JS_ExecutePendingJob(self.runtime, ctx_mut)
282                    };
283                    if flag < 0 {
284                        let e = get_exception(self.context).unwrap_or_else(|| {
285                            ExecutionError::Internal("Unknown exception".to_string())
286                        });
287                        return Err(e);
288                    }
289
290                    // Check if promise is finished.
291                    let res_val = global.property_require("__promiseResult")?;
292                    if res_val.is_bool() {
293                        let ok = res_val.to_bool()?;
294                        let value = global.property_require("__promiseValue")?;
295
296                        if ok {
297                            return self.resolve_value(value);
298                        } else {
299                            let err_msg = value.js_to_string()?;
300                            return Err(ExecutionError::Exception(OwnedJsValue::new(
301                                self.context,
302                                create_string(self.context, &err_msg).unwrap(),
303                            )));
304                        }
305                    }
306                }
307            } else {
308                Ok(obj.into_value())
309            }
310        } else {
311            Ok(value)
312        }
313    }
314
315    /// Evaluates Javascript code and returns the value of the final expression.
316    ///
317    /// resolve: Whether to resolve the returned value if it is a promise. See more details as follows.
318    ///
319    /// **Promises**:
320    /// If the evaluated code returns a Promise, the event loop
321    /// will be executed until the promise is finished. The final value of
322    /// the promise will be returned, or a `ExecutionError::Exception` if the
323    /// promise failed.
324    ///
325    /// ```rust
326    /// use quickjs_rusty::Context;
327    /// let context = Context::builder().build().unwrap();
328    ///
329    /// let value = context.eval(" 1 + 2 + 3 ", false).unwrap();
330    /// assert_eq!(
331    ///     value.to_int(),
332    ///     Ok(6),
333    /// );
334    ///
335    /// let value = context.eval(r#"
336    ///     function f() { return 55 * 3; }
337    ///     let y = f();
338    ///     var x = y.toString() + "!"
339    ///     x
340    /// "#, false).unwrap();
341    /// assert_eq!(
342    ///     value.to_string().unwrap(),
343    ///     "165!",
344    /// );
345    /// ```
346    pub fn eval(&self, code: &str, resolve: bool) -> Result<OwnedJsValue, ExecutionError> {
347        let filename = "script.js";
348        let filename_c = make_cstring(filename)?;
349        let code_c = make_cstring(code)?;
350
351        let value_raw = unsafe {
352            q::JS_Eval(
353                self.context,
354                code_c.as_ptr(),
355                code.len(),
356                filename_c.as_ptr(),
357                q::JS_EVAL_TYPE_GLOBAL as i32,
358            )
359        };
360        let value = OwnedJsValue::new(self.context, value_raw);
361
362        self.check_exception(&value)?;
363
364        if resolve {
365            self.resolve_value(value)
366        } else {
367            Ok(value)
368        }
369    }
370
371    /// Evaluates Javascript code and returns the value of the final expression
372    /// on module mode.
373    ///
374    /// resolve: Whether to resolve the returned value if it is a promise. See more details as follows.
375    ///
376    /// **Promises**:
377    /// If the evaluated code returns a Promise, the event loop
378    /// will be executed until the promise is finished. The final value of
379    /// the promise will be returned, or a `ExecutionError::Exception` if the
380    /// promise failed.
381    ///
382    /// **Returns**:
383    /// Return value will always be undefined on module mode.
384    ///
385    /// ```ignore
386    /// use quickjs_rusty::Context;
387    /// let context = Context::builder().build().unwrap();
388    ///
389    /// let value = context.eval_module("import {foo} from 'bar'; foo();", false).unwrap();
390    /// ```
391    pub fn eval_module(&self, code: &str, resolve: bool) -> Result<OwnedJsValue, ExecutionError> {
392        let filename = "module.js";
393        let filename_c = make_cstring(filename)?;
394        let code_c = make_cstring(code)?;
395
396        let value_raw = unsafe {
397            q::JS_Eval(
398                self.context,
399                code_c.as_ptr(),
400                code.len(),
401                filename_c.as_ptr(),
402                q::JS_EVAL_TYPE_MODULE as i32,
403            )
404        };
405        let value = OwnedJsValue::new(self.context, value_raw);
406
407        self.check_exception(&value)?;
408
409        if resolve {
410            self.resolve_value(value)
411        } else {
412            Ok(value)
413        }
414    }
415
416    /// Evaluates Javascript code and returns the value of the final expression
417    /// as a Rust type.
418    ///
419    /// **Promises**:
420    /// If the evaluated code returns a Promise, the event loop
421    /// will be executed until the promise is finished. The final value of
422    /// the promise will be returned, or a `ExecutionError::Exception` if the
423    /// promise failed.
424    ///
425    /// ```rust
426    /// use quickjs_rusty::{Context};
427    /// let context = Context::builder().build().unwrap();
428    ///
429    /// let res = context.eval_as::<bool>(" 100 > 10 ");
430    /// assert_eq!(
431    ///     res,
432    ///     Ok(true),
433    /// );
434    ///
435    /// let value: i32 = context.eval_as(" 10 + 10 ").unwrap();
436    /// assert_eq!(
437    ///     value,
438    ///     20,
439    /// );
440    /// ```
441    pub fn eval_as<R>(&self, code: &str) -> Result<R, ExecutionError>
442    where
443        R: TryFrom<OwnedJsValue>,
444        R::Error: Into<ValueError>,
445    {
446        let value = self.eval(code, true)?;
447        let ret = R::try_from(value).map_err(|e| e.into())?;
448        Ok(ret)
449    }
450
451    /// Evaluates Javascript code and returns the value of the final expression
452    /// on module mode.
453    ///
454    /// **Promises**:
455    /// If the evaluated code returns a Promise, the event loop
456    /// will be executed until the promise is finished. The final value of
457    /// the promise will be returned, or a `ExecutionError::Exception` if the
458    /// promise failed.
459    ///
460    /// ```ignore
461    /// use quickjs_rusty::Context;
462    /// let context = Context::builder().build().unwrap();
463    ///
464    /// let value = context.run_module("./module");
465    /// ```
466    pub fn run_module(&self, filename: &str) -> Result<OwnedJsPromise, ExecutionError> {
467        let filename_c = make_cstring(filename)?;
468
469        let ret = unsafe {
470            q::JS_LoadModule(
471                self.context,
472                ".\0".as_ptr() as *const c_char,
473                filename_c.as_ptr(),
474            )
475        };
476
477        let ret = OwnedJsValue::new(self.context, ret);
478
479        ensure_no_excpetion(self.context)?;
480
481        if ret.is_promise() {
482            Ok(ret.try_into_promise()?)
483        } else {
484            Err(ExecutionError::Internal(
485                "Module did not return a promise".to_string(),
486            ))
487        }
488    }
489
490    /// register module loader function, giving module name as input and return module code as output.
491    pub fn set_module_loader(
492        &self,
493        module_loader_func: JSModuleLoaderFunc,
494        module_normalize: Option<JSModuleNormalizeFunc>,
495        opaque: *mut c_void,
496    ) {
497        let has_module_normalize = module_normalize.is_some();
498
499        let module_loader = ModuleLoader {
500            loader: module_loader_func,
501            normalize: module_normalize,
502            opaque,
503        };
504
505        let module_loader = Box::new(module_loader);
506        let module_loader_ptr = module_loader.as_ref() as *const _ as *mut c_void;
507
508        unsafe {
509            if has_module_normalize {
510                q::JS_SetModuleLoaderFunc(
511                    self.runtime,
512                    Some(js_module_normalize),
513                    Some(js_module_loader),
514                    module_loader_ptr,
515                );
516            } else {
517                q::JS_SetModuleLoaderFunc(
518                    self.runtime,
519                    None,
520                    Some(js_module_loader),
521                    module_loader_ptr,
522                );
523            }
524        }
525
526        *self.module_loader.lock().unwrap() = Some(module_loader);
527    }
528
529    /// Set the host promise rejection tracker.\
530    /// This function works not as expected, see more details in the example.
531    pub fn set_host_promise_rejection_tracker(
532        &self,
533        func: q::JSHostPromiseRejectionTracker,
534        opaque: *mut c_void,
535    ) {
536        unsafe {
537            q::JS_SetHostPromiseRejectionTracker(self.runtime, func, opaque);
538        }
539    }
540
541    /// Call a global function in the Javascript namespace.
542    ///
543    /// **Promises**:
544    /// If the evaluated code returns a Promise, the event loop
545    /// will be executed until the promise is finished. The final value of
546    /// the promise will be returned, or a `ExecutionError::Exception` if the
547    /// promise failed.
548    ///
549    /// ```rust
550    /// use quickjs_rusty::Context;
551    /// let context = Context::builder().build().unwrap();
552    ///
553    /// let res = context.call_function("encodeURIComponent", vec!["a=b"]).unwrap();
554    /// assert_eq!(
555    ///     res.to_string(),
556    ///     Ok("a%3Db".to_string()),
557    /// );
558    /// ```
559    pub fn call_function(
560        &self,
561        function_name: &str,
562        args: impl IntoIterator<Item = impl ToOwnedJsValue>,
563    ) -> Result<OwnedJsValue, ExecutionError> {
564        let qargs = args
565            .into_iter()
566            .map(|v| (self.context, v).into())
567            .collect::<Vec<OwnedJsValue>>();
568
569        let global = self.global()?;
570        let func = global
571            .property_require(function_name)?
572            .try_into_function()?;
573
574        let ret = func.call(qargs)?;
575        let v = self.resolve_value(ret)?;
576
577        Ok(v)
578    }
579
580    /// Create a JS function that is backed by a Rust function or closure.
581    /// Can be used to create a function and add it to an object.
582    ///
583    /// The callback must satisfy several requirements:
584    /// * accepts 0 - 5 arguments
585    /// * each argument must be convertible from a JsValue
586    /// * must return a value
587    /// * the return value must either:
588    ///   - be convertible to JsValue
589    ///   - be a Result<T, E> where T is convertible to JsValue
590    ///     if Err(e) is returned, a Javascript exception will be raised
591    ///
592    /// ```rust
593    /// use quickjs_rusty::{Context, OwnedJsValue};
594    /// use std::collections::HashMap;
595    ///
596    /// let context = Context::builder().build().unwrap();
597    ///
598    /// // Register an object.
599    /// let mut obj = HashMap::<String, OwnedJsValue>::new();
600    /// let func = context
601    ///         .create_callback(|a: i32, b: i32| a + b)
602    ///         .unwrap();
603    /// let func = OwnedJsValue::from((unsafe{context.context_raw()}, func));
604    /// // insert add function into the object.
605    /// obj.insert("add".to_string(), func);
606    /// // insert the myObj to global.
607    /// context.set_global("myObj", obj).unwrap();
608    /// // Now we try out the 'myObj.add' function via eval.    
609    /// let output = context.eval_as::<i32>("myObj.add( 3 , 4 ) ").unwrap();
610    /// assert_eq!(output, 7);
611    /// ```
612    pub fn create_callback<'a, F>(
613        &self,
614        callback: impl Callback<F> + 'static,
615    ) -> Result<JsFunction, ExecutionError> {
616        let argcount = callback.argument_count() as i32;
617
618        let context = self.context;
619        let wrapper = move |argc: c_int, argv: *mut q::JSValue| -> q::JSValue {
620            match exec_callback(context, argc, argv, &callback) {
621                Ok(value) => value,
622                // TODO: better error reporting.
623                Err(e) => {
624                    let js_exception_value = match e {
625                        ExecutionError::Exception(e) => unsafe { e.extract() },
626                        other => create_string(context, other.to_string().as_str()).unwrap(),
627                    };
628                    unsafe {
629                        q::JS_Throw(context, js_exception_value);
630                    }
631
632                    unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_EXCEPTION, 0) }
633                }
634            }
635        };
636
637        let (pair, trampoline) = unsafe { build_closure_trampoline(wrapper) };
638        let data = (&*pair.1) as *const q::JSValue as *mut q::JSValue;
639        self.callbacks.lock().unwrap().push(pair);
640
641        let obj = unsafe {
642            let f = q::JS_NewCFunctionData(self.context, trampoline, argcount, 0, 1, data);
643            OwnedJsValue::new(self.context, f)
644        };
645
646        let f = obj.try_into_function()?;
647        Ok(f)
648    }
649
650    /// Add a global JS function that is backed by a Rust function or closure.
651    ///
652    /// The callback must satisfy several requirements:
653    /// * accepts 0 - 5 arguments
654    /// * each argument must be convertible from a JsValue
655    /// * must return a value
656    /// * the return value must either:
657    ///   - be convertible to JsValue
658    ///   - be a Result<T, E> where T is convertible to JsValue
659    ///     if Err(e) is returned, a Javascript exception will be raised
660    ///
661    /// ```rust
662    /// use quickjs_rusty::Context;
663    /// let context = Context::builder().build().unwrap();
664    ///
665    /// // Register a closue as a callback under the "add" name.
666    /// // The 'add' function can now be called from Javascript code.
667    /// context.add_callback("add", |a: i32, b: i32| { a + b }).unwrap();
668    ///
669    /// // Now we try out the 'add' function via eval.
670    /// let output = context.eval_as::<i32>(" add( 3 , 4 ) ").unwrap();
671    /// assert_eq!(
672    ///     output,
673    ///     7,
674    /// );
675    /// ```
676    pub fn add_callback<'a, F>(
677        &self,
678        name: &str,
679        callback: impl Callback<F> + 'static,
680    ) -> Result<(), ExecutionError> {
681        let cfunc = self.create_callback(callback)?;
682        let global = self.global()?;
683        global.set_property(name, cfunc.into_value())?;
684        Ok(())
685    }
686
687    /// create a custom callback function
688    pub fn create_custom_callback(
689        &self,
690        callback: CustomCallback,
691    ) -> Result<JsFunction, ExecutionError> {
692        let context = self.context;
693        let wrapper = move |argc: c_int, argv: *mut q::JSValue| -> q::JSValue {
694            let result = std::panic::catch_unwind(|| {
695                let arg_slice = unsafe { std::slice::from_raw_parts(argv, argc as usize) };
696                match callback(context, arg_slice) {
697                    Ok(Some(value)) => value,
698                    Ok(None) => unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_UNDEFINED, 0) },
699                    // TODO: better error reporting.
700                    Err(e) => {
701                        // TODO: should create an Error type.
702                        let js_exception_value =
703                            create_string(context, e.to_string().as_str()).unwrap();
704
705                        unsafe {
706                            q::JS_Throw(context, js_exception_value);
707                        }
708
709                        unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_EXCEPTION, 0) }
710                    }
711                }
712            });
713
714            match result {
715                Ok(v) => v,
716                Err(_) => {
717                    // TODO: should create an Error type.
718                    let js_exception_value = create_string(context, "Callback panicked!").unwrap();
719
720                    unsafe {
721                        q::JS_Throw(context, js_exception_value);
722                    }
723
724                    unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_EXCEPTION, 0) }
725                }
726            }
727        };
728
729        let (pair, trampoline) = unsafe { build_closure_trampoline(wrapper) };
730        let data = (&*pair.1) as *const q::JSValue as *mut q::JSValue;
731        self.callbacks.lock().unwrap().push(pair);
732
733        let obj = unsafe {
734            let f = q::JS_NewCFunctionData(self.context, trampoline, 0, 0, 1, data);
735            OwnedJsValue::new(self.context, f)
736        };
737
738        let f = obj.try_into_function()?;
739        Ok(f)
740    }
741}