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