Skip to content

Commit

Permalink
Update Trap for Wasmtime 3
Browse files Browse the repository at this point in the history
In Wasmtime 3, `Trap` is now only an Enum (just like `TrapCode` was).
See bytecodealliance/wasmtime#5149

I've adjusted the Ruby bindings to match that change:
- Moved the trap code constants on `Trap`, removing the `TrapCode`
  module.
- Renamed `Trap#trap_code` to `Trap#trap`.
- Made `Trap#message` exclude the Wasm backtrace by default, and
  instead added a `wasm_backtrace_message` method for that.
  I chose this name so we can still implement `wasm_backtrace` that
  returns an Enumerable of traces, if we ever have the need.

This is probably fine for now. In the spirit of matching Wasmtime
closely, I wonder if we should instead have a single error type
(`Wasmtime::Error`) and define `trap_code` and `wasm_backtrace` on it.
We can take that on later and suggest merging this as it's a smaller
diff and still gets us on Wasmtime 3.
  • Loading branch information
jbourassa committed Nov 22, 2022
1 parent a4c734d commit 2333c13
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 70 deletions.
12 changes: 6 additions & 6 deletions ext/src/ruby_api/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use magnus::{
use std::cell::UnsafeCell;
use wasmtime::{
AsContext, AsContextMut, Caller as CallerImpl, Func as FuncImpl, StoreContext, StoreContextMut,
Trap, Val,
Val,
};

/// @yard
Expand Down Expand Up @@ -168,7 +168,7 @@ impl From<&Func<'_>> for wasmtime::Extern {
pub fn make_func_closure(
ty: &wasmtime::FuncType,
callable: Proc,
) -> impl Fn(CallerImpl<'_, StoreData>, &[Val], &mut [Val]) -> Result<(), Trap> + Send + Sync + 'static
) -> impl Fn(CallerImpl<'_, StoreData>, &[Val], &mut [Val]) -> anyhow::Result<()> + Send + Sync + 'static
{
let ty = ty.to_owned();
let callable = ShareableProc(callable);
Expand All @@ -182,9 +182,9 @@ pub fn make_func_closure(
rparams.push(Value::from(wrapped_caller)).unwrap();

for (i, param) in params.iter().enumerate() {
let rparam = param.to_ruby_value(&store_context).map_err(|e| {
wasmtime::Trap::new(format!("invalid argument at index {}: {}", i, e))
})?;
let rparam = param
.to_ruby_value(&store_context)
.map_err(|e| anyhow::anyhow!(format!("invalid argument at index {}: {}", i, e)))?;
rparams.push(rparam).unwrap();
}

Expand Down Expand Up @@ -223,7 +223,7 @@ pub fn make_func_closure(
}
})
.map_err(|e| {
wasmtime::Trap::new(format!(
anyhow::anyhow!(format!(
"Error when calling Func {}\n Error: {}",
callable.inspect(),
e
Expand Down
8 changes: 4 additions & 4 deletions ext/src/ruby_api/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use magnus::{
Module, Object, TypedData, Value, QNIL,
};
use std::cell::{RefCell, UnsafeCell};
use std::convert::TryFrom;
use wasmtime::{AsContext, AsContextMut, Store as StoreImpl, StoreContext, StoreContextMut};

#[derive(Debug)]
Expand Down Expand Up @@ -162,10 +163,9 @@ impl<'a> StoreContextValue<'a> {
pub fn handle_wasm_error(&self, error: anyhow::Error) -> Error {
match self.context_mut() {
Ok(mut context) => context.data_mut().take_last_error().unwrap_or_else(|| {
match error.downcast_ref::<wasmtime::Trap>() {
Some(t) => Trap::from(t.to_owned()).into(),
_ => error!("{}", error),
}
Trap::try_from(error)
.map(|trap| trap.into())
.unwrap_or_else(|e| error!("{}", e))
}),
Err(e) => e,
}
Expand Down
106 changes: 70 additions & 36 deletions ext/src/ruby_api/trap.rs
Original file line number Diff line number Diff line change
@@ -1,77 +1,98 @@
use std::convert::TryFrom;

use crate::helpers::WrappedStruct;
use crate::ruby_api::{errors::base_error, root};
use magnus::Error;
use magnus::{
memoize, method, rb_sys::AsRawValue, DataTypeFunctions, ExceptionClass, Module as _, RModule,
Symbol, TypedData, Value,
memoize, method, rb_sys::AsRawValue, DataTypeFunctions, ExceptionClass, Module as _, Symbol,
TypedData, Value,
};
use wasmtime::TrapCode;

pub fn trap_error() -> ExceptionClass {
*memoize!(ExceptionClass: root().define_error("Trap", base_error()).unwrap())
}

pub fn trap_code() -> RModule {
*memoize!(RModule: root().define_module("TrapCode").unwrap())
}

macro_rules! trap_const {
($trap:ident) => {
trap_code().const_get(stringify!($trap)).map(Some)
trap_error().const_get(stringify!($trap)).map(Some)
};
}

#[derive(TypedData, Debug)]
#[magnus(class = "Wasmtime::Trap", size, free_immediatly)]
/// @yard
pub struct Trap {
inner: wasmtime::Trap,
trap: wasmtime::Trap,
wasm_backtrace: Option<wasmtime::WasmBacktrace>,
}
impl DataTypeFunctions for Trap {}

impl Trap {
pub fn new(trap: wasmtime::Trap, wasm_backtrace: Option<wasmtime::WasmBacktrace>) -> Self {
Self {
trap,
wasm_backtrace,
}
}

/// @yard
/// Returns the message with backtrace. Example message:
/// Returns a textual description of the trap error, for example:
/// wasm trap: wasm `unreachable` instruction executed
/// wasm backtrace:
/// 0: 0x1a - <unknown>!<wasm function 0>
/// @return [String]
pub fn message(&self) -> String {
self.inner.to_string()
self.trap.to_string()
}

/// @yard
/// Returns a textual representation of the Wasm backtrce, if it exists.
/// For example:
/// error while executing at wasm backtrace:
/// 0: 0x1a - <unknown>!<wasm function 0>
/// @return [String, nil]
pub fn wasm_backtrace_message(&self) -> Option<String> {
self.wasm_backtrace.as_ref().map(|bt| format!("{}", bt))
}

/// @yard
/// Returns the trap code as a Symbol, possibly nil if the trap did not
/// origin from Wasm code. All possible trap codes are defined as constants on {Trap}.
/// @return [Symbol, nil]
pub fn trap_code(&self) -> Result<Option<Symbol>, Error> {
if let Some(code) = self.inner.trap_code() {
match code {
TrapCode::HeapMisaligned => trap_const!(HEAP_MISALIGNED),
TrapCode::TableOutOfBounds => trap_const!(TABLE_OUT_OF_BOUNDS),
TrapCode::IndirectCallToNull => trap_const!(INDIRECT_CALL_TO_NULL),
TrapCode::BadSignature => trap_const!(BAD_SIGNATURE),
TrapCode::IntegerOverflow => trap_const!(INTEGER_OVERFLOW),
TrapCode::IntegerDivisionByZero => trap_const!(INTEGER_DIVISION_BY_ZERO),
TrapCode::BadConversionToInteger => trap_const!(BAD_CONVERSION_TO_INTEGER),
TrapCode::UnreachableCodeReached => trap_const!(UNREACHABLE_CODE_REACHED),
TrapCode::Interrupt => trap_const!(INTERRUPT),
TrapCode::AlwaysTrapAdapter => trap_const!(ALWAYS_TRAP_ADAPTER),
// When adding a trap code here, define a matching constant on Wasmtime::Trap (in Ruby)
_ => trap_const!(UNKNOWN),
}
} else {
Ok(None)
pub fn code(&self) -> Result<Option<Symbol>, Error> {
match self.trap {
wasmtime::Trap::HeapMisaligned => trap_const!(HEAP_MISALIGNED),
wasmtime::Trap::TableOutOfBounds => trap_const!(TABLE_OUT_OF_BOUNDS),
wasmtime::Trap::IndirectCallToNull => trap_const!(INDIRECT_CALL_TO_NULL),
wasmtime::Trap::BadSignature => trap_const!(BAD_SIGNATURE),
wasmtime::Trap::IntegerOverflow => trap_const!(INTEGER_OVERFLOW),
wasmtime::Trap::IntegerDivisionByZero => trap_const!(INTEGER_DIVISION_BY_ZERO),
wasmtime::Trap::BadConversionToInteger => trap_const!(BAD_CONVERSION_TO_INTEGER),
wasmtime::Trap::UnreachableCodeReached => trap_const!(UNREACHABLE_CODE_REACHED),
wasmtime::Trap::Interrupt => trap_const!(INTERRUPT),
wasmtime::Trap::AlwaysTrapAdapter => trap_const!(ALWAYS_TRAP_ADAPTER),
// When adding a trap code here, define a matching constant on Wasmtime::Trap (in Ruby)
_ => trap_const!(UNKNOWN),
}
}

// pub fn wasm_backtrace(&self) -> Option<RArray> {
// self.wasm_backtrace.as_ref().map(|backtrace| {
// let array = RArray::with_capacity(backtrace.frames().len());
// backtrace
// .frames()
// .iter()
// .for_each(|frame| array.push(frame.format()).unwrap());

// array
// })
// }

pub fn inspect(rb_self: WrappedStruct<Self>) -> Result<String, Error> {
let rs_self = rb_self.get()?;

Ok(format!(
"#<Wasmtime::Trap:0x{:016x} @trap_code={}>",
rb_self.to_value().as_raw(),
Value::from(rs_self.trap_code()?).inspect()
Value::from(rs_self.code()?).inspect()
))
}
}
Expand All @@ -85,16 +106,29 @@ impl From<Trap> for Error {
}
}

impl From<wasmtime::Trap> for Trap {
fn from(trap: wasmtime::Trap) -> Self {
Self { inner: trap }
impl TryFrom<anyhow::Error> for Trap {
type Error = anyhow::Error;

fn try_from(value: anyhow::Error) -> Result<Self, Self::Error> {
match value.downcast_ref::<wasmtime::Trap>() {
Some(trap) => {
let trap = trap.to_owned();
let bt = value.downcast::<wasmtime::WasmBacktrace>();
Ok(Trap::new(trap, bt.map(Some).unwrap_or(None)))
}
None => Err(value),
}
}
}

pub fn init() -> Result<(), Error> {
let class = trap_error();
class.define_method("message", method!(Trap::message, 0))?;
class.define_method("trap_code", method!(Trap::trap_code, 0))?;
class.define_method(
"wasm_backtrace_message",
method!(Trap::wasm_backtrace_message, 0),
)?;
class.define_method("code", method!(Trap::code, 0))?;
class.define_method("inspect", method!(Trap::inspect, 0))?;
class.define_alias("to_s", "message")?;
Ok(())
Expand Down
16 changes: 14 additions & 2 deletions lib/wasmtime.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# frozen_string_literal: true

require_relative "wasmtime/version"
require_relative "wasmtime/trap_code"

# Tries to require the extension for the given Ruby version first
begin
Expand All @@ -16,5 +15,18 @@ class Error < StandardError; end

class ConversionError < Error; end

class Trap < Error; end
class Trap < Error
STACK_OVERFLOW = :stack_overflow
HEAP_MISALIGNED = :heap_misaligned
TABLE_OUT_OF_BOUNDS = :table_out_of_bounds
INDIRECT_CALL_TO_NULL = :indirect_call_to_null
BAD_SIGNATURE = :bad_signature
INTEGER_OVERFLOW = :integer_overflow
INTEGER_DIVISION_BY_ZERO = :integer_division_by_zero
BAD_CONVERSION_TO_INTEGER = :bad_conversion_to_integer
UNREACHABLE_CODE_REACHED = :unreachable_code_reached
INTERRUPT = :interrupt
ALWAYS_TRAP_ADAPTER = :always_trap_adapter
UNKNOWN = :unknown
end
end
16 changes: 0 additions & 16 deletions lib/wasmtime/trap_code.rb

This file was deleted.

22 changes: 16 additions & 6 deletions spec/unit/trap_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,28 @@ module Wasmtime
end

describe "#message" do
it "has the full message including backtrace" do
expect(trap.message).to eq(<<~MSG)
wasm trap: wasm `unreachable` instruction executed
wasm backtrace:
it "has a short message" do
expect(trap.message).to eq("wasm trap: wasm `unreachable` instruction executed")
end
end

describe "#message_with_backtrace" do
it "includes the backtrace" do
expect(trap.wasm_backtrace_message).to eq(<<~MSG.rstrip)
error while executing at wasm backtrace:
0: 0x1a - <unknown>!<wasm function 0>
MSG
end
end

describe "#trap_code" do
describe "#wasm_backtrace" do
it "returns an enumerable of trace entries" do
end
end

describe "#code" do
it "returns a symbol matching a constant" do
expect(trap.trap_code).to eq(TrapCode::UNREACHABLE_CODE_REACHED)
expect(trap.code).to eq(Trap::UNREACHABLE_CODE_REACHED)
end
end

Expand Down

0 comments on commit 2333c13

Please sign in to comment.