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 /// Call a global function in the Javascript namespace.
542 ///
543 /// **Promises**:
544 /// If the evaluated code returns a Promise, the event loop
545 /// will be executed until the promise is finished. The final value of
546 /// the promise will be returned, or a `ExecutionError::Exception` if the
547 /// promise failed.
548 ///
549 /// ```rust
550 /// use quickjs_rusty::Context;
551 /// let context = Context::builder().build().unwrap();
552 ///
553 /// let res = context.call_function("encodeURIComponent", vec!["a=b"]).unwrap();
554 /// assert_eq!(
555 /// res.to_string(),
556 /// Ok("a%3Db".to_string()),
557 /// );
558 /// ```
559 pub fn call_function(
560 &self,
561 function_name: &str,
562 args: impl IntoIterator<Item = impl ToOwnedJsValue>,
563 ) -> Result<OwnedJsValue, ExecutionError> {
564 let qargs = args
565 .into_iter()
566 .map(|v| (self.context, v).into())
567 .collect::<Vec<OwnedJsValue>>();
568
569 let global = self.global()?;
570 let func = global
571 .property_require(function_name)?
572 .try_into_function()?;
573
574 let ret = func.call(qargs)?;
575 let v = self.resolve_value(ret)?;
576
577 Ok(v)
578 }
579
580 /// Create a JS function that is backed by a Rust function or closure.
581 /// Can be used to create a function and add it to an object.
582 ///
583 /// The callback must satisfy several requirements:
584 /// * accepts 0 - 5 arguments
585 /// * each argument must be convertible from a JsValue
586 /// * must return a value
587 /// * the return value must either:
588 /// - be convertible to JsValue
589 /// - be a Result<T, E> where T is convertible to JsValue
590 /// if Err(e) is returned, a Javascript exception will be raised
591 ///
592 /// ```rust
593 /// use quickjs_rusty::{Context, OwnedJsValue};
594 /// use std::collections::HashMap;
595 ///
596 /// let context = Context::builder().build().unwrap();
597 ///
598 /// // Register an object.
599 /// let mut obj = HashMap::<String, OwnedJsValue>::new();
600 /// let func = context
601 /// .create_callback(|a: i32, b: i32| a + b)
602 /// .unwrap();
603 /// let func = OwnedJsValue::from((unsafe{context.context_raw()}, func));
604 /// // insert add function into the object.
605 /// obj.insert("add".to_string(), func);
606 /// // insert the myObj to global.
607 /// context.set_global("myObj", obj).unwrap();
608 /// // Now we try out the 'myObj.add' function via eval.
609 /// let output = context.eval_as::<i32>("myObj.add( 3 , 4 ) ").unwrap();
610 /// assert_eq!(output, 7);
611 /// ```
612 pub fn create_callback<'a, F>(
613 &self,
614 callback: impl Callback<F> + 'static,
615 ) -> Result<JsFunction, ExecutionError> {
616 let argcount = callback.argument_count() as i32;
617
618 let context = self.context;
619 let wrapper = move |argc: c_int, argv: *mut q::JSValue| -> q::JSValue {
620 match exec_callback(context, argc, argv, &callback) {
621 Ok(value) => value,
622 // TODO: better error reporting.
623 Err(e) => {
624 let js_exception_value = match e {
625 ExecutionError::Exception(e) => unsafe { e.extract() },
626 other => create_string(context, other.to_string().as_str()).unwrap(),
627 };
628 unsafe {
629 q::JS_Throw(context, js_exception_value);
630 }
631
632 unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_EXCEPTION, 0) }
633 }
634 }
635 };
636
637 let (pair, trampoline) = unsafe { build_closure_trampoline(wrapper) };
638 let data = (&*pair.1) as *const q::JSValue as *mut q::JSValue;
639 self.callbacks.lock().unwrap().push(pair);
640
641 let obj = unsafe {
642 let f = q::JS_NewCFunctionData(self.context, trampoline, argcount, 0, 1, data);
643 OwnedJsValue::new(self.context, f)
644 };
645
646 let f = obj.try_into_function()?;
647 Ok(f)
648 }
649
650 /// Add a global JS function that is backed by a Rust function or closure.
651 ///
652 /// The callback must satisfy several requirements:
653 /// * accepts 0 - 5 arguments
654 /// * each argument must be convertible from a JsValue
655 /// * must return a value
656 /// * the return value must either:
657 /// - be convertible to JsValue
658 /// - be a Result<T, E> where T is convertible to JsValue
659 /// if Err(e) is returned, a Javascript exception will be raised
660 ///
661 /// ```rust
662 /// use quickjs_rusty::Context;
663 /// let context = Context::builder().build().unwrap();
664 ///
665 /// // Register a closue as a callback under the "add" name.
666 /// // The 'add' function can now be called from Javascript code.
667 /// context.add_callback("add", |a: i32, b: i32| { a + b }).unwrap();
668 ///
669 /// // Now we try out the 'add' function via eval.
670 /// let output = context.eval_as::<i32>(" add( 3 , 4 ) ").unwrap();
671 /// assert_eq!(
672 /// output,
673 /// 7,
674 /// );
675 /// ```
676 pub fn add_callback<'a, F>(
677 &self,
678 name: &str,
679 callback: impl Callback<F> + 'static,
680 ) -> Result<(), ExecutionError> {
681 let cfunc = self.create_callback(callback)?;
682 let global = self.global()?;
683 global.set_property(name, cfunc.into_value())?;
684 Ok(())
685 }
686
687 /// create a custom callback function
688 pub fn create_custom_callback(
689 &self,
690 callback: CustomCallback,
691 ) -> Result<JsFunction, ExecutionError> {
692 let context = self.context;
693 let wrapper = move |argc: c_int, argv: *mut q::JSValue| -> q::JSValue {
694 let result = std::panic::catch_unwind(|| {
695 let arg_slice = unsafe { std::slice::from_raw_parts(argv, argc as usize) };
696 match callback(context, arg_slice) {
697 Ok(Some(value)) => value,
698 Ok(None) => unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_UNDEFINED, 0) },
699 // TODO: better error reporting.
700 Err(e) => {
701 // TODO: should create an Error type.
702 let js_exception_value =
703 create_string(context, e.to_string().as_str()).unwrap();
704
705 unsafe {
706 q::JS_Throw(context, js_exception_value);
707 }
708
709 unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_EXCEPTION, 0) }
710 }
711 }
712 });
713
714 match result {
715 Ok(v) => v,
716 Err(_) => {
717 // TODO: should create an Error type.
718 let js_exception_value = create_string(context, "Callback panicked!").unwrap();
719
720 unsafe {
721 q::JS_Throw(context, js_exception_value);
722 }
723
724 unsafe { q::JS_Ext_NewSpecialValue(q::JS_TAG_EXCEPTION, 0) }
725 }
726 }
727 };
728
729 let (pair, trampoline) = unsafe { build_closure_trampoline(wrapper) };
730 let data = (&*pair.1) as *const q::JSValue as *mut q::JSValue;
731 self.callbacks.lock().unwrap().push(pair);
732
733 let obj = unsafe {
734 let f = q::JS_NewCFunctionData(self.context, trampoline, 0, 0, 1, data);
735 OwnedJsValue::new(self.context, f)
736 };
737
738 let f = obj.try_into_function()?;
739 Ok(f)
740 }
741}