quick_js/
lib.rs

1//! quick-js is a a Rust wrapper for [QuickJS](https://bellard.org/quickjs/), a new Javascript
2//! engine by Fabrice Bellard.
3//!
4//! It enables easy and straight-forward execution of modern Javascript from Rust.
5//!
6//! ## Limitations
7//!
8//! * Building on Windows requires the `x86_64-pc-windows-gnu` toolchain
9//!
10//! ## Quickstart:
11//!
12//! ```rust
13//! use quick_js::{Context, JsValue};
14//!
15//! let context = Context::new().unwrap();
16//!
17//! // Eval.
18//!
19//! let value = context.eval("1 + 2").unwrap();
20//! assert_eq!(value, JsValue::Int(3));
21//!
22//! let value = context.eval_as::<String>(" var x = 100 + 250; x.toString() ").unwrap();
23//! assert_eq!(&value, "350");
24//!
25//! // Callbacks.
26//!
27//! context.add_callback("myCallback", |a: i32, b: i32| a + b).unwrap();
28//!
29//! context.eval(r#"
30//!     // x will equal 30
31//!     var x = myCallback(10, 20);
32//! "#).unwrap();
33//! ```
34
35#![allow(dead_code, clippy::missing_safety_doc)]
36#![deny(missing_docs)]
37
38mod bindings;
39mod callback;
40pub mod console;
41mod value;
42
43#[cfg(test)]
44mod tests;
45
46use std::{convert::TryFrom, error, fmt};
47
48pub use self::{
49    callback::{Arguments, Callback},
50    value::*,
51};
52
53/// Error on Javascript execution.
54#[derive(PartialEq, Debug)]
55#[non_exhaustive]
56pub enum ExecutionError {
57    /// Code to be executed contained zero-bytes.
58    InputWithZeroBytes,
59    /// Value conversion failed. (either input arguments or result value).
60    Conversion(ValueError),
61    /// Internal error.
62    Internal(String),
63    /// JS Exception was thrown.
64    Exception(JsValue),
65    /// JS Runtime exceeded the memory limit.
66    OutOfMemory,
67}
68
69impl fmt::Display for ExecutionError {
70    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
71        use ExecutionError::*;
72        match self {
73            InputWithZeroBytes => write!(f, "Invalid script input: code contains zero byte (\\0)"),
74            Conversion(e) => e.fmt(f),
75            Internal(e) => write!(f, "Internal error: {e}"),
76            Exception(e) => write!(f, "{e:?}"),
77            OutOfMemory => write!(f, "Out of memory: runtime memory limit exceeded"),
78        }
79    }
80}
81
82impl error::Error for ExecutionError {}
83
84impl From<ValueError> for ExecutionError {
85    fn from(v: ValueError) -> Self {
86        ExecutionError::Conversion(v)
87    }
88}
89
90/// Error on context creation.
91#[derive(Debug)]
92#[non_exhaustive]
93pub enum ContextError {
94    /// Runtime could not be created.
95    RuntimeCreationFailed,
96    /// Context could not be created.
97    ContextCreationFailed,
98    /// Execution error while building.
99    Execution(ExecutionError),
100}
101
102impl fmt::Display for ContextError {
103    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
104        use ContextError::*;
105        match self {
106            RuntimeCreationFailed => write!(f, "Could not create runtime"),
107            ContextCreationFailed => write!(f, "Could not create context"),
108            Execution(e) => e.fmt(f),
109        }
110    }
111}
112
113impl error::Error for ContextError {}
114
115/// A builder for [Context](Context).
116///
117/// Create with [Context::builder](Context::builder).
118pub struct ContextBuilder {
119    memory_limit: Option<usize>,
120    console_backend: Option<Box<dyn console::ConsoleBackend>>,
121}
122
123impl ContextBuilder {
124    fn new() -> Self {
125        Self {
126            memory_limit: None,
127            console_backend: None,
128        }
129    }
130
131    /// Sets the memory limit of the Javascript runtime (in bytes).
132    ///
133    /// If the limit is exceeded, methods like `eval` will return
134    /// a `Err(ExecutionError::Exception(JsValue::Null))`
135    // TODO: investigate why we don't get a proper exception message here.
136    pub fn memory_limit(self, max_bytes: usize) -> Self {
137        let mut s = self;
138        s.memory_limit = Some(max_bytes);
139        s
140    }
141
142    /// Set a console handler that will proxy `console.{log,trace,debug,...}`
143    /// calls.
144    ///
145    /// The given argument must implement the [console::ConsoleBackend] trait.
146    ///
147    /// A very simple logger could look like this:
148    pub fn console<B>(mut self, backend: B) -> Self
149    where
150        B: console::ConsoleBackend,
151    {
152        self.console_backend = Some(Box::new(backend));
153        self
154    }
155
156    /// Finalize the builder and build a JS Context.
157    pub fn build(self) -> Result<Context, ContextError> {
158        let wrapper = bindings::ContextWrapper::new(self.memory_limit)?;
159        if let Some(be) = self.console_backend {
160            wrapper.set_console(be).map_err(ContextError::Execution)?;
161        }
162        Ok(Context::from_wrapper(wrapper))
163    }
164}
165
166/// Context is a wrapper around a QuickJS Javascript context.
167/// It is the primary way to interact with the runtime.
168///
169/// For each `Context` instance a new instance of QuickJS
170/// runtime is created. It means that it is safe to use
171/// different contexts in different threads, but each
172/// `Context` instance must be used only from a single thread.
173pub struct Context {
174    wrapper: bindings::ContextWrapper,
175}
176
177impl Context {
178    fn from_wrapper(wrapper: bindings::ContextWrapper) -> Self {
179        Self { wrapper }
180    }
181
182    /// Create a `ContextBuilder` that allows customization of JS Runtime settings.
183    ///
184    /// For details, see the methods on `ContextBuilder`.
185    ///
186    /// ```rust
187    /// let _context = quick_js::Context::builder()
188    ///     .memory_limit(100_000)
189    ///     .build()
190    ///     .unwrap();
191    /// ```
192    pub fn builder() -> ContextBuilder {
193        ContextBuilder::new()
194    }
195
196    /// Create a new Javascript context with default settings.
197    pub fn new() -> Result<Self, ContextError> {
198        let wrapper = bindings::ContextWrapper::new(None)?;
199        Ok(Self::from_wrapper(wrapper))
200    }
201
202    /// Reset the Javascript engine.
203    ///
204    /// All state and callbacks will be removed.
205    pub fn reset(self) -> Result<Self, ContextError> {
206        let wrapper = self.wrapper.reset()?;
207        Ok(Self { wrapper })
208    }
209
210    /// Evaluates Javascript code and returns the value of the final expression.
211    ///
212    /// **Promises**:
213    /// If the evaluated code returns a Promise, the event loop
214    /// will be executed until the promise is finished. The final value of
215    /// the promise will be returned, or a `ExecutionError::Exception` if the
216    /// promise failed.
217    ///
218    /// ```rust
219    /// use quick_js::{Context, JsValue};
220    /// let context = Context::new().unwrap();
221    ///
222    /// let value = context.eval(" 1 + 2 + 3 ");
223    /// assert_eq!(
224    ///     value,
225    ///     Ok(JsValue::Int(6)),
226    /// );
227    ///
228    /// let value = context.eval(r#"
229    ///     function f() { return 55 * 3; }
230    ///     let y = f();
231    ///     var x = y.toString() + "!"
232    ///     x
233    /// "#);
234    /// assert_eq!(
235    ///     value,
236    ///     Ok(JsValue::String("165!".to_string())),
237    /// );
238    /// ```
239    pub fn eval(&self, code: &str) -> Result<JsValue, ExecutionError> {
240        let value_raw = self.wrapper.eval(code)?;
241        let value = value_raw.to_value()?;
242        Ok(value)
243    }
244
245    /// Evaluates Javascript code and returns the value of the final expression
246    /// as a Rust type.
247    ///
248    /// **Promises**:
249    /// If the evaluated code returns a Promise, the event loop
250    /// will be executed until the promise is finished. The final value of
251    /// the promise will be returned, or a `ExecutionError::Exception` if the
252    /// promise failed.
253    ///
254    /// ```rust
255    /// use quick_js::{Context};
256    /// let context = Context::new().unwrap();
257    ///
258    /// let res = context.eval_as::<bool>(" 100 > 10 ");
259    /// assert_eq!(
260    ///     res,
261    ///     Ok(true),
262    /// );
263    ///
264    /// let value: i32 = context.eval_as(" 10 + 10 ").unwrap();
265    /// assert_eq!(
266    ///     value,
267    ///     20,
268    /// );
269    /// ```
270    pub fn eval_as<R>(&self, code: &str) -> Result<R, ExecutionError>
271    where
272        R: TryFrom<JsValue>,
273        R::Error: Into<ValueError>,
274    {
275        let value_raw = self.wrapper.eval(code)?;
276        let value = value_raw.to_value()?;
277        let ret = R::try_from(value).map_err(|e| e.into())?;
278        Ok(ret)
279    }
280
281    /// Set a global variable.
282    ///
283    /// ```rust
284    /// use quick_js::{Context, JsValue};
285    /// let context = Context::new().unwrap();
286    ///
287    /// context.set_global("someGlobalVariable", 42).unwrap();
288    /// let value = context.eval_as::<i32>("someGlobalVariable").unwrap();
289    /// assert_eq!(
290    ///     value,
291    ///     42,
292    /// );
293    /// ```
294    pub fn set_global<V>(&self, name: &str, value: V) -> Result<(), ExecutionError>
295    where
296        V: Into<JsValue>,
297    {
298        let global = self.wrapper.global()?;
299        let v = self.wrapper.serialize_value(value.into())?;
300        global.set_property(name, v)?;
301        Ok(())
302    }
303
304    /// Call a global function in the Javascript namespace.
305    ///
306    /// **Promises**:
307    /// If the evaluated code returns a Promise, the event loop
308    /// will be executed until the promise is finished. The final value of
309    /// the promise will be returned, or a `ExecutionError::Exception` if the
310    /// promise failed.
311    ///
312    /// ```rust
313    /// use quick_js::{Context, JsValue};
314    /// let context = Context::new().unwrap();
315    ///
316    /// let res = context.call_function("encodeURIComponent", vec!["a=b"]);
317    /// assert_eq!(
318    ///     res,
319    ///     Ok(JsValue::String("a%3Db".to_string())),
320    /// );
321    /// ```
322    pub fn call_function(
323        &self,
324        function_name: &str,
325        args: impl IntoIterator<Item = impl Into<JsValue>>,
326    ) -> Result<JsValue, ExecutionError> {
327        let qargs = args
328            .into_iter()
329            .map(|arg| self.wrapper.serialize_value(arg.into()))
330            .collect::<Result<Vec<_>, _>>()?;
331
332        let global = self.wrapper.global()?;
333        let func = global
334            .property_require(function_name)?
335            .try_into_function()?;
336        let v = self.wrapper.call_function(func, qargs)?.to_value()?;
337        Ok(v)
338    }
339
340    /// Add a global JS function that is backed by a Rust function or closure.
341    ///
342    /// The callback must satisfy several requirements:
343    /// * accepts 0 - 5 arguments
344    /// * each argument must be convertible from a JsValue
345    /// * must return a value
346    /// * the return value must either:
347    ///   - be convertible to JsValue
348    ///   - be a Result<T, E> where T is convertible to JsValue
349    ///     if Err(e) is returned, a Javascript exception will be raised
350    ///
351    /// ```rust
352    /// use quick_js::{Context, JsValue};
353    /// let context = Context::new().unwrap();
354    ///
355    /// // Register a closue as a callback under the "add" name.
356    /// // The 'add' function can now be called from Javascript code.
357    /// context.add_callback("add", |a: i32, b: i32| { a + b }).unwrap();
358    ///
359    /// // Now we try out the 'add' function via eval.
360    /// let output = context.eval_as::<i32>(" add( 3 , 4 ) ").unwrap();
361    /// assert_eq!(
362    ///     output,
363    ///     7,
364    /// );
365    /// ```
366    pub fn add_callback<F>(
367        &self,
368        name: &str,
369        callback: impl Callback<F> + 'static,
370    ) -> Result<(), ExecutionError> {
371        self.wrapper.add_callback(name, callback)
372    }
373}