Skip to content

Commit

Permalink
Add interpreter support for exception handling
Browse files Browse the repository at this point in the history
Details about the implementation approach:

  * Try blocks generate metadata tracking the instruction ranges for the
    handlers and which exception tags are handled (or if a `catch_all` is
    present). The metadata is stored in a function's `FuncDesc`, and is
    transferred into the `Frame` when a function call is executed.
  * The stack is unwound when a `throw` is executed. This unwinding also
    handles tag dispatch to the appropriate catch. The metadata to find
    the matching handler is looked up in the call `Frame` stack.
  * If a `try-delegate` is present, it is used in the stack unwinding
    process to skip over to the relevant handler.
  * A separate `exceptions_` stack in call frames tracks caught
    exceptions that can be accessed via a `rethrow`. The stack is popped
    on exit from a try block or when exiting via control instructions
    like `br`.
  * Because stack unwinding relies on finding metadata in the call
    frame, `return_call` needs to be modified slightly to adjust the
    current frame when executing the call, rather than re-using the
    frame completely as-is.
  • Loading branch information
takikawa committed Oct 28, 2021
1 parent c7ed7c4 commit 9167cfd
Show file tree
Hide file tree
Showing 15 changed files with 802 additions and 29 deletions.
218 changes: 199 additions & 19 deletions src/interp/binary-reader-interp.cc

Large diffs are not rendered by default.

