rquickjs_core/context/
ctx.rs

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