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#[non_exhaustive]
29pub struct EvalOptions {
30 pub global: bool,
32 pub strict: bool,
34 pub backtrace_barrier: bool,
36 pub promise: bool,
38 #[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#[derive(Debug)]
82pub struct Ctx<'js> {
83 ctx: NonNull<qjs::JSContext>,
84 _marker: Invariant<'js>,
85}
86
87impl<'js> Clone for Ctx<'js> {
88 fn clone(&self) -> Self {
89 unsafe { qjs::JS_DupContext(self.ctx.as_ptr()) };
90 Ctx {
91 ctx: self.ctx,
92 _marker: self._marker,
93 }
94 }
95}
96
97impl<'js> Drop for Ctx<'js> {
98 fn drop(&mut self) {
99 unsafe { qjs::JS_FreeContext(self.ctx.as_ptr()) };
100 }
101}
102
103unsafe impl Send for Ctx<'_> {}
104
105impl<'js> Ctx<'js> {
106 pub(crate) fn as_ptr(&self) -> *mut qjs::JSContext {
107 self.ctx.as_ptr()
108 }
109
110 pub(crate) unsafe fn from_ptr(ctx: *mut qjs::JSContext) -> Self {
111 unsafe { qjs::JS_DupContext(ctx) };
112 let ctx = NonNull::new_unchecked(ctx);
113 Ctx {
114 ctx,
115 _marker: Invariant::new(),
116 }
117 }
118
119 pub(crate) unsafe fn new(ctx: &'js Context) -> Self {
120 unsafe { qjs::JS_DupContext(ctx.0.ctx().as_ptr()) };
121 Ctx {
122 ctx: ctx.0.ctx(),
123 _marker: Invariant::new(),
124 }
125 }
126
127 #[cfg(feature = "futures")]
128 pub(crate) unsafe fn new_async(ctx: &'js AsyncContext) -> Self {
129 unsafe { qjs::JS_DupContext(ctx.0.ctx().as_ptr()) };
130 Ctx {
131 ctx: ctx.0.ctx(),
132 _marker: Invariant::new(),
133 }
134 }
135
136 pub(crate) unsafe fn eval_raw<S: Into<Vec<u8>>>(
137 &self,
138 source: S,
139 file_name: &CStr,
140 flag: i32,
141 ) -> Result<qjs::JSValue> {
142 let src = source.into();
143 let len = src.len();
144 let src = CString::new(src)?;
145
146 #[cfg(feature = "parallel")]
147 qjs::JS_UpdateStackTop(qjs::JS_GetRuntime(self.ctx.as_ptr()));
148
149 let val = qjs::JS_Eval(
150 self.ctx.as_ptr(),
151 src.as_ptr(),
152 len as _,
153 file_name.as_ptr(),
154 flag,
155 );
156 self.handle_exception(val)
157 }
158
159 pub fn eval<V: FromJs<'js>, S: Into<Vec<u8>>>(&self, source: S) -> Result<V> {
161 self.eval_with_options(source, Default::default())
162 }
163
164 pub fn eval_promise<S: Into<Vec<u8>>>(&self, source: S) -> Result<Promise<'js>> {
169 self.eval_with_options(
170 source,
171 EvalOptions {
172 promise: true,
173 ..Default::default()
174 },
175 )
176 }
177
178 pub fn eval_with_options<V: FromJs<'js>, S: Into<Vec<u8>>>(
180 &self,
181 source: S,
182 options: EvalOptions,
183 ) -> Result<V> {
184 #[cfg(feature = "std")]
185 let file_name = {
186 if let Some(filename) = &options.filename {
187 &CString::new(filename.clone())?
188 } else {
189 c"eval_script"
190 }
191 };
192
193 #[cfg(not(feature = "std"))]
194 let file_name = c"eval_script";
195
196 V::from_js(self, unsafe {
197 let val = self.eval_raw(source, file_name, options.to_flag())?;
198 Value::from_js_value(self.clone(), val)
199 })
200 }
201
202 #[cfg(feature = "std")]
203 pub fn eval_file<V: FromJs<'js>, P: AsRef<Path>>(&self, path: P) -> Result<V> {
205 self.eval_file_with_options(path, Default::default())
206 }
207
208 #[cfg(feature = "std")]
209 pub fn eval_file_with_options<V: FromJs<'js>, P: AsRef<Path>>(
210 &self,
211 path: P,
212 options: EvalOptions,
213 ) -> Result<V> {
214 let buffer = fs::read(path.as_ref())?;
215 let file_name = CString::new(
216 path.as_ref()
217 .file_name()
218 .unwrap()
219 .to_string_lossy()
220 .into_owned(),
221 )?;
222
223 V::from_js(self, unsafe {
224 let val = self.eval_raw(buffer, file_name.as_c_str(), options.to_flag())?;
225 Value::from_js_value(self.clone(), val)
226 })
227 }
228
229 pub fn globals(&self) -> Object<'js> {
231 unsafe {
232 let v = qjs::JS_GetGlobalObject(self.ctx.as_ptr());
233 Object::from_js_value(self.clone(), v)
234 }
235 }
236
237 pub fn catch(&self) -> Value<'js> {
253 unsafe {
254 let v = qjs::JS_GetException(self.ctx.as_ptr());
255 Value::from_js_value(self.clone(), v)
256 }
257 }
258
259 pub fn has_exception(&self) -> bool {
261 unsafe { qjs::JS_HasException(self.ctx.as_ptr()) }
262 }
263
264 pub fn throw(&self, value: Value<'js>) -> Error {
267 unsafe {
268 let v = value.into_js_value();
269 qjs::JS_Throw(self.ctx.as_ptr(), v);
270 }
271 Error::Exception
272 }
273
274 pub fn json_parse<S>(&self, json: S) -> Result<Value<'js>>
276 where
277 S: Into<Vec<u8>>,
278 {
279 let src = json.into();
280 let len = src.len();
281 let src = CString::new(src)?;
282 unsafe {
283 let name = b"<input>\0";
284 let v = qjs::JS_ParseJSON(
285 self.as_ptr(),
286 src.as_ptr().cast(),
287 len.try_into().expect(qjs::SIZE_T_ERROR),
288 name.as_ptr().cast(),
289 );
290 self.handle_exception(v)?;
291 Ok(Value::from_js_value(self.clone(), v))
292 }
293 }
294
295 pub fn json_stringify<V>(&self, value: V) -> Result<Option<String<'js>>>
297 where
298 V: IntoJs<'js>,
299 {
300 self.json_stringify_inner(&value.into_js(self)?, qjs::JS_UNDEFINED, qjs::JS_UNDEFINED)
301 }
302
303 pub fn json_stringify_replacer<V, R>(
308 &self,
309 value: V,
310 replacer: R,
311 ) -> Result<Option<String<'js>>>
312 where
313 V: IntoJs<'js>,
314 R: IntoJs<'js>,
315 {
316 let replacer = replacer.into_js(self)?;
317
318 self.json_stringify_inner(
319 &value.into_js(self)?,
320 replacer.as_js_value(),
321 qjs::JS_UNDEFINED,
322 )
323 }
324
325 pub fn json_stringify_replacer_space<V, R, S>(
335 &self,
336 value: V,
337 replacer: R,
338 space: S,
339 ) -> Result<Option<String<'js>>>
340 where
341 V: IntoJs<'js>,
342 R: IntoJs<'js>,
343 S: IntoJs<'js>,
344 {
345 let replacer = replacer.into_js(self)?;
346 let space = space.into_js(self)?;
347
348 self.json_stringify_inner(
349 &value.into_js(self)?,
350 replacer.as_js_value(),
351 space.as_js_value(),
352 )
353 }
354
355 fn json_stringify_inner(
357 &self,
358 value: &Value<'js>,
359 replacer: qjs::JSValueConst,
360 space: qjs::JSValueConst,
361 ) -> Result<Option<String<'js>>> {
362 unsafe {
363 let res = qjs::JS_JSONStringify(self.as_ptr(), value.as_js_value(), replacer, space);
364 self.handle_exception(res)?;
365 let v = Value::from_js_value(self.clone(), res);
366 if v.is_undefined() {
367 Ok(None)
368 } else {
369 let v = v.into_string().expect(
370 "JS_JSONStringify did not return either an exception, undefined, or a string",
371 );
372 Ok(Some(v))
373 }
374 }
375 }
376
377 pub fn promise(&self) -> Result<(Promise<'js>, Function<'js>, Function<'js>)> {
379 let mut funcs = mem::MaybeUninit::<[qjs::JSValue; 2]>::uninit();
380
381 Ok(unsafe {
382 let promise = self.handle_exception(qjs::JS_NewPromiseCapability(
383 self.ctx.as_ptr(),
384 funcs.as_mut_ptr() as _,
385 ))?;
386 let [resolve, reject] = funcs.assume_init();
387 (
388 Promise::from_js_value(self.clone(), promise),
389 Function::from_js_value(self.clone(), resolve),
390 Function::from_js_value(self.clone(), reject),
391 )
392 })
393 }
394
395 pub fn execute_pending_job(&self) -> bool {
400 let mut ptr = MaybeUninit::<*mut qjs::JSContext>::uninit();
401 let rt = unsafe { qjs::JS_GetRuntime(self.ctx.as_ptr()) };
402 let res = unsafe { qjs::JS_ExecutePendingJob(rt, ptr.as_mut_ptr()) };
403 if res < 0 {
404 let ctx = unsafe { Self::from_ptr(ptr.assume_init()) };
411 ctx.catch();
412 }
413 res != 0
414 }
415
416 pub(crate) unsafe fn get_opaque(&self) -> &Opaque<'js> {
417 Opaque::from_runtime_ptr(qjs::JS_GetRuntime(self.ctx.as_ptr()))
418 }
419
420 #[cfg(feature = "futures")]
422 #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
423 pub fn spawn<F>(&self, future: F)
424 where
425 F: Future<Output = ()> + 'js,
426 {
427 unsafe { self.get_opaque().push(future) }
428 }
429
430 pub unsafe fn from_raw_invariant(ctx: NonNull<qjs::JSContext>, inv: Invariant<'js>) -> Self {
437 unsafe { qjs::JS_DupContext(ctx.as_ptr()) };
438 Ctx { ctx, _marker: inv }
439 }
440
441 pub unsafe fn from_raw(ctx: NonNull<qjs::JSContext>) -> Self {
448 unsafe { qjs::JS_DupContext(ctx.as_ptr()) };
449 Ctx {
450 ctx,
451 _marker: Invariant::new(),
452 }
453 }
454
455 pub fn script_or_module_name(&self, stack_level: isize) -> Option<Atom<'js>> {
461 let stack_level = core::ffi::c_int::try_from(stack_level).unwrap();
462 let atom = unsafe { qjs::JS_GetScriptOrModuleName(self.as_ptr(), stack_level) };
463 #[allow(clippy::useless_conversion)] if qjs::__JS_ATOM_NULL == atom.try_into().unwrap() {
465 unsafe { qjs::JS_FreeAtom(self.as_ptr(), atom) };
466 return None;
467 }
468 unsafe { Some(Atom::from_atom_val(self.clone(), atom)) }
469 }
470
471 pub fn run_gc(&self) {
477 unsafe { qjs::JS_RunGC(qjs::JS_GetRuntime(self.ctx.as_ptr())) }
478 }
479
480 pub fn store_userdata<U>(&self, data: U) -> StdResult<Option<Box<U>>, UserDataError<U>>
486 where
487 U: JsLifetime<'js>,
488 U::Changed<'static>: Any,
489 {
490 unsafe { self.get_opaque().insert_userdata(data) }
491 }
492
493 pub fn remove_userdata<U>(&self) -> StdResult<Option<Box<U>>, UserDataError<()>>
498 where
499 U: JsLifetime<'js>,
500 U::Changed<'static>: Any,
501 {
502 unsafe { self.get_opaque().remove_userdata() }
503 }
504
505 pub fn userdata<U>(&self) -> Option<UserDataGuard<U>>
509 where
510 U: JsLifetime<'js>,
511 U::Changed<'static>: Any,
512 {
513 unsafe { self.get_opaque().get_userdata() }
514 }
515
516 pub fn as_raw(&self) -> NonNull<qjs::JSContext> {
518 self.ctx
519 }
520}
521
522#[cfg(test)]
523mod test {
524 use crate::{CatchResultExt, JsLifetime};
525
526 #[test]
527 fn exports() {
528 use crate::{context::intrinsic, Context, Function, Module, Promise, Runtime};
529
530 let runtime = Runtime::new().unwrap();
531 let ctx = Context::custom::<(intrinsic::Promise, intrinsic::Eval)>(&runtime).unwrap();
532 ctx.with(|ctx| {
533 let (module, promise) = Module::declare(ctx, "test", "export default async () => 1;")
534 .unwrap()
535 .eval()
536 .unwrap();
537 promise.finish::<()>().unwrap();
538 let func: Function = module.get("default").unwrap();
539 func.call::<(), Promise>(()).unwrap();
540 });
541 }
542
543 #[test]
544 fn execute_pending_job_clears_exception_from_failed_job() {
545 use crate::{Context, Runtime};
546
547 let runtime = Runtime::new().unwrap();
548 let ctx = Context::full(&runtime).unwrap();
549 ctx.with(|ctx| {
550 ctx.eval::<(), _>("queueMicrotask(() => { throw new Error('boom'); });")
552 .unwrap();
553
554 while ctx.execute_pending_job() {}
557
558 assert!(!ctx.has_exception());
559
560 let sum: i32 = ctx.eval("1 + 1").unwrap();
562 assert_eq!(sum, 2);
563 })
564 }
565
566 #[test]
567 fn eval() {
568 use crate::{Context, Runtime};
569
570 let runtime = Runtime::new().unwrap();
571 let ctx = Context::full(&runtime).unwrap();
572 ctx.with(|ctx| {
573 let res: String = ctx
574 .eval(
575 r#"
576 function test() {
577 var foo = "bar";
578 return foo;
579 }
580
581 test()
582 "#,
583 )
584 .unwrap();
585
586 assert_eq!("bar".to_string(), res);
587 })
588 }
589
590 #[test]
591 fn eval_minimal_test() {
592 use crate::{Context, Runtime};
593
594 let runtime = Runtime::new().unwrap();
595 let ctx = Context::full(&runtime).unwrap();
596 ctx.with(|ctx| {
597 let res: i32 = ctx.eval(" 1 + 1 ").unwrap();
598 assert_eq!(2, res);
599 })
600 }
601
602 #[test]
603 #[should_panic(expected = "foo is not defined")]
604 fn eval_with_sloppy_code() {
605 use crate::{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(
612 r#"
613 function test() {
614 foo = "bar";
615 return foo;
616 }
617
618 test()
619 "#,
620 )
621 .catch(&ctx)
622 .unwrap();
623 })
624 }
625
626 #[test]
627 fn eval_with_options_no_strict_sloppy_code() {
628 use crate::{context::EvalOptions, Context, Runtime};
629
630 let runtime = Runtime::new().unwrap();
631 let ctx = Context::full(&runtime).unwrap();
632 ctx.with(|ctx| {
633 let res: String = ctx
634 .eval_with_options(
635 r#"
636 function test() {
637 foo = "bar";
638 return foo;
639 }
640
641 test()
642 "#,
643 EvalOptions {
644 strict: false,
645 ..Default::default()
646 },
647 )
648 .unwrap();
649
650 assert_eq!("bar".to_string(), res);
651 })
652 }
653
654 #[test]
655 #[should_panic(expected = "foo is not defined")]
656 fn eval_with_options_strict_sloppy_code() {
657 use crate::{context::EvalOptions, CatchResultExt, Context, Runtime};
658
659 let runtime = Runtime::new().unwrap();
660 let ctx = Context::full(&runtime).unwrap();
661 ctx.with(|ctx| {
662 let _: String = ctx
663 .eval_with_options(
664 r#"
665 function test() {
666 foo = "bar";
667 return foo;
668 }
669
670 test()
671 "#,
672 EvalOptions {
673 strict: true,
674 ..Default::default()
675 },
676 )
677 .catch(&ctx)
678 .unwrap();
679 })
680 }
681
682 #[test]
683 fn json_parse() {
684 use crate::{Array, Context, Object, Runtime};
685
686 let runtime = Runtime::new().unwrap();
687 let ctx = Context::full(&runtime).unwrap();
688 ctx.with(|ctx| {
689 let v = ctx
690 .json_parse(r#"{ "a": { "b": 1, "c": true }, "d": [0,"foo"] }"#)
691 .unwrap();
692 let obj = v.into_object().unwrap();
693 let inner_obj: Object = obj.get("a").unwrap();
694 assert_eq!(inner_obj.get::<_, i32>("b").unwrap(), 1);
695 assert!(inner_obj.get::<_, bool>("c").unwrap());
696 let inner_array: Array = obj.get("d").unwrap();
697 assert_eq!(inner_array.get::<i32>(0).unwrap(), 0);
698 assert_eq!(inner_array.get::<String>(1).unwrap(), "foo".to_string());
699 })
700 }
701
702 #[test]
703 fn json_stringify() {
704 use crate::{Array, Context, Object, Runtime};
705
706 let runtime = Runtime::new().unwrap();
707 let ctx = Context::full(&runtime).unwrap();
708 ctx.with(|ctx| {
709 let obj_inner = Object::new(ctx.clone()).unwrap();
710 obj_inner.set("b", 1).unwrap();
711 obj_inner.set("c", true).unwrap();
712
713 let array_inner = Array::new(ctx.clone()).unwrap();
714 array_inner.set(0, 0).unwrap();
715 array_inner.set(1, "foo").unwrap();
716
717 let obj = Object::new(ctx.clone()).unwrap();
718 obj.set("a", obj_inner).unwrap();
719 obj.set("d", array_inner).unwrap();
720
721 let str = ctx
722 .json_stringify(obj)
723 .unwrap()
724 .unwrap()
725 .to_string()
726 .unwrap();
727
728 assert_eq!(str, r#"{"a":{"b":1,"c":true},"d":[0,"foo"]}"#);
729 })
730 }
731
732 #[test]
733 fn userdata() {
734 use crate::{Context, Function, Runtime};
735
736 pub struct MyUserData<'js> {
737 base: Function<'js>,
738 }
739
740 unsafe impl<'js> JsLifetime<'js> for MyUserData<'js> {
741 type Changed<'to> = MyUserData<'to>;
742 }
743
744 let rt = Runtime::new().unwrap();
745 let ctx = Context::full(&rt).unwrap();
746
747 ctx.with(|ctx| {
748 let func = ctx.eval("() => 42").catch(&ctx).unwrap();
749 ctx.store_userdata(MyUserData { base: func }).unwrap();
750 });
751
752 ctx.with(|ctx| {
753 let userdata = ctx.userdata::<MyUserData>().unwrap();
754
755 assert!(ctx.remove_userdata::<MyUserData>().is_err());
756
757 let r: usize = userdata.base.call(()).unwrap();
758 assert_eq!(r, 42)
759 });
760
761 ctx.with(|ctx| {
762 ctx.remove_userdata::<MyUserData>().unwrap().unwrap();
763 })
764 }
765}