27 changes: 26 additions & 1 deletion src/interp/interp-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,15 @@ inline ExportType& ExportType::operator=(const ExportType& other) {
inline Frame::Frame(Ref func,
u32 values,
u32 offset,
u32 exceptions,
Instance* inst,
Module* mod)
: func(func), values(values), offset(offset), inst(inst), mod(mod) {}
: func(func),
values(values),
offset(offset),
exceptions(exceptions),
inst(inst),
mod(mod) {}

//// FreeList ////
template <typename T>
Expand Down Expand Up @@ -524,6 +530,25 @@ inline std::string Trap::message() const {
return message_;
}

//// Exception ////
// static
inline bool Exception::classof(const Object* obj) {
return obj->kind() == skind;
}

// static
inline Exception::Ptr Exception::New(Store& store, Ref tag, Values& args) {
return store.Alloc<Exception>(store, tag, args);
}

inline Ref Exception::tag() const {
return tag_;
}

inline Values& Exception::args() {
return args_;
}

//// Extern ////
// static
inline bool Extern::classof(const Object* obj) {
Expand Down
142 changes: 137 additions & 5 deletions src/interp/interp.cc
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,21 @@ void Trap::Mark(Store& store) {
}
}

//// Exception ////
Exception::Exception(Store& store, Ref tag, Values& args)
: Object(skind), tag_(tag), args_(args) {}

void Exception::Mark(Store& store) {
Tag::Ptr tag(store, tag_);
store.Mark(tag_);
ValueTypes params = tag->type().signature;
for (size_t i = 0; i < params.size(); i++) {
if (params[i].IsRef()) {
store.Mark(args_[i].Get<Ref>());
}
}
}

//// Extern ////
template <typename T>
Result Extern::MatchImpl(Store& store,
Expand Down Expand Up @@ -386,6 +401,11 @@ Result DefinedFunc::DoCall(Thread& thread,
result = thread.Run(out_trap);
if (result == RunResult::Trap) {
return Result::Error;
} else if (result == RunResult::Exception) {
// While this is not actually a trap, it is a convenient way
// to report an uncaught exception.
*out_trap = Trap::New(thread.store(), "uncaught exception");
return Result::Error;
}
thread.PopValues(type_.results, &results);
return Result::Ok;
Expand Down Expand Up @@ -955,7 +975,8 @@ Instance* Thread::GetCallerInstance() {

RunResult Thread::PushCall(Ref func, u32 offset, Trap::Ptr* out_trap) {
TRAP_IF(frames_.size() == frames_.capacity(), "call stack exhausted");
frames_.emplace_back(func, values_.size(), offset, inst_, mod_);
frames_.emplace_back(func, values_.size(), offset, exceptions_.size(), inst_,
mod_);
return RunResult::Ok;
}

Expand All @@ -964,15 +985,16 @@ RunResult Thread::PushCall(const DefinedFunc& func, Trap::Ptr* out_trap) {
inst_ = store_.UnsafeGet<Instance>(func.instance()).get();
mod_ = store_.UnsafeGet<Module>(inst_->module()).get();
frames_.emplace_back(func.self(), values_.size(), func.desc().code_offset,
inst_, mod_);
exceptions_.size(), inst_, mod_);
return RunResult::Ok;
}

RunResult Thread::PushCall(const HostFunc& func, Trap::Ptr* out_trap) {
TRAP_IF(frames_.size() == frames_.capacity(), "call stack exhausted");
inst_ = nullptr;
mod_ = nullptr;
frames_.emplace_back(func.self(), values_.size(), 0, inst_, mod_);
frames_.emplace_back(func.self(), values_.size(), 0, exceptions_.size(),
inst_, mod_);
return RunResult::Ok;
}

Expand Down Expand Up @@ -1414,6 +1436,24 @@ RunResult Thread::StepInternal(Trap::Ptr* out_trap) {
break;
}

case O::InterpCatchDrop: {
auto drop = instr.imm_u32;
for (u32 i = 0; i < drop; i++) {
exceptions_.pop_back();
}
break;
}

// This operation adjusts the function reference of the reused frame
// after a return_call. This ensures the correct exception handlers are
// used for the call.
case O::InterpAdjustFrameForReturnCall: {
Ref new_func_ref = inst_->funcs()[instr.imm_u32];
Frame& current_frame = frames_.back();
current_frame.func = new_func_ref;
break;
}

case O::I32TruncSatF32S: return DoUnop(IntTruncSat<s32, f32>);
case O::I32TruncSatF32U: return DoUnop(IntTruncSat<u32, f32>);
case O::I32TruncSatF64S: return DoUnop(IntTruncSat<s32, f64>);
Expand Down Expand Up @@ -1786,6 +1826,21 @@ RunResult Thread::StepInternal(Trap::Ptr* out_trap) {
case O::I64AtomicRmw16CmpxchgU: return DoAtomicRmwCmpxchg<u64, u16>(instr, out_trap);
case O::I64AtomicRmw32CmpxchgU: return DoAtomicRmwCmpxchg<u64, u32>(instr, out_trap);

case O::Throw: {
u32 tag_index = instr.imm_u32;
Values params;
Ref tag_ref = inst_->tags()[tag_index];
Tag::Ptr tag{store_, tag_ref};
PopValues(tag->type().signature, &params);
Ref exn = Exception::New(store_, tag_ref, params).ref();
return DoThrow(exn);
}
case O::Rethrow: {
u32 exn_index = instr.imm_u32;
Ref exn = exceptions_[exceptions_.size() - exn_index - 1];
return DoThrow(exn);
}

// The following opcodes are either never generated or should never be
// executed.
case O::Nop:
Expand All @@ -1802,8 +1857,6 @@ RunResult Thread::StepInternal(Trap::Ptr* out_trap) {
case O::Catch:
case O::CatchAll:
case O::Delegate:
case O::Throw:
case O::Rethrow:
case O::InterpData:
case O::Invalid:
WABT_UNREACHABLE;
Expand Down Expand Up @@ -2390,6 +2443,85 @@ RunResult Thread::DoAtomicRmwCmpxchg(Instr instr, Trap::Ptr* out_trap) {
return RunResult::Ok;
}

RunResult Thread::DoThrow(Ref exn_ref) {
Istream::Offset target_offset = Istream::kInvalidOffset;
Exception::Ptr exn{store_, exn_ref};
Tag::Ptr exn_tag{store_, exn->tag()};
bool popped_frame = false;
bool had_catch_all = false;

// DoThrow is responsible for unwinding the stack at the point at which an
// exception is thrown, and also branching to the appropriate catch within
// the target try-catch. In a compiler, the tag dispatch might be done in
// generated code in a landing pad, but this is easier for the interpreter.
while (!frames_.empty()) {
const Frame& frame = frames_.back();
DefinedFunc::Ptr func{store_, frame.func};
u32 pc = frame.offset;
auto handlers = func->desc().handlers;

// We iterate in reverse order, in order to traverse handlers from most
// specific (pushed last) to least specific within a nested stack of
// try-catch blocks.
auto iter = handlers.rbegin();
while (iter != handlers.rend()) {
const HandlerDesc& handler = *iter;
if (pc >= handler.start_offset && pc < handler.end_offset) {
// For a try-delegate, skip part of the traversal by directly going
// up to an outer handler specified by the delegate depth.
if (handler.kind == HandlerKind::Delegate) {
iter = handlers.rend() - handler.delegate_offset - 1;
continue;
}
// Otherwise, check for a matching catch tag or catch_all.
for (auto _catch : handler.catches) {
Ref catch_ref = inst_->tags()[_catch.tag_index];
Tag::Ptr catch_tag{store_, catch_ref};
if (exn_tag == catch_tag) {
target_offset = _catch.offset;
goto found_handler;
}
}
if (handler.catch_all_offset != Istream::kInvalidOffset) {
target_offset = handler.catch_all_offset;
had_catch_all = true;
goto found_handler;
}
}
iter++;
}
frames_.pop_back();
popped_frame = true;
}

// If the call frames are empty now, the exception is uncaught.
assert(frames_.empty());
return RunResult::Exception;

found_handler:
assert(target_offset != Istream::kInvalidOffset);

Frame& target_frame = frames_.back();
// If the throw crosses call frames, we need to reset the state to that
// call frame's values.
if (popped_frame) {
values_.resize(target_frame.values);
exceptions_.resize(target_frame.exceptions);
inst_ = target_frame.inst;
mod_ = target_frame.mod;
}
// Jump to the handler.
target_frame.offset = target_offset;
if (!had_catch_all) {
PushValues(exn_tag->type().signature, exn->args());
}
// When an exception is caught, it needs to be tracked in a stack
// to allow for rethrows. This stack is popped on leaving the try-catch
// or by control instructions such as `br`.
exceptions_.push_back(exn_ref);
return RunResult::Ok;
}

Thread::TraceSource::TraceSource(Thread* thread) : thread_(thread) {}

std::string Thread::TraceSource::Header(Istream::Offset offset) {
Expand Down
61 changes: 60 additions & 1 deletion src/interp/interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ enum class ObjectKind {
Null,
Foreign,
Trap,
Exception,
DefinedFunc,
HostFunc,
Table,
Expand Down Expand Up @@ -305,13 +306,37 @@ struct LocalDesc {
u32 end;
};

// Metadata for representing exception handlers associated with a function's
// code. This is needed to look up exceptions from call frames from interpreter
// instructions.
struct CatchDesc {
Index tag_index;
u32 offset;
};

// The Try kind is for catch-less try blocks, which have a HandlerDesc that
// is ignored as the block cannot ever catch an exception.
enum class HandlerKind { Catch, Delegate };

struct HandlerDesc {
HandlerKind kind;
u32 start_offset;
u32 end_offset;
std::vector<CatchDesc> catches;
union {
u32 catch_all_offset;
u32 delegate_offset;
};
};

struct FuncDesc {
// Includes params.
ValueType GetLocalType(Index) const;

FuncType type;
std::vector<LocalDesc> locals;
u32 code_offset;
std::vector<HandlerDesc> handlers;
};

struct TableDesc {
Expand Down Expand Up @@ -378,13 +403,19 @@ struct ModuleDesc {
//// Runtime ////

struct Frame {
explicit Frame(Ref func, u32 values, u32 offset, Instance*, Module*);
explicit Frame(Ref func,
u32 values,
u32 offset,
u32 exceptions,
Instance*,
Module*);

void Mark(Store&);

Ref func;
u32 values; // Height of the value stack at this activation.
u32 offset; // Istream offset; either the return PC, or the current PC.
u32 exceptions; // Height of the exception stack at this activation.

// Cached for convenience. Both are null if func is a HostFunc.
Instance* inst;
Expand Down Expand Up @@ -647,6 +678,27 @@ class Trap : public Object {
std::vector<Frame> trace_;
};

class Exception : public Object {
public:
static bool classof(const Object* obj);
static const ObjectKind skind = ObjectKind::Exception;
static const char* GetTypeName() { return "Exception"; }
using Ptr = RefPtr<Exception>;

static Exception::Ptr New(Store&, Ref tag, Values& args);

Ref tag() const;
Values& args();

private:
friend Store;
explicit Exception(Store&, Ref, Values&);
void Mark(Store&) override;

Ref tag_;
Values args_;
};

class Extern : public Object {
public:
static bool classof(const Object* obj);
Expand Down Expand Up @@ -1025,6 +1077,7 @@ enum class RunResult {
Ok,
Return,
Trap,
Exception,
};

// TODO: Kinda weird to have a thread as an object, but it makes reference
Expand Down Expand Up @@ -1183,12 +1236,18 @@ class Thread : public Object {
template <typename T, typename V = T>
RunResult DoAtomicRmwCmpxchg(Instr, Trap::Ptr* out_trap);

RunResult DoThrow(Ref exn_ref);

RunResult StepInternal(Trap::Ptr* out_trap);

std::vector<Frame> frames_;
std::vector<Value> values_;
std::vector<u32> refs_; // Index into values_.

// Exception handling requires tracking a separate stack of caught
// exceptions for catch blocks.
RefVec exceptions_;

// Cached for convenience.
Store& store_;
Instance* inst_ = nullptr;
Expand Down
Loading

0 comments on commit 9167cfd

Please sign in to comment.