quickjs_rusty/context/context.rs
1#![allow(missing_docs)]
2
3use std::{
4 convert::TryFrom,
5 ffi::{c_char, c_int, c_void},
6 sync::Mutex,
7};
8
9use libquickjs_ng_sys::{self as q, JSContext};
10
11use crate::callback::*;
12use crate::console::ConsoleBackend;
13use crate::errors::*;
14use crate::module_loader::*;
15use crate::utils::{create_string, ensure_no_excpetion, get_exception, make_cstring};
16use crate::value::*;
17
18use super::ContextBuilder;
19
20/// Context is a wrapper around a QuickJS Javascript context.
21/// It is the primary way to interact with the runtime.
22///
23/// For each `Context` instance a new instance of QuickJS
24/// runtime is created. It means that it is safe to use
25/// different contexts in different threads, but each
26/// `Context` instance must be used only from a single thread.
27pub struct Context {
28 runtime: *mut q::JSRuntime,
29 pub(crate) context: *mut q::JSContext,
30 /// Stores callback closures and quickjs data pointers.
31 /// This array is write-only and only exists to ensure the lifetime of
32 /// the closure.
33 // A Mutex is used over a RefCell because it needs to be unwind-safe.
34 callbacks: Mutex<Vec<(Box<WrappedCallback>, Box<q::JSValue>)>>,
35 module_loader: Mutex<Option<Box<ModuleLoader>>>,
36}
37
38impl Drop for Context {
39 fn drop(&mut self) {
40 unsafe {
41 q::JS_FreeContext(self.context);
42 q::JS_FreeRuntime(self.runtime);
43
44 // Drop the module loader.
45 let _ = self.module_loader.lock().unwrap().take();
46 }
47 }
48}
49
50impl Context {
51 /// Create a `ContextBuilder` that allows customization of JS Runtime settings.
52 ///
53 /// For details, see the methods on `ContextBuilder`.
54 ///
55 /// ```rust
56 /// let _context = quickjs_rusty::Context::builder()
57 /// .memory_limit(100_000)
58 /// .build()
59 /// .unwrap();
60 /// ```
61 pub fn builder() -> ContextBuilder {
62 ContextBuilder::new()
63 }
64
65 /// Initialize a wrapper by creating a JSRuntime and JSContext.
66 pub fn new(memory_limit: Option<usize>) -> Result<Self, ContextError> {
67 let runtime = unsafe { q::JS_NewRuntime() };
68 if runtime.is_null() {
69 return Err(ContextError::RuntimeCreationFailed);
70 }
71
72 // Configure memory limit if specified.
73 if let Some(limit) = memory_limit {
74 unsafe {
75 q::JS_SetMemoryLimit(runtime, limit as _);
76 }
77 }
78
79 let context = unsafe { q::JS_NewContext(runtime) };
80 if context.is_null() {
81 unsafe {
82 q::JS_FreeRuntime(runtime);
83 }
84 return Err(ContextError::ContextCreationFailed);
85 }
86
87 // Initialize the promise resolver helper code.
88 // This code is needed by Self::resolve_value
89 let wrapper = Self {
90 runtime,
91 context,
92 callbacks: Mutex::new(Vec::new()),
93 module_loader: Mutex::new(None),
94 };
95
96 Ok(wrapper)
97 }
98
99 // See console standard: https://console.spec.whatwg.org
100 pub fn set_console(&self, backend: Box<dyn ConsoleBackend>) -> Result<(), ExecutionError> {
101 use crate::console::Level;
102
103 self.add_callback("__console_write", move |args: Arguments| {
104 let mut args = args.into_vec();
105
106 if args.len() > 1 {
107 let level_raw = args.remove(0);
108
109 let level_opt = level_raw.to_string().ok().and_then(|v| match v.as_str() {
110 "trace" => Some(Level::Trace),
111 "debug" => Some(Level::Debug),
112 "log" => Some(Level::Log),
113 "info" => Some(Level::Info),
114 "warn" => Some(Level::Warn),
115 "error" => Some(Level::Error),
116 _ => None,
117 });
118
119 if let Some(level) = level_opt {
120 backend.log(level, args);
121 }
122 }
123 })?;
124
125 self.eval(
126 r#"
127 globalThis.console = {
128 trace: (...args) => {
129 globalThis.__console_write("trace", ...args);
130 },
131 debug: (...args) => {
132 globalThis.__console_write("debug", ...args);
133 },
134 log: (...args) => {
135 globalThis.__console_write("log", ...args);
136 },
137 info: (...args) => {
138 globalThis.__console_write("info", ...args);
139 },
140 warn: (...args) => {
141 globalThis.__console_write("warn", ...args);
142 },
143 error: (...args) => {
144 globalThis.__console_write("error", ...args);
145 },
146 };
147 "#,
148 false,
149 )?;
150
151 Ok(())
152 }
153
154 /// Reset the Javascript engine.
155 ///
156 /// All state and callbacks will be removed.
157 pub fn reset(self) -> Result<Self, ContextError> {
158 unsafe {
159 q::JS_FreeContext(self.context);
160 };
161 self.callbacks.lock().unwrap().clear();
162 let context = unsafe { q::JS_NewContext(self.runtime) };
163 if context.is_null() {
164 return Err(ContextError::ContextCreationFailed);
165 }
166
167 let mut s = self;
168 s.context = context;
169 Ok(s)
170 }
171
172 // Get raw pointer to the underlying QuickJS context.
173 pub unsafe fn context_raw(&self) -> *mut q::JSContext {
174 self.context
175 }
176
177 /// Get the global object.
178 pub fn global(&self) -> Result<OwnedJsObject, ExecutionError> {
179 let global_raw = unsafe { q::JS_GetGlobalObject(self.context) };
180 let global_ref = OwnedJsValue::new(self.context, global_raw);
181 let global = global_ref.try_into_object()?;
182 Ok(global)
183 }
184
185 /// Set a global variable.
186 ///
187 /// ```rust
188 /// use quickjs_rusty::Context;
189 /// let context = Context::builder().build().unwrap();
190 ///
191 /// context.set_global("someGlobalVariable", 42).unwrap();
192 /// let value = context.eval_as::<i32>("someGlobalVariable").unwrap();
193 /// assert_eq!(
194 /// value,
195 /// 42,
196 /// );
197 /// ```
198 pub fn set_global<T>(&self, name: &str, value: T) -> Result<(), ExecutionError>
199 where
200 T: ToOwnedJsValue,
201 {
202 let global = self.global()?;
203 global.set_property(name, (self.context, value).into())?;
204 Ok(())
205 }
206
207 /// Execute the pending job in the event loop.
208 pub fn execute_pending_job(&self) -> Result<(), ExecutionError> {
209 let mut pctx = Box::new(std::ptr::null_mut::<JSContext>());
210 unsafe {
211 loop {
212 // TODO: is it actually essential to lock the context here?
213 // let _handle = self.context_lock.lock();
214 let err = q::JS_ExecutePendingJob(self.runtime, pctx.as_mut());
215
216 if err <= 0 {
217 if err < 0 {
218 ensure_no_excpetion(*pctx)?
219 }
220 break;
221 }
222 }
223 }
224 Ok(())
225 }
226
227 /// Check if the given value is an exception, and return the exception if it is.
228 pub fn check_exception(&self, value: &OwnedJsValue) -> Result<(), ExecutionError> {
229 if value.is_exception() {
230 let err = get_exception(self.context)
231 .unwrap_or_else(|| ExecutionError::Internal("Unknown exception".to_string()));
232 Err(err)
233 } else {
234 Ok(())
235 }
236 }
237
238 /// If the given value is a promise, run the event loop until it is
239 /// resolved, and return the final value.
240 pub fn resolve_value(&self, value: OwnedJsValue) -> Result<OwnedJsValue, ExecutionError> {
241 if value.is_object() {
242 let obj = value.try_into_object()?;
243 if obj.is_promise()? {
244 self.eval(
245 r#"
246 // Values:
247 // - undefined: promise not finished
248 // - false: error ocurred, __promiseError is set.
249 // - true: finished, __promiseSuccess is set.
250 var __promiseResult = 0;
251 var __promiseValue = 0;
252
253 var __resolvePromise = function(p) {
254 p
255 .then(value => {
256 __promiseResult = true;
257 __promiseValue = value;
258 })
259 .catch(e => {
260 __promiseResult = false;
261 __promiseValue = e;
262 });
263 }
264 "#,
265 false,
266 )?;
267
268 let global = self.global()?;
269 let resolver = global
270 .property_require("__resolvePromise")?
271 .try_into_function()?;
272
273 // Call the resolver code that sets the result values once
274 // the promise resolves.
275 resolver.call(vec![obj.into_value()])?;
276
277 loop {
278 let flag = unsafe {
279 let wrapper_mut = self as *const Self as *mut Self;
280 let ctx_mut = &mut (*wrapper_mut).context;
281 q::JS_ExecutePendingJob(self.runtime, ctx_mut)
282 };
283 if flag < 0 {
284 let e = get_exception(self.context).unwrap_or_else(|| {
285 ExecutionError::Internal("Unknown exception".to_string())
286 });
287 return Err(e);
288 }
289
290 // Check if promise is finished.
291 let res_val = global.property_require("__promiseResult")?;
292 if res_val.is_bool() {
293 let ok = res_val.to_bool()?;
294 let value = global.property_require("__promiseValue")?;
295
296 if ok {
297 return self.resolve_value(value);
298 } else {
299 let err_msg = value.js_to_string()?;
300 return Err(ExecutionError::Exception(OwnedJsValue::new(
301 self.context,
302 create_string(self.context, &err_msg).unwrap(),
303 )));
304 }
305 }
306 }
307 } else {
308 Ok(obj.into_value())
309 }
310 } else {
311 Ok(value)
312 }
313 }
314
315 /// Evaluates Javascript code and returns the value of the final expression.
316 ///
317 /// resolve: Whether to resolve the returned value if it is a promise. See more details as follows.
318 ///
319 /// **Promises**:
320 /// If the evaluated code returns a Promise, the event loop
321 /// will be executed until the promise is finished. The final value of
322 /// the promise will be returned, or a `ExecutionError::Exception` if the
323 /// promise failed.
324 ///
325 /// ```rust
326 /// use quickjs_rusty::Context;
327 /// let context = Context::builder().build().unwrap();
328 ///
329 /// let value = context.eval(" 1 + 2 + 3 ", false).unwrap();
330 /// assert_eq!(
331 /// value.to_int(),
332 /// Ok(6),
333 /// );
334 ///
335 /// let value = context.eval(r#"
336 /// function f() { return 55 * 3; }
337 /// let y = f();
338 /// var x = y.toString() + "!"
339 /// x
340 /// "#, false).unwrap();
341 /// assert_eq!(
342 /// value.to_string().unwrap(),
343 /// "165!",
344 /// );
345 /// ```
346 pub fn eval(&self, code: &str, resolve: bool) -> Result<OwnedJsValue, ExecutionError> {
347 let filename = "script.js";
348 let filename_c = make_cstring(filename)?;
349 let code_c = make_cstring(code)?;
350
351 let value_raw = unsafe {
352 q::JS_Eval(
353 self.context,
354 code_c.as_ptr(),
355 code.len(),
356 filename_c.as_ptr(),
357 q::JS_EVAL_TYPE_GLOBAL as i32,
358 )
359 };
360 let value = OwnedJsValue::new(self.context, value_raw);
361
362 self.check_exception(&value)?;
363
364 if resolve {
365 self.resolve_value(value)
366 } else {
367 Ok(value)
368 }
369 }
370
371 /// Evaluates Javascript code and returns the value of the final expression
372 /// on module mode.
373 ///
374 /// resolve: Whether to resolve the returned value if it is a promise. See more details as follows.
375 ///
376 /// **Promises**:
377 /// If the evaluated code returns a Promise, the event loop
378 /// will be executed until the promise is finished. The final value of
379 /// the promise will be returned, or a `ExecutionError::Exception` if the
380 /// promise failed.
381 ///
382 /// **Returns**:
383 /// Return value will always be undefined on module mode.
384 ///
385 /// ```ignore
386 /// use quickjs_rusty::Context;
387 /// let context = Context::builder().build().unwrap();
388 ///
389 /// let value = context.eval_module("import {foo} from 'bar'; foo();", false).unwrap();
390 /// ```
391 pub fn eval_module(&self, code: &str, resolve: bool) -> Result<OwnedJsValue, ExecutionError> {
392 let filename = "module.js";
393 let filename_c = make_cstring(filename)?;
394 let code_c = make_cstring(code)?;
395
396 let value_raw = unsafe {
397 q::JS_Eval(
398 self.context,
399 code_c.as_ptr(),
400 code.len(),
401 filename_c.as_ptr(),
402 q::JS_EVAL_TYPE_MODULE as i32,
403 )
404 };
405 let value = OwnedJsValue::new(self.context, value_raw);
406
407 self.check_exception(&value)?;
408
409 if resolve {
410 self.resolve_value(value)
411 } else {
412 Ok(value)
413 }
414 }
415
416 /// Evaluates Javascript code and returns the value of the final expression
417 /// as a Rust type.
418 ///
419 /// **Promises**:
420 /// If the evaluated code returns a Promise, the event loop
421 /// will be executed until the promise is finished. The final value of
422 /// the promise will be returned, or a `ExecutionError::Exception` if the
423 /// promise failed.
424 ///
425 /// ```rust
426 /// use quickjs_rusty::{Context};
427 /// let context = Context::builder().build().unwrap();
428 ///
429 /// let res = context.eval_as::<bool>(" 100 > 10 ");
430 /// assert_eq!(
431 /// res,
432 /// Ok(true),
433 /// );
434 ///
435 /// let value: i32 = context.eval_as(" 10 + 10 ").unwrap();
436 /// assert_eq!(
437 /// value,
438 /// 20,
439 /// );
440 /// ```
441 pub fn eval_as<R>(&self, code: &str) -> Result<R, ExecutionError>
442 where
443 R: TryFrom<OwnedJsValue>,
444 R::Error: Into<ValueError>,
445 {
446 let value = self.eval(code, true)?;
447 let ret = R::try_from(value).map_err(|e| e.into())?;
448 Ok(ret)
449 }
450
451 /// Evaluates Javascript code and returns the value of the final expression
452 /// on module mode.
453 ///
454 /// **Promises**:
455 /// If the evaluated code returns a Promise, the event loop
456 /// will be executed until the promise is finished. The final value of
457 /// the promise will be returned, or a `ExecutionError::Exception` if the
458 /// promise failed.
459 ///
460 /// ```ignore
461 /// use quickjs_rusty::Context;
462 /// let context = Context::builder().build().unwrap();
463 ///
464 /// let value = context.run_module("./module");
465 /// ```
466 pub fn run_module(&self, filename: &str) -> Result<OwnedJsPromise, ExecutionError> {
467 let filename_c = make_cstring(filename)?;
468
469 let ret = unsafe {
470 q::JS_LoadModule(
471 self.context,
472 ".\0".as_ptr() as *const c_char,
473 filename_c.as_ptr(),
474 )
475 };
476
477 let ret = OwnedJsValue::new(self.context, ret);
478
479 ensure_no_excpetion(self.context)?;
480
481 if ret.is_promise() {
482 Ok(ret.try_into_promise()?)
483 } else {
484 Err(ExecutionError::Internal(
485 "Module did not return a promise".to_string(),
486 ))
487 }
488 }
489
490 /// register module loader function, giving module name as input and return module code as output.
491 pub fn set_module_loader(
492 &self,
493 module_loader_func: JSModuleLoaderFunc,
494 module_normalize: Option<JSModuleNormalizeFunc>,
495 opaque: *mut c_void,
496 ) {
497 let has_module_normalize = module_normalize.is_some();
498
499 let module_loader = ModuleLoader {
500 loader: module_loader_func,
501 normalize: module_normalize,
502 opaque,
503 };
504
505 let module_loader = Box::new(module_loader);
506 let module_loader_ptr = module_loader.as_ref() as *const _ as *mut c_void;
507
508 unsafe {
509 if has_module_normalize {
510 q::JS_SetModuleLoaderFunc(
511 self.runtime,
512 Some(js_module_normalize),
513 Some(js_module_loader),
514 module_loader_ptr,
515 );
516 } else {
517 q::JS_SetModuleLoaderFunc(
518 self.runtime,
519 None,
520 Some(js_module_loader),
521 module_loader_ptr,
522 );
523 }
524 }
525
526 *self.module_loader.lock().unwrap() = Some(module_loader);
527 }
528
529 /// Set the host promise rejection tracker.\
530 /// This function works not as expected, see more details in the example.
531 pub fn set_host_promise_rejection_tracker(
532 &self,
533 func: q::JSHostPromiseRejectionTracker,
534 opaque: *mut c_void,
535 ) {
536 unsafe {
537 q::JS_SetHostPromiseRejectionTracker(self.runtime, func, opaque);
538 }
539 }
540
541 /// Set the interrupt handler.\
542 /// Return != 0 if the JS code needs to be interrupted.
543 pub fn set_interrupt_handler(&self, func: q::JSInterruptHandler, opaque: *mut c_void) {
544 unsafe {
545 q::JS_SetInterruptHandler(self.runtime, func, opaque);
546 }
547 }
548
549 /// Call a global function in the Javascript namespace.
550 ///
551 /// **Promises**:
552 /// If the evaluated code returns a Promise, the event loop
553 /// will be executed until the promise is finished. The final value of
554 /// the promise will be returned, or a `ExecutionError::Exception` if the
555 /// promise failed.
556 ///
557 /// ```rust
558 /// use quickjs_rusty::Context;
559 /// let context = Context::builder().build().unwrap();
560 ///
561 /// let res = context.call_function("encodeURIComponent", vec!["a=b"]).unwrap();
562 /// assert_eq!(
563 /// res.to_string(),
564 /// Ok("a%3Db".to_string()),
565 /// );
566 /// ```
567 pub fn call_function(
568 &self,
569 function_name: &str,
570 args: impl IntoIterator<Item = impl ToOwnedJsValue>,
571 ) -> Result<OwnedJsValue, ExecutionError> {
572 let qargs = args
573 .into_iter()
574 .map(|v| (self.context, v).into())
575 .collect::<Vec<OwnedJsValue>>();
576
577 let global = self.global()?;
578 let func = global
579 .property_require(function_name)?
580 .try_into_function()?;
581
582 let ret = func.call(qargs)?;
583 let v = self.resolve_value(ret)?;
584
585 Ok(v)
586 }
587
588 /// Create a JS function that is backed by a Rust function or closure.
589 /// Can be used to create a function and add it to an object.
590 ///
591 /// The callback must satisfy several requirements:
592 /// * accepts 0 - 5 arguments
593 /// * each argument must be convertible from a JsValue
594 /// * must return a value
595 /// * the return value must either:
596 /// - be convertible to JsValue
597 /// - be a Result<T, E> where T is convertible to JsValue
598 /// if Err(e) is returned, a Javascript exception will be raised
599 ///
600 /// ```rust
601 /// use quickjs_rusty::{Context, OwnedJsValue};
602 /// use std::collections::HashMap;
603 ///
604 /// let context = Context::builder().build().unwrap();
605 ///
606 /// // Register an object.
607 /// let mut obj = HashMap::<String, OwnedJsValue>::new();
608 /// let func = context
609 /// .create_callback(|a: i32, b: i32| a + b)
610 /// .unwrap();
611 /// let func = OwnedJsValue::from((unsafe{context.context_raw()}, func));
612 /// // insert add function into the object.
613 /// obj.insert("add".to_string(), func);
614 /// // insert the myObj to global.
615 /// context.set_global("myObj", obj).unwrap();
616 /// // Now we try out the 'myObj.add' function via eval.
617 /// let output = context.eval_as::<i32>("myObj.add( 3 , 4 ) ").unwrap();
618 /// assert_eq!(output, 7);
619 /// ```
620 pub fn create_callback<'a, F>(
621 &self,
622 callback: impl Callback<F> + 'static,
623 ) -> Result<JsFunction, ExecutionError> {
624 let argcount = callback.argument_count() as i32;
625
626 let context = self.context;
627 let wrapper = move |argc: c_int, argv: *mut q::JSValue| -> q::JSValue {
628 match exec_callback(context, argc, argv, &callback) {
629 Ok(value) => value,
630 // TODO: better error reporting.
631 Err(e) => {
632 let js_exception_value = match e {
633 ExecutionError::Exception(e) => unsafe { e.extract() },
634 other => create_string(context, other.to_string().as_str()).unwrap(),
635 };
636 unsafe {
637 q::JS_Throw(context, js_exception_value);
638 }
639
640 unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_EXCEPTION, 0) }
641 }
642 }
643 };
644
645 let (pair, trampoline) = unsafe { build_closure_trampoline(wrapper) };
646 let data = (&*pair.1) as *const q::JSValue as *mut q::JSValue;
647 self.callbacks.lock().unwrap().push(pair);
648
649 let obj = unsafe {
650 let f = q::JS_NewCFunctionData(self.context, trampoline, argcount, 0, 1, data);
651 OwnedJsValue::new(self.context, f)
652 };
653
654 let f = obj.try_into_function()?;
655 Ok(f)
656 }
657
658 /// Add a global JS function that is backed by a Rust function or closure.
659 ///
660 /// The callback must satisfy several requirements:
661 /// * accepts 0 - 5 arguments
662 /// * each argument must be convertible from a JsValue
663 /// * must return a value
664 /// * the return value must either:
665 /// - be convertible to JsValue
666 /// - be a Result<T, E> where T is convertible to JsValue
667 /// if Err(e) is returned, a Javascript exception will be raised
668 ///
669 /// ```rust
670 /// use quickjs_rusty::Context;
671 /// let context = Context::builder().build().unwrap();
672 ///
673 /// // Register a closue as a callback under the "add" name.
674 /// // The 'add' function can now be called from Javascript code.
675 /// context.add_callback("add", |a: i32, b: i32| { a + b }).unwrap();
676 ///
677 /// // Now we try out the 'add' function via eval.
678 /// let output = context.eval_as::<i32>(" add( 3 , 4 ) ").unwrap();
679 /// assert_eq!(
680 /// output,
681 /// 7,
682 /// );
683 /// ```
684 pub fn add_callback<'a, F>(
685 &self,
686 name: &str,
687 callback: impl Callback<F> + 'static,
688 ) -> Result<(), ExecutionError> {
689 let cfunc = self.create_callback(callback)?;
690 let global = self.global()?;
691 global.set_property(name, cfunc.into_value())?;
692 Ok(())
693 }
694
695 /// create a custom callback function
696 pub fn create_custom_callback(
697 &self,
698 callback: CustomCallback,
699 ) -> Result<JsFunction, ExecutionError> {
700 let context = self.context;
701 let wrapper = move |argc: c_int, argv: *mut q::JSValue| -> q::JSValue {
702 let result = std::panic::catch_unwind(|| {
703 let arg_slice = unsafe { std::slice::from_raw_parts(argv, argc as usize) };
704 match callback(context, arg_slice) {
705 Ok(Some(value)) => value,
706 Ok(None) => unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_UNDEFINED, 0) },
707 // TODO: better error reporting.
708 Err(e) => {
709 // TODO: should create an Error type.
710 let js_exception_value =
711 create_string(context, e.to_string().as_str()).unwrap();
712
713 unsafe {
714 q::JS_Throw(context, js_exception_value);
715 }
716
717 unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_EXCEPTION, 0) }
718 }
719 }
720 });
721
722 match result {
723 Ok(v) => v,
724 Err(_) => {
725 // TODO: should create an Error type.
726 let js_exception_value = create_string(context, "Callback panicked!").unwrap();
727
728 unsafe {
729 q::JS_Throw(context, js_exception_value);
730 }
731
732 unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_EXCEPTION, 0) }
733 }
734 }
735 };
736
737 let (pair, trampoline) = unsafe { build_closure_trampoline(wrapper) };
738 let data = (&*pair.1) as *const q::JSValue as *mut q::JSValue;
739 self.callbacks.lock().unwrap().push(pair);
740
741 let obj = unsafe {
742 let f = q::JS_NewCFunctionData(self.context, trampoline, 0, 0, 1, data);
743 OwnedJsValue::new(self.context, f)
744 };
745
746 let f = obj.try_into_function()?;
747 Ok(f)
748 }
749}