Skip to content

Commit

Permalink
fix: Manually downcast JsValues back to traps because DowncastJS isn'…
Browse files Browse the repository at this point in the history
…t thread-safe
  • Loading branch information
Michael-F-Bryan committed Aug 31, 2023
1 parent 5ebbe65 commit 4cd65e8
Showing 1 changed file with 99 additions and 20 deletions.
119 changes: 99 additions & 20 deletions lib/api/src/js/trap.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
use crate::RuntimeError;
use std::error::Error;
use std::fmt;
use std::{
error::Error,
fmt::{self, Display},
};

use js_sys::Reflect;
use wasm_bindgen::{prelude::*, JsValue};
use wasm_bindgen_downcast::DowncastJS;

use crate::RuntimeError;

#[derive(Debug)]
enum InnerTrap {
User(Box<dyn Error + Send + Sync>),
Js(JsValue),
Js(JsTrap),
}

/// A struct representing a Trap
#[wasm_bindgen]
#[derive(Debug, DowncastJS)]
#[wasm_bindgen(skip_typescript)]
#[derive(Debug)]
pub struct Trap {
inner: InnerTrap,
}

unsafe impl Send for Trap {}
unsafe impl Sync for Trap {}

impl Trap {
pub fn user(error: Box<dyn Error + Send + Sync>) -> Self {
Self {
Expand Down Expand Up @@ -55,6 +55,11 @@ impl Trap {
}
}

#[wasm_bindgen]
impl Trap {
pub fn __wbg_wasmer_trap() {}
}

impl std::error::Error for Trap {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.inner {
Expand All @@ -67,20 +72,94 @@ impl std::error::Error for Trap {
impl fmt::Display for Trap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.inner {
InnerTrap::User(e) => write!(f, "user: {}", e),
InnerTrap::Js(value) => write!(f, "js: {:?}", value.as_string()),
InnerTrap::User(e) => write!(f, "user: {e}"),
InnerTrap::Js(value) => write!(f, "js: {value}"),
}
}
}

impl From<JsValue> for RuntimeError {
fn from(original: JsValue) -> Self {
// We try to downcast the error and see if it's
// an instance of RuntimeError instead, so we don't need
// to re-wrap it.
let trap = Trap::downcast_js(original).unwrap_or_else(|o| Trap {
inner: InnerTrap::Js(o),
});
trap.into()
fn from(value: JsValue) -> Self {
// We try to downcast the error and see if it's an instance of Trap
// instead, so we don't need to re-wrap it.
if let Some(obj) = value.dyn_ref() {
if let Some(trap) = downcast_from_ptr(obj) {
return trap.into();
}
}

RuntimeError::from(Trap {
inner: InnerTrap::Js(value.into()),
})
}
}

/// This whole mechanism works because the JavaScript wrapper class has a static
/// `__wbg_wasmer_trap()` method which marks that it is a [`Trap`].
///
/// If that method exists, we assume the pointer is valid and safe to cast back
/// to our type.
fn downcast_from_ptr(value: &JsValue) -> Option<Trap> {
if !value.is_object() {
return None;
}

let prototype = &Reflect::get_prototype_of(value).ok()?;
let class = prototype.constructor();
let key = JsValue::from_str("__wbg_wasmer_trap");

let _marker_func: js_sys::Function = Reflect::get(&class, &key)
.and_then(|v: JsValue| v.dyn_into())
.ok()?;

// Note: this assumes the wrapper class generated by #[wasm_bindgen] will
// always store the pointer in a field called "__wbg_ptr". This is valid
// as of wasm-bindgen version 0.2.87
let key = JsValue::from_str("__wbg_ptr");
let ptr = Reflect::get(value, &key).ok().and_then(|v| v.as_f64())?;

// Safety: The marker function exists, therefore it's safe to cast back to a trap.
unsafe {
Some(<Trap as wasm_bindgen::convert::FromWasmAbi>::from_abi(
ptr as u32,
))
}
}

/// A `Send+Sync` version of a JavaScript error.
#[derive(Debug)]
enum JsTrap {
/// An error message.
Message(String),
/// Unable to determine the underlying error.
Unknown,
}

impl From<JsValue> for JsTrap {
fn from(value: JsValue) -> Self {
// Let's try some easy special cases first
if let Some(error) = value.dyn_ref::<js_sys::Error>() {
return JsTrap::Message(error.message().into());
}

if let Some(s) = value.as_string() {
return JsTrap::Message(s);
}

// Otherwise, we'll try to stringify the error and hope for the best
if let Some(obj) = value.dyn_ref::<js_sys::Object>() {
return JsTrap::Message(obj.to_string().into());
}

JsTrap::Unknown
}
}

impl Display for JsTrap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JsTrap::Message(m) => write!(f, "{m}"),
JsTrap::Unknown => write!(f, "unknown"),
}
}
}

0 comments on commit 4cd65e8

Please sign in to comment.