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)
    }
}