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