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}