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}