Skip to main content

rquickjs_core/context/
ctx.rs

1#[cfg(feature = "futures")]
2use core::future::Future;
3
4use alloc::{boxed::Box, ffi::CString, vec::Vec};
5use core::{
6    any::Any,
7    ffi::CStr,
8    mem::{self, MaybeUninit},
9    ptr::NonNull,
10    result::Result as StdResult,
11};
12
13#[cfg(feature = "std")]
14use std::{fs, path::Path, string::String as StdString};
15
16#[cfg(feature = "futures")]
17use crate::AsyncContext;
18use crate::{
19    markers::Invariant,
20    qjs,
21    runtime::{opaque::Opaque, UserDataError, UserDataGuard},
22    Atom, Error, FromJs, Function, IntoJs, JsLifetime, Object, Promise, Result, String, Value,
23};
24
25use super::Context;
26
27/// Eval options.
28#[non_exhaustive]
29pub struct EvalOptions {
30    /// Global code.
31    pub global: bool,
32    /// Force 'strict' mode.
33    pub strict: bool,
34    /// Don't include the stack frames before this eval in the Error() backtraces.
35    pub backtrace_barrier: bool,
36    /// Support top-level-await.
37    pub promise: bool,
38    /// Filename. Ignored when calling eval_file_*.
39    #[cfg(feature = "std")]
40    pub filename: Option<StdString>,
41}
42
43impl EvalOptions {
44    fn to_flag(&self) -> i32 {
45        let mut flag = if self.global {
46            qjs::JS_EVAL_TYPE_GLOBAL
47        } else {
48            qjs::JS_EVAL_TYPE_MODULE
49        };
50
51        if self.strict {
52            flag |= qjs::JS_EVAL_FLAG_STRICT;
53        }
54
55        if self.backtrace_barrier {
56            flag |= qjs::JS_EVAL_FLAG_BACKTRACE_BARRIER;
57        }
58
59        if self.promise {
60            flag |= qjs::JS_EVAL_FLAG_ASYNC;
61        }
62
63        flag as i32
64    }
65}
66
67impl Default for EvalOptions {
68    fn default() -> Self {
69        EvalOptions {
70            global: true,
71            strict: true,
72            backtrace_barrier: false,
73            promise: false,
74            #[cfg(feature = "std")]
75            filename: None,
76        }
77    }
78}
79
80/// Context in use, passed to [`Context::with`].
81#[derive(Debug)]
82pub struct Ctx<'js> {
83    ctx: NonNull<qjs::JSContext>,
84    _marker: Invariant<'js>,
85}
86
87impl<'js> Clone for Ctx<'js> {
88    fn clone(&self) -> Self {
89        unsafe { qjs::JS_DupContext(self.ctx.as_ptr()) };
90        Ctx {
91            ctx: self.ctx,
92            _marker: self._marker,
93        }
94    }
95}
96
97impl<'js> Drop for Ctx<'js> {
98    fn drop(&mut self) {
99        unsafe { qjs::JS_FreeContext(self.ctx.as_ptr()) };
100    }
101}
102
103unsafe impl Send for Ctx<'_> {}
104
105impl<'js> Ctx<'js> {
106    pub(crate) fn as_ptr(&self) -> *mut qjs::JSContext {
107        self.ctx.as_ptr()
108    }
109
110    pub(crate) unsafe fn from_ptr(ctx: *mut qjs::JSContext) -> Self {
111        unsafe { qjs::JS_DupContext(ctx) };
112        let ctx = NonNull::new_unchecked(ctx);
113        Ctx {
114            ctx,
115            _marker: Invariant::new(),
116        }
117    }
118
119    pub(crate) unsafe fn new(ctx: &'js Context) -> Self {
120        unsafe { qjs::JS_DupContext(ctx.0.ctx().as_ptr()) };
121        Ctx {
122            ctx: ctx.0.ctx(),
123            _marker: Invariant::new(),
124        }
125    }
126
127    #[cfg(feature = "futures")]
128    pub(crate) unsafe fn new_async(ctx: &'js AsyncContext) -> Self {
129        unsafe { qjs::JS_DupContext(ctx.0.ctx().as_ptr()) };
130        Ctx {
131            ctx: ctx.0.ctx(),
132            _marker: Invariant::new(),
133        }
134    }
135
136    pub(crate) unsafe fn eval_raw<S: Into<Vec<u8>>>(
137        &self,
138        source: S,
139        file_name: &CStr,
140        flag: i32,
141    ) -> Result<qjs::JSValue> {
142        let src = source.into();
143        let len = src.len();
144        let src = CString::new(src)?;
145
146        #[cfg(feature = "parallel")]
147        qjs::JS_UpdateStackTop(qjs::JS_GetRuntime(self.ctx.as_ptr()));
148
149        let val = qjs::JS_Eval(
150            self.ctx.as_ptr(),
151            src.as_ptr(),
152            len as _,
153            file_name.as_ptr(),
154            flag,
155        );
156        self.handle_exception(val)
157    }
158
159    /// Evaluate a script in global context.
160    pub fn eval<V: FromJs<'js>, S: Into<Vec<u8>>>(&self, source: S) -> Result<V> {
161        self.eval_with_options(source, Default::default())
162    }
163
164    /// Evaluate a script in global context with top level await support.
165    ///
166    /// This function always returns a promise which resolves to the result of the evaluated
167    /// expression.
168    pub fn eval_promise<S: Into<Vec<u8>>>(&self, source: S) -> Result<Promise<'js>> {
169        self.eval_with_options(
170            source,
171            EvalOptions {
172                promise: true,
173                ..Default::default()
174            },
175        )
176    }
177
178    /// Evaluate a script with the given options.
179    pub fn eval_with_options<V: FromJs<'js>, S: Into<Vec<u8>>>(
180        &self,
181        source: S,
182        options: EvalOptions,
183    ) -> Result<V> {
184        #[cfg(feature = "std")]
185        let file_name = {
186            if let Some(filename) = &options.filename {
187                &CString::new(filename.clone())?
188            } else {
189                c"eval_script"
190            }
191        };
192
193        #[cfg(not(feature = "std"))]
194        let file_name = c"eval_script";
195
196        V::from_js(self, unsafe {
197            let val = self.eval_raw(source, file_name, options.to_flag())?;
198            Value::from_js_value(self.clone(), val)
199        })
200    }
201
202    #[cfg(feature = "std")]
203    /// Evaluate a script directly from a file.
204    pub fn eval_file<V: FromJs<'js>, P: AsRef<Path>>(&self, path: P) -> Result<V> {
205        self.eval_file_with_options(path, Default::default())
206    }
207
208    #[cfg(feature = "std")]
209    pub fn eval_file_with_options<V: FromJs<'js>, P: AsRef<Path>>(
210        &self,
211        path: P,
212        options: EvalOptions,
213    ) -> Result<V> {
214        let buffer = fs::read(path.as_ref())?;
215        let file_name = CString::new(
216            path.as_ref()
217                .file_name()
218                .unwrap()
219                .to_string_lossy()
220                .into_owned(),
221        )?;
222
223        V::from_js(self, unsafe {
224            let val = self.eval_raw(buffer, file_name.as_c_str(), options.to_flag())?;
225            Value::from_js_value(self.clone(), val)
226        })
227    }
228
229    /// Returns the global object of this context.
230    pub fn globals(&self) -> Object<'js> {
231        unsafe {
232            let v = qjs::JS_GetGlobalObject(self.ctx.as_ptr());
233            Object::from_js_value(self.clone(), v)
234        }
235    }
236
237    /// Returns the last raised JavaScript exception, if there is no exception the JavaScript value `null` is returned.
238    ///
239    /// # Usage
240    /// ```
241    /// # use rquickjs::{Error, Context, Runtime};
242    /// # let rt = Runtime::new().unwrap();
243    /// # let ctx = Context::full(&rt).unwrap();
244    /// # ctx.with(|ctx|{
245    /// if let Err(Error::Exception) = ctx.eval::<(),_>("throw 3"){
246    ///     assert_eq!(ctx.catch().as_int(),Some(3));
247    /// # }else{
248    /// #    panic!()
249    /// }
250    /// # });
251    /// ```
252    pub fn catch(&self) -> Value<'js> {
253        unsafe {
254            let v = qjs::JS_GetException(self.ctx.as_ptr());
255            Value::from_js_value(self.clone(), v)
256        }
257    }
258
259    /// Returns true if there is a pending JavaScript exception.
260    pub fn has_exception(&self) -> bool {
261        unsafe { qjs::JS_HasException(self.ctx.as_ptr()) }
262    }
263
264    /// Throws a JavaScript value as a new exception.
265    /// Always returns `Error::Exception`;
266    pub fn throw(&self, value: Value<'js>) -> Error {
267        unsafe {
268            let v = value.into_js_value();
269            qjs::JS_Throw(self.ctx.as_ptr(), v);
270        }
271        Error::Exception
272    }
273
274    /// Parse json into a JavaScript value.
275    pub fn json_parse<S>(&self, json: S) -> Result<Value<'js>>
276    where
277        S: Into<Vec<u8>>,
278    {
279        let src = json.into();
280        let len = src.len();
281        let src = CString::new(src)?;
282        unsafe {
283            let name = b"<input>\0";
284            let v = qjs::JS_ParseJSON(
285                self.as_ptr(),
286                src.as_ptr().cast(),
287                len.try_into().expect(qjs::SIZE_T_ERROR),
288                name.as_ptr().cast(),
289            );
290            self.handle_exception(v)?;
291            Ok(Value::from_js_value(self.clone(), v))
292        }
293    }
294
295    /// Stringify a JavaScript value into its JSON representation
296    pub fn json_stringify<V>(&self, value: V) -> Result<Option<String<'js>>>
297    where
298        V: IntoJs<'js>,
299    {
300        self.json_stringify_inner(&value.into_js(self)?, qjs::JS_UNDEFINED, qjs::JS_UNDEFINED)
301    }
302
303    /// Stringify a JavaScript value into its JSON representation with a possible replacer.
304    ///
305    /// The replacer is the same as the replacer argument for `JSON.stringify`.
306    /// It is is a function that alters the behavior of the stringification process.
307    pub fn json_stringify_replacer<V, R>(
308        &self,
309        value: V,
310        replacer: R,
311    ) -> Result<Option<String<'js>>>
312    where
313        V: IntoJs<'js>,
314        R: IntoJs<'js>,
315    {
316        let replacer = replacer.into_js(self)?;
317
318        self.json_stringify_inner(
319            &value.into_js(self)?,
320            replacer.as_js_value(),
321            qjs::JS_UNDEFINED,
322        )
323    }
324
325    /// Stringify a JavaScript value into its JSON representation with a possible replacer and
326    /// spaces
327    ///
328    /// The replacer is the same as the replacer argument for `JSON.stringify`.
329    /// It is is a function that alters the behavior of the stringification process.
330    ///
331    /// Space is either a number or a string which is used to insert whitespace into the output
332    /// string for readability purposes. This behaves the same as the space argument for
333    /// `JSON.stringify`.
334    pub fn json_stringify_replacer_space<V, R, S>(
335        &self,
336        value: V,
337        replacer: R,
338        space: S,
339    ) -> Result<Option<String<'js>>>
340    where
341        V: IntoJs<'js>,
342        R: IntoJs<'js>,
343        S: IntoJs<'js>,
344    {
345        let replacer = replacer.into_js(self)?;
346        let space = space.into_js(self)?;
347
348        self.json_stringify_inner(
349            &value.into_js(self)?,
350            replacer.as_js_value(),
351            space.as_js_value(),
352        )
353    }
354
355    // Inner non-generic version of json stringify>
356    fn json_stringify_inner(
357        &self,
358        value: &Value<'js>,
359        replacer: qjs::JSValueConst,
360        space: qjs::JSValueConst,
361    ) -> Result<Option<String<'js>>> {
362        unsafe {
363            let res = qjs::JS_JSONStringify(self.as_ptr(), value.as_js_value(), replacer, space);
364            self.handle_exception(res)?;
365            let v = Value::from_js_value(self.clone(), res);
366            if v.is_undefined() {
367                Ok(None)
368            } else {
369                let v = v.into_string().expect(
370                    "JS_JSONStringify did not return either an exception, undefined, or a string",
371                );
372                Ok(Some(v))
373            }
374        }
375    }
376
377    /// Creates javascipt promise along with its reject and resolve functions.
378    pub fn promise(&self) -> Result<(Promise<'js>, Function<'js>, Function<'js>)> {
379        let mut funcs = mem::MaybeUninit::<[qjs::JSValue; 2]>::uninit();
380
381        Ok(unsafe {
382            let promise = self.handle_exception(qjs::JS_NewPromiseCapability(
383                self.ctx.as_ptr(),
384                funcs.as_mut_ptr() as _,
385            ))?;
386            let [resolve, reject] = funcs.assume_init();
387            (
388                Promise::from_js_value(self.clone(), promise),
389                Function::from_js_value(self.clone(), resolve),
390                Function::from_js_value(self.clone(), reject),
391            )
392        })
393    }
394
395    /// Executes a quickjs job.
396    ///
397    /// Returns wether a job was actually executed.
398    /// If this function returned false, no job was pending.
399    pub fn execute_pending_job(&self) -> bool {
400        let mut ptr = MaybeUninit::<*mut qjs::JSContext>::uninit();
401        let rt = unsafe { qjs::JS_GetRuntime(self.ctx.as_ptr()) };
402        let res = unsafe { qjs::JS_ExecutePendingJob(rt, ptr.as_mut_ptr()) };
403        res != 0
404    }
405
406    pub(crate) unsafe fn get_opaque(&self) -> &Opaque<'js> {
407        Opaque::from_runtime_ptr(qjs::JS_GetRuntime(self.ctx.as_ptr()))
408    }
409
410    /// Spawn future using configured async runtime
411    #[cfg(feature = "futures")]
412    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
413    pub fn spawn<F>(&self, future: F)
414    where
415        F: Future<Output = ()> + 'js,
416    {
417        unsafe { self.get_opaque().push(future) }
418    }
419
420    /// Create a new `Ctx` from a pointer to the context and a invariant lifetime.
421    ///
422    /// # Safety
423    /// User must ensure that a lock was acquired over the runtime and that invariant is a unique
424    /// lifetime which can't be coerced to a lifetime outside the scope of the lock of to the
425    /// lifetime of another runtime.
426    pub unsafe fn from_raw_invariant(ctx: NonNull<qjs::JSContext>, inv: Invariant<'js>) -> Self {
427        unsafe { qjs::JS_DupContext(ctx.as_ptr()) };
428        Ctx { ctx, _marker: inv }
429    }
430
431    /// Create a new `Ctx` from a pointer to the context and a invariant lifetime.
432    ///
433    /// # Safety
434    /// User must ensure that a lock was acquired over the runtime and that invariant is a unique
435    /// lifetime which can't be coerced to a lifetime outside the scope of the lock of to the
436    /// lifetime of another runtime.
437    pub unsafe fn from_raw(ctx: NonNull<qjs::JSContext>) -> Self {
438        unsafe { qjs::JS_DupContext(ctx.as_ptr()) };
439        Ctx {
440            ctx,
441            _marker: Invariant::new(),
442        }
443    }
444
445    /// Returns the name of the current module or script that is running.
446    ///
447    /// It called from a javascript callback it will return the current running javascript script
448    /// name.
449    /// Otherwise it will return none.
450    pub fn script_or_module_name(&self, stack_level: isize) -> Option<Atom<'js>> {
451        let stack_level = core::ffi::c_int::try_from(stack_level).unwrap();
452        let atom = unsafe { qjs::JS_GetScriptOrModuleName(self.as_ptr(), stack_level) };
453        #[allow(clippy::useless_conversion)] //needed for multi platform binding support
454        if qjs::__JS_ATOM_NULL == atom.try_into().unwrap() {
455            unsafe { qjs::JS_FreeAtom(self.as_ptr(), atom) };
456            return None;
457        }
458        unsafe { Some(Atom::from_atom_val(self.clone(), atom)) }
459    }
460
461    /// Runs the quickjs garbage collector for a cycle.
462    ///
463    /// Quickjs uses reference counting with a collection cycle for cyclic references.
464    /// This runs the cyclic reference collector cycle, types which are not part of a reference cycle
465    /// will be freed the momement their reference count becomes zero.
466    pub fn run_gc(&self) {
467        unsafe { qjs::JS_RunGC(qjs::JS_GetRuntime(self.ctx.as_ptr())) }
468    }
469
470    /// Store a type in the runtime which can be retrieved later with `Ctx::userdata`.
471    ///
472    /// Returns the value from the argument if the userdata is currently being accessed and
473    /// insertion is not possible.
474    /// Otherwise returns the exising value for this type if it existed.
475    pub fn store_userdata<U>(&self, data: U) -> StdResult<Option<Box<U>>, UserDataError<U>>
476    where
477        U: JsLifetime<'js>,
478        U::Changed<'static>: Any,
479    {
480        unsafe { self.get_opaque().insert_userdata(data) }
481    }
482
483    /// Remove the userdata of the given type from the userdata storage.
484    ///
485    /// Returns Err(()) if the userdata is currently being accessed and removing isn't possible.
486    /// Returns Ok(None) if userdata of the given type wasn't inserted.
487    pub fn remove_userdata<U>(&self) -> StdResult<Option<Box<U>>, UserDataError<()>>
488    where
489        U: JsLifetime<'js>,
490        U::Changed<'static>: Any,
491    {
492        unsafe { self.get_opaque().remove_userdata() }
493    }
494
495    /// Retrieves a borrow to the userdata of the given type from the userdata storage.
496    ///
497    /// Returns None if userdata of the given type wasn't inserted.
498    pub fn userdata<U>(&self) -> Option<UserDataGuard<U>>
499    where
500        U: JsLifetime<'js>,
501        U::Changed<'static>: Any,
502    {
503        unsafe { self.get_opaque().get_userdata() }
504    }
505
506    /// Returns the pointer to the C library context.
507    pub fn as_raw(&self) -> NonNull<qjs::JSContext> {
508        self.ctx
509    }
510}
511
512#[cfg(test)]
513mod test {
514    use crate::{CatchResultExt, JsLifetime};
515
516    #[test]
517    fn exports() {
518        use crate::{context::intrinsic, Context, Function, Module, Promise, Runtime};
519
520        let runtime = Runtime::new().unwrap();
521        let ctx = Context::custom::<(intrinsic::Promise, intrinsic::Eval)>(&runtime).unwrap();
522        ctx.with(|ctx| {
523            let (module, promise) = Module::declare(ctx, "test", "export default async () => 1;")
524                .unwrap()
525                .eval()
526                .unwrap();
527            promise.finish::<()>().unwrap();
528            let func: Function = module.get("default").unwrap();
529            func.call::<(), Promise>(()).unwrap();
530        });
531    }
532
533    #[test]
534    fn eval() {
535        use crate::{Context, Runtime};
536
537        let runtime = Runtime::new().unwrap();
538        let ctx = Context::full(&runtime).unwrap();
539        ctx.with(|ctx| {
540            let res: String = ctx
541                .eval(
542                    r#"
543                    function test() {
544                        var foo = "bar";
545                        return foo;
546                    }
547
548                    test()
549                "#,
550                )
551                .unwrap();
552
553            assert_eq!("bar".to_string(), res);
554        })
555    }
556
557    #[test]
558    fn eval_minimal_test() {
559        use crate::{Context, Runtime};
560
561        let runtime = Runtime::new().unwrap();
562        let ctx = Context::full(&runtime).unwrap();
563        ctx.with(|ctx| {
564            let res: i32 = ctx.eval(" 1 + 1 ").unwrap();
565            assert_eq!(2, res);
566        })
567    }
568
569    #[test]
570    #[should_panic(expected = "foo is not defined")]
571    fn eval_with_sloppy_code() {
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            let _: String = ctx
578                .eval(
579                    r#"
580                    function test() {
581                        foo = "bar";
582                        return foo;
583                    }
584
585                    test()
586                "#,
587                )
588                .catch(&ctx)
589                .unwrap();
590        })
591    }
592
593    #[test]
594    fn eval_with_options_no_strict_sloppy_code() {
595        use crate::{context::EvalOptions, Context, Runtime};
596
597        let runtime = Runtime::new().unwrap();
598        let ctx = Context::full(&runtime).unwrap();
599        ctx.with(|ctx| {
600            let res: String = ctx
601                .eval_with_options(
602                    r#"
603                    function test() {
604                        foo = "bar";
605                        return foo;
606                    }
607
608                    test()
609                "#,
610                    EvalOptions {
611                        strict: false,
612                        ..Default::default()
613                    },
614                )
615                .unwrap();
616
617            assert_eq!("bar".to_string(), res);
618        })
619    }
620
621    #[test]
622    #[should_panic(expected = "foo is not defined")]
623    fn eval_with_options_strict_sloppy_code() {
624        use crate::{context::EvalOptions, CatchResultExt, Context, Runtime};
625
626        let runtime = Runtime::new().unwrap();
627        let ctx = Context::full(&runtime).unwrap();
628        ctx.with(|ctx| {
629            let _: String = ctx
630                .eval_with_options(
631                    r#"
632                    function test() {
633                        foo = "bar";
634                        return foo;
635                    }
636
637                    test()
638                "#,
639                    EvalOptions {
640                        strict: true,
641                        ..Default::default()
642                    },
643                )
644                .catch(&ctx)
645                .unwrap();
646        })
647    }
648
649    #[test]
650    fn json_parse() {
651        use crate::{Array, Context, Object, Runtime};
652
653        let runtime = Runtime::new().unwrap();
654        let ctx = Context::full(&runtime).unwrap();
655        ctx.with(|ctx| {
656            let v = ctx
657                .json_parse(r#"{ "a": { "b": 1, "c": true }, "d": [0,"foo"] }"#)
658                .unwrap();
659            let obj = v.into_object().unwrap();
660            let inner_obj: Object = obj.get("a").unwrap();
661            assert_eq!(inner_obj.get::<_, i32>("b").unwrap(), 1);
662            assert!(inner_obj.get::<_, bool>("c").unwrap());
663            let inner_array: Array = obj.get("d").unwrap();
664            assert_eq!(inner_array.get::<i32>(0).unwrap(), 0);
665            assert_eq!(inner_array.get::<String>(1).unwrap(), "foo".to_string());
666        })
667    }
668
669    #[test]
670    fn json_stringify() {
671        use crate::{Array, Context, Object, Runtime};
672
673        let runtime = Runtime::new().unwrap();
674        let ctx = Context::full(&runtime).unwrap();
675        ctx.with(|ctx| {
676            let obj_inner = Object::new(ctx.clone()).unwrap();
677            obj_inner.set("b", 1).unwrap();
678            obj_inner.set("c", true).unwrap();
679
680            let array_inner = Array::new(ctx.clone()).unwrap();
681            array_inner.set(0, 0).unwrap();
682            array_inner.set(1, "foo").unwrap();
683
684            let obj = Object::new(ctx.clone()).unwrap();
685            obj.set("a", obj_inner).unwrap();
686            obj.set("d", array_inner).unwrap();
687
688            let str = ctx
689                .json_stringify(obj)
690                .unwrap()
691                .unwrap()
692                .to_string()
693                .unwrap();
694
695            assert_eq!(str, r#"{"a":{"b":1,"c":true},"d":[0,"foo"]}"#);
696        })
697    }
698
699    #[test]
700    fn userdata() {
701        use crate::{Context, Function, Runtime};
702
703        pub struct MyUserData<'js> {
704            base: Function<'js>,
705        }
706
707        unsafe impl<'js> JsLifetime<'js> for MyUserData<'js> {
708            type Changed<'to> = MyUserData<'to>;
709        }
710
711        let rt = Runtime::new().unwrap();
712        let ctx = Context::full(&rt).unwrap();
713
714        ctx.with(|ctx| {
715            let func = ctx.eval("() => 42").catch(&ctx).unwrap();
716            ctx.store_userdata(MyUserData { base: func }).unwrap();
717        });
718
719        ctx.with(|ctx| {
720            let userdata = ctx.userdata::<MyUserData>().unwrap();
721
722            assert!(ctx.remove_userdata::<MyUserData>().is_err());
723
724            let r: usize = userdata.base.call(()).unwrap();
725            assert_eq!(r, 42)
726        });
727
728        ctx.with(|ctx| {
729            ctx.remove_userdata::<MyUserData>().unwrap().unwrap();
730        })
731    }
732}