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