javy/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
//! Configurable JavaScript runtime for WebAssembly.
//!
//! Example usage:
//! ```
//! use anyhow::anyhow;
//! use javy::{Runtime, from_js_error};
//! let runtime = Runtime::default();
//! let context = runtime.context();
//!
//! context.with(|cx| {
//! let globals = this.globals();
//! globals.set(
//! "print_hello",
//! Function::new(
//! this.clone(),
//! MutFn::new(move |_, _| {
//! println!("Hello, world!");
//! }),
//! )?,
//! )?;
//! });
//!
//! context.with(|cx| {
//! cx.eval_with_options(Default::default(), "print_hello();")
//! .map_err(|e| from_js_error(cx.clone(), e))
//! .map(|_| ())
//! });
//! ```
//!
//! ## Core concepts
//! * [`Runtime`] - The entrypoint for using the JavaScript runtime. Use a
//! [`Config`] to configure behavior.
//!
//! ## Features
//! * `export_alloc_fns` - exports [`alloc::canonical_abi_realloc`] and
//! [`alloc::canonical_abi_free`] from generated WebAssembly for allocating
//! and freeing memory
//! * `json` - functions for converting between [`quickjs::JSValueRef`] and JSON
//! byte slices
//! * `messagepack` - functions for converting between [`quickjs::JSValueRef`]
//! and MessagePack byte slices
pub use config::*;
pub use rquickjs as quickjs;
pub use runtime::Runtime;
use std::str;
pub mod alloc;
mod config;
mod runtime;
mod serde;
use anyhow::{anyhow, Error, Result};
use rquickjs::{
convert, prelude::Rest, qjs, Ctx, Error as JSError, Exception, FromJs, String as JSString,
Value,
};
#[cfg(feature = "messagepack")]
pub mod messagepack;
#[cfg(feature = "json")]
pub mod json;
mod apis;
/// A struct to hold the current [`Ctx`] and [`Value`]s passed as arguments to Rust
/// functions.
/// A struct here is used to explicitly tie these values with a particular
/// lifetime.
//
// See: https://github.com/rust-lang/rfcs/pull/3216
pub struct Args<'js>(Ctx<'js>, Rest<Value<'js>>);
impl<'js> Args<'js> {
/// Tie the [Ctx] and [Rest<Value>].
pub fn hold(cx: Ctx<'js>, args: Rest<Value<'js>>) -> Self {
Self(cx, args)
}
/// Get the [Ctx] and [Rest<Value>].
pub fn release(self) -> (Ctx<'js>, Rest<Value<'js>>) {
(self.0, self.1)
}
}
/// Alias for [`Args::hold(cx, args).release()`]
#[macro_export]
macro_rules! hold_and_release {
($cx:expr, $args:expr) => {
Args::hold($cx, $args).release()
};
}
/// Alias for [`Args::hold`]
#[macro_export]
macro_rules! hold {
($cx:expr, $args:expr) => {
Args::hold($cx, $args)
};
}
/// Handles a JavaScript error or exception and converts to [anyhow::Error].
pub fn from_js_error(ctx: Ctx<'_>, e: JSError) -> Error {
if e.is_exception() {
let val = ctx.catch();
if let Some(exception) = val.clone().into_exception() {
anyhow!("{exception}")
} else {
anyhow!(val_to_string(&ctx, val).unwrap_or_else(|_| "Internal error".to_string()))
}
} else {
Into::into(e)
}
}
/// Converts an [`anyhow::Error`] to a [`JSError`].
///
/// If the error is an [`anyhow::Error`] this function will construct and throw
/// a JS [`Exception`] in order to construct the [`JSError`].
pub fn to_js_error(cx: Ctx, e: Error) -> JSError {
match e.downcast::<JSError>() {
Ok(e) => e,
Err(e) => {
// In some cases the original error context is lost i.e. we can't
// retain the orginal JSError when invoking serde_transcode,
// particularly for json::stringify. The process of transcoding will
// report the Serializer error, which is totally implementation
// dependent, in this case particular to serde_json::Error. To
// workaround this, we identify the exception via its string
// representation. This is not ideal, but its also fine as it only
// happens in the transcoding case.
//
// Ref: https://github.com/sfackler/serde-transcode/issues/8
if e.to_string()
.contains("JSError: Exception generated by QuickJS")
{
return JSError::Exception;
}
cx.throw(Value::from_exception(
Exception::from_message(cx.clone(), &e.to_string())
.expect("creating an exception to succeed"),
))
}
}
}
/// Converts the JavaScript value to a string, replacing any invalid UTF-8 sequences with the
/// Unicode replacement character (U+FFFD).
// TODO: Upstream this?
pub fn to_string_lossy<'js>(cx: &Ctx<'js>, string: &JSString<'js>, error: JSError) -> String {
let mut len: qjs::size_t = 0;
let ptr = unsafe { qjs::JS_ToCStringLen2(cx.as_raw().as_ptr(), &mut len, string.as_raw(), 0) };
let buffer = unsafe { std::slice::from_raw_parts(ptr as *const u8, len as usize) };
// The error here *must* be a Utf8 error; the `JSString::to_string()` may
// return `JSError::Unknown`, but at that point, something else has gone
// wrong too.
let mut utf8_error = match error {
JSError::Utf8(e) => e,
_ => unreachable!("expected Utf8 error"),
};
let mut res = String::new();
let mut buffer = buffer;
loop {
let (valid, after_valid) = buffer.split_at(utf8_error.valid_up_to());
res.push_str(unsafe { str::from_utf8_unchecked(valid) });
res.push(char::REPLACEMENT_CHARACTER);
// see https://simonsapin.github.io/wtf-8/#surrogate-byte-sequence
let lone_surrogate = matches!(after_valid, [0xED, 0xA0..=0xBF, 0x80..=0xBF, ..]);
// https://simonsapin.github.io/wtf-8/#converting-wtf-8-utf-8 states that each
// 3-byte lone surrogate sequence should be replaced by 1 UTF-8 replacement
// char. Rust's `Utf8Error` reports each byte in the lone surrogate byte
// sequence as a separate error with an `error_len` of 1. Since we insert a
// replacement char for each error, this results in 3 replacement chars being
// inserted. So we use an `error_len` of 3 instead of 1 to treat the entire
// 3-byte sequence as 1 error instead of as 3 errors and only emit 1
// replacement char.
let error_len = if lone_surrogate {
3
} else {
utf8_error
.error_len()
.expect("Error length should always be available on underlying buffer")
};
buffer = &after_valid[error_len..];
match str::from_utf8(buffer) {
Ok(valid) => {
res.push_str(valid);
break;
}
Err(e) => utf8_error = e,
}
}
res
}
/// Retrieves the string representation of a JavaScript value.
pub fn val_to_string<'js>(this: &Ctx<'js>, val: Value<'js>) -> Result<String> {
if let Some(symbol) = val.as_symbol() {
if let Some(description) = symbol.description()?.into_string() {
let description = description
.to_string()
.unwrap_or_else(|e| to_string_lossy(this, &description, e));
Ok(format!("Symbol({description})"))
} else {
Ok("Symbol()".into())
}
} else {
let stringified = <convert::Coerced<JSString>>::from_js(this, val).map(|string| {
string
.to_string()
.unwrap_or_else(|e| to_string_lossy(this, &string.0, e))
})?;
Ok(stringified)
}
}