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