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