Bug 1694537 - support reference types in wasm exceptions r=rhunt

This patch adds support for using reference types in wasm
exception types and allows exceptions including such values
to be thrown and caught.

Differential Revision: https://phabricator.services.mozilla.com/D106850
This commit is contained in:
Asumu Takikawa 2021-03-14 20:13:47 +00:00
Родитель 637bff54ac
Коммит 681a9661aa
12 изменённых файлов: 300 добавлений и 74 удалений

Просмотреть файл

@ -0,0 +1,143 @@
// |jit-test| skip-if: !wasmReftypesEnabled()
//
// Exception tests that use reference types.
load(libdir + "eqArrayHelper.js");
assertEq(
wasmEvalText(
`(module
(event $exn (param externref))
(func (export "f") (result externref)
try (result externref)
ref.null extern
throw $exn
catch $exn
end))`
).exports.f(),
null
);
{
let f = wasmEvalText(
`(module
(event $exn (param funcref))
(func $f (export "f") (result funcref)
try (result funcref)
ref.func $f
throw $exn
catch $exn
end))`
).exports.f;
assertEq(f(), f);
}
{
let f = wasmEvalText(
`(module
(event $exn (param externref))
(func (export "f") (param externref) (result externref)
try (result externref)
local.get 0
throw $exn
catch $exn
end))`
).exports.f;
for (v of WasmExternrefValues) {
assertEq(f(v), v);
}
}
assertEqArray(
wasmEvalText(
`(module
(event $exn (param externref externref))
(func (export "f") (param externref externref)
(result externref externref)
try (result externref externref)
local.get 0
local.get 1
throw $exn
catch $exn
end))`
).exports.f("foo", "bar"),
["foo", "bar"]
);
assertEqArray(
wasmEvalText(
`(module
(event $exn (param i32 i32 externref f32))
(func (export "f") (param externref)
(result i32 i32 externref f32)
try (result i32 i32 externref f32)
i32.const 0
i32.const 1
local.get 0
f32.const 2.0
throw $exn
catch $exn
end))`
).exports.f("foo"),
[0, 1, "foo", 2.0]
);
assertEqArray(
wasmEvalText(
`(module
(event $exn (param i32 i32 externref f32 externref f64))
(func (export "f") (param externref externref)
(result i32 i32 externref f32 externref f64)
try (result i32 i32 externref f32 externref f64)
i32.const 0
i32.const 1
local.get 0
f32.const 2.0
local.get 1
f64.const 3.0
throw $exn
catch $exn
end))`
).exports.f("foo", "bar"),
[0, 1, "foo", 2.0, "bar", 3.0]
);
// Test to ensure that reference typed values are tracked correctly by
// the GC within exception objects.
{
gczeal(2, 1); // Collect on every allocation.
var thrower;
let exports = wasmEvalText(
`(module
(event $exn (param externref))
(import "m" "f" (func $f (result externref)))
(func (export "thrower") (param externref)
(local.get 0)
(throw $exn))
(func (export "catcher") (result externref)
try (result externref)
(call $f)
catch $exn
end))`,
{
m: {
f: () => {
// The purpose of this intermediate try-catch in JS is to force
// some allocation to occur after Wasm's `throw`, triggering GC, and
// then rethrow back to Wasm to observe any errors.
try {
thrower("foo");
} catch (e) {
let v = { x: 5 };
throw e;
}
},
},
}
).exports;
thrower = exports.thrower;
assertEq(exports.catcher(), "foo");
}

Просмотреть файл

@ -337,37 +337,8 @@ function testValidateExnPayload() {
wasmInvalid(invalid1, /event index out of range/);
}
// Exception types with reference types are not supported yet.
function testValidateExnWithRef() {
invalid0 = `(module
(event $exn (param externref))
(func try nop catch $exn drop end))`;
error0 = /exception with reference types not supported/;
invalid1 = `(module
(event $exn (param externref))
(func (param externref) (local.get 0) (throw $exn)))`;
error1 = /exception with reference types not supported/;
invalid2 = `(module
(event $exn (param funcref))
(func try nop catch $exn drop end))`;
error2 = /exception with reference types not supported/;
invalid3 = `(module
(event $exn (param funcref))
(func (param funcref) (local.get 0) (throw $exn)))`;
error3 = /exception with reference types not supported/;
wasmFailValidateText(invalid0, error0);
wasmFailValidateText(invalid1, error1);
wasmFailValidateText(invalid2, error2);
wasmFailValidateText(invalid3, error3);
}
testValidateDecode();
testValidateThrow();
testValidateTryCatch();
testValidateCatch();
testValidateExnPayload();
testValidateExnWithRef();

Просмотреть файл

@ -10632,6 +10632,13 @@ bool BaseCompiler::emitCatch() {
// Extract the arguments in the exception package and push them.
const ResultType params = moduleEnv_.events[eventIndex].resultType();
uint32_t refCount = 0;
for (uint32_t i = 0; i < params.length(); i++) {
if (params[i].isReference()) {
refCount++;
}
}
const uint32_t dataOffset =
NativeObject::getFixedSlotOffset(ArrayBufferObject::DATA_SLOT);
@ -10639,57 +10646,92 @@ bool BaseCompiler::emitCatch() {
// is live in this register.
RegRef exn = RegRef(WasmExceptionReg);
needRef(exn);
RegRef scratch = needRef();
RegRef values = needRef();
RegRef refs = needRef();
masm.unboxObject(Address(exn, WasmRuntimeExceptionObject::offsetOfValues()),
scratch);
values);
masm.unboxObject(Address(exn, WasmRuntimeExceptionObject::offsetOfRefs()),
refs);
# ifdef DEBUG
Label ok;
RegI32 scratch = needI32();
masm.load32(Address(refs, NativeObject::offsetOfFixedElements() +
ObjectElements::offsetOfLength()),
scratch);
masm.branch32(Assembler::Equal, scratch, Imm32(refCount), &ok);
masm.assumeUnreachable("Array length should be equal to exn ref count.");
masm.bind(&ok);
freeI32(scratch);
# endif
masm.loadPtr(Address(refs, NativeObject::offsetOfElements()), refs);
freeRef(exn);
masm.loadPtr(Address(scratch, dataOffset), scratch);
masm.loadPtr(Address(values, dataOffset), values);
size_t argOffset = 0;
// The ref values have been pushed into the ArrayObject in a stacklike
// fashion so we need to load them starting from the last element.
int32_t refIndex = refCount - 1;
for (uint32_t i = 0; i < params.length(); i++) {
switch (params[i].kind()) {
case ValType::I32: {
RegI32 reg = needI32();
masm.load32(Address(scratch, argOffset), reg);
masm.load32(Address(values, argOffset), reg);
pushI32(reg);
break;
}
case ValType::I64: {
RegI64 reg = needI64();
masm.load64(Address(scratch, argOffset), reg);
masm.load64(Address(values, argOffset), reg);
pushI64(reg);
break;
}
case ValType::F32: {
RegF32 reg = needF32();
masm.loadFloat32(Address(scratch, argOffset), reg);
masm.loadFloat32(Address(values, argOffset), reg);
pushF32(reg);
break;
}
case ValType::F64: {
RegF64 reg = needF64();
masm.loadDouble(Address(scratch, argOffset), reg);
masm.loadDouble(Address(values, argOffset), reg);
pushF64(reg);
break;
}
case ValType::V128: {
# ifdef ENABLE_WASM_SIMD
RegV128 reg = needV128();
masm.loadUnalignedSimd128(Address(scratch, argOffset), reg);
masm.loadUnalignedSimd128(Address(values, argOffset), reg);
pushV128(reg);
break;
# else
MOZ_CRASH("No SIMD support");
# endif
}
case ValType::Ref:
case ValType::Rtt:
MOZ_CRASH("NYI - no reftype support");
case ValType::Ref: {
// TODO/AnyRef-boxing: With boxed immediates and strings, this may need
// to handle other kinds of values.
ASSERT_ANYREF_IS_JSOBJECT;
RegRef reg = needRef();
NativeObject::elementsSizeMustNotOverflow();
uint32_t offset = refIndex * sizeof(Value);
masm.unboxObjectOrNull(Address(refs, offset), reg);
pushRef(reg);
refIndex--;
break;
}
}
if (!params[i].isReference()) {
argOffset += SizeOf(params[i]);
}
argOffset += SizeOf(params[i]);
}
freeRef(scratch);
MOZ_ASSERT(refIndex == -1);
freeRef(values);
freeRef(refs);
return true;
}
@ -10805,7 +10847,9 @@ bool BaseCompiler::emitThrow() {
// Measure space we need for all the args to put in the exception.
uint32_t exnBytes = 0;
for (size_t i = 0; i < params.length(); i++) {
exnBytes += SizeOf(params[i]);
if (!params[i].isReference()) {
exnBytes += SizeOf(params[i]);
}
}
// Create the new exception object that we will throw.
@ -10821,13 +10865,15 @@ bool BaseCompiler::emitThrow() {
const uint32_t dataOffset =
NativeObject::getFixedSlotOffset(ArrayBufferObject::DATA_SLOT);
masm.unboxObject(Address(exn, WasmRuntimeExceptionObject::offsetOfValues()),
scratch);
Address exnValuesAddress(exn, WasmRuntimeExceptionObject::offsetOfValues());
masm.unboxObject(exnValuesAddress, scratch);
masm.loadPtr(Address(scratch, dataOffset), scratch);
size_t argOffset = exnBytes;
for (int32_t i = params.length() - 1; i >= 0; i--) {
argOffset -= SizeOf(params[i]);
if (!params[i].isReference()) {
argOffset -= SizeOf(params[i]);
}
switch (params[i].kind()) {
case ValType::I32: {
RegI32 reg = popI32();
@ -10863,11 +10909,37 @@ bool BaseCompiler::emitThrow() {
MOZ_CRASH("No SIMD support");
# endif
}
case ValType::Ref:
case ValType::Rtt:
MOZ_CRASH("NYI - no reftype support");
case ValType::Ref: {
RegRef refArg = popRef();
// Keep exn on the stack to preserve it across the call.
RegRef tmp = needRef();
moveRef(exn, tmp);
pushRef(tmp);
// Arguments to the instance call start here.
pushRef(exn);
pushRef(refArg);
if (!emitInstanceCall(lineOrBytecode, SASigPushRefIntoExn)) {
return false;
}
// The call result is checked by the instance call failure handling,
// so we do not need to use the result here.
freeI32(popI32());
exn = popRef();
// Restore scratch register contents that got clobbered.
masm.unboxObject(exnValuesAddress, scratch);
masm.loadPtr(Address(scratch, dataOffset), scratch);
break;
}
}
}
MOZ_ASSERT(argOffset == 0);
freeRef(scratch);
return throwFrom(exn, lineOrBytecode);

Просмотреть файл

@ -236,6 +236,12 @@ const SymbolicAddressSignature SASigGetLocalExceptionIndex = {
_Infallible,
2,
{_PTR, _RoN, _END}};
const SymbolicAddressSignature SASigPushRefIntoExn = {
SymbolicAddress::PushRefIntoExn,
_I32,
_FailOnNegI32,
3,
{_PTR, _RoN, _RoN, _END}};
#endif
const SymbolicAddressSignature SASigArrayNew = {SymbolicAddress::ArrayNew,
_RoN,
@ -1231,6 +1237,11 @@ void* wasm::AddressOf(SymbolicAddress imm, ABIFunctionType* abiType) {
{ArgType_General, ArgType_General});
MOZ_ASSERT(*abiType == ToABIType(SASigGetLocalExceptionIndex));
return FuncCast(Instance::getLocalExceptionIndex, *abiType);
case SymbolicAddress::PushRefIntoExn:
*abiType = MakeABIFunctionType(
ArgType_Int32, {ArgType_General, ArgType_General, ArgType_General});
MOZ_ASSERT(*abiType == ToABIType(SASigPushRefIntoExn));
return FuncCast(Instance::pushRefIntoExn, *abiType);
#endif
#if defined(JS_CODEGEN_MIPS32)
@ -1352,6 +1363,7 @@ bool wasm::NeedsBuiltinThunk(SymbolicAddress sym) {
case SymbolicAddress::ExceptionNew:
case SymbolicAddress::ThrowException:
case SymbolicAddress::GetLocalExceptionIndex:
case SymbolicAddress::PushRefIntoExn:
#endif
case SymbolicAddress::ArrayNew:
case SymbolicAddress::RefTest:

Просмотреть файл

@ -78,6 +78,7 @@ extern const SymbolicAddressSignature SASigStructNew;
extern const SymbolicAddressSignature SASigExceptionNew;
extern const SymbolicAddressSignature SASigThrowException;
extern const SymbolicAddressSignature SASigGetLocalExceptionIndex;
extern const SymbolicAddressSignature SASigPushRefIntoExn;
#endif
extern const SymbolicAddressSignature SASigArrayNew;
extern const SymbolicAddressSignature SASigRefTest;

Просмотреть файл

@ -1453,6 +1453,8 @@ static const char* ThunkedNativeToDescription(SymbolicAddress func) {
return "call to native throw exception (in wasm)";
case SymbolicAddress::GetLocalExceptionIndex:
return "call to native get the local index of an exn's tag (in wasm)";
case SymbolicAddress::PushRefIntoExn:
return "call to native that pushes a ref value into an exn (in wasm)";
#endif
case SymbolicAddress::ArrayNew:
return "call to native array.new (in wasm)";

Просмотреть файл

@ -1004,7 +1004,14 @@ bool Instance::initElems(uint32_t tableIndex, const ElemSegment& seg,
return nullptr;
}
return AnyRef::fromJSObject(WasmRuntimeExceptionObject::create(cx, tag, buf))
RootedArrayObject refs(cx, NewDenseEmptyArray(cx));
if (!refs) {
return nullptr;
}
return AnyRef::fromJSObject(
WasmRuntimeExceptionObject::create(cx, tag, buf, refs))
.forCompiledCode();
}
@ -1046,6 +1053,30 @@ bool Instance::initElems(uint32_t tableIndex, const ElemSegment& seg,
// JS value.
return UINT32_MAX;
}
/* static */ int32_t Instance::pushRefIntoExn(Instance* instance, JSObject* exn,
JSObject* ref) {
MOZ_ASSERT(SASigPushRefIntoExn.failureMode == FailureMode::FailOnNegI32);
JSContext* cx = TlsContext.get();
MOZ_ASSERT(exn->is<WasmRuntimeExceptionObject>());
RootedWasmRuntimeExceptionObject exnObj(
cx, &exn->as<WasmRuntimeExceptionObject>());
// TODO/AnyRef-boxing: With boxed immediates and strings, this may need to
// handle other kinds of values.
ASSERT_ANYREF_IS_JSOBJECT;
RootedValue refVal(cx, ObjectOrNullValue(ref));
RootedArrayObject arr(cx, &exnObj->refs());
if (!NewbornArrayPush(cx, arr, refVal)) {
return -1;
}
return 0;
}
#endif
/* static */ int32_t Instance::refTest(Instance* instance, void* refPtr,

Просмотреть файл

@ -225,6 +225,8 @@ class Instance {
uint32_t nbytes);
static void* throwException(Instance* instance, JSObject* exn);
static uint32_t getLocalExceptionIndex(Instance* instance, JSObject* exn);
static int32_t pushRefIntoExn(Instance* instance, JSObject* exn,
JSObject* ref);
#endif
static void* arrayNew(Instance* instance, uint32_t length, void* arrayDescr);
static int32_t refTest(Instance* instance, void* refPtr, void* rttPtr);

Просмотреть файл

@ -3815,8 +3815,8 @@ bool WasmRuntimeExceptionObject::construct(JSContext* cx, unsigned argc,
/* static */
WasmRuntimeExceptionObject* WasmRuntimeExceptionObject::create(
JSContext* cx, wasm::SharedExceptionTag tag,
HandleArrayBufferObject values) {
JSContext* cx, wasm::SharedExceptionTag tag, HandleArrayBufferObject values,
HandleArrayObject refs) {
RootedObject proto(
cx, &cx->global()->getPrototype(JSProto_WasmRuntimeException).toObject());
@ -3832,6 +3832,7 @@ WasmRuntimeExceptionObject* WasmRuntimeExceptionObject::create(
MemoryUse::WasmRuntimeExceptionTag);
obj->initFixedSlot(VALUES_SLOT, ObjectValue(*values));
obj->initFixedSlot(REFS_SLOT, ObjectValue(*refs));
MOZ_ASSERT(!obj->isNewborn());
@ -3840,7 +3841,7 @@ WasmRuntimeExceptionObject* WasmRuntimeExceptionObject::create(
bool WasmRuntimeExceptionObject::isNewborn() const {
MOZ_ASSERT(is<WasmRuntimeExceptionObject>());
return getReservedSlot(VALUES_SLOT).isUndefined();
return getReservedSlot(REFS_SLOT).isUndefined();
}
const JSPropertySpec WasmRuntimeExceptionObject::properties[] = {
@ -3856,6 +3857,10 @@ ExceptionTag& WasmRuntimeExceptionObject::tag() const {
return *(ExceptionTag*)getReservedSlot(TAG_SLOT).toPrivate();
}
ArrayObject& WasmRuntimeExceptionObject::refs() const {
return getReservedSlot(REFS_SLOT).toObject().as<ArrayObject>();
}
// ============================================================================
// WebAssembly class and static methods

Просмотреть файл

@ -518,6 +518,7 @@ class WasmExceptionObject : public NativeObject {
class WasmRuntimeExceptionObject : public NativeObject {
static const unsigned TAG_SLOT = 0;
static const unsigned VALUES_SLOT = 1;
static const unsigned REFS_SLOT = 2;
static const JSClassOps classOps_;
static const ClassSpec classSpec_;
@ -525,7 +526,7 @@ class WasmRuntimeExceptionObject : public NativeObject {
static void trace(JSTracer* trc, JSObject* obj);
public:
static const unsigned RESERVED_SLOTS = 2;
static const unsigned RESERVED_SLOTS = 3;
static const JSClass class_;
static const JSClass& protoClass_;
static const JSPropertySpec properties[];
@ -535,14 +536,20 @@ class WasmRuntimeExceptionObject : public NativeObject {
static WasmRuntimeExceptionObject* create(JSContext* cx,
wasm::SharedExceptionTag tag,
Handle<ArrayBufferObject*> values);
Handle<ArrayBufferObject*> values,
HandleArrayObject refs);
bool isNewborn() const;
wasm::ExceptionTag& tag() const;
ArrayObject& refs() const;
static size_t offsetOfValues() {
return NativeObject::getFixedSlotOffset(VALUES_SLOT);
}
static size_t offsetOfRefs() {
return NativeObject::getFixedSlotOffset(REFS_SLOT);
}
};
// The class of the WebAssembly global namespace object.

Просмотреть файл

@ -393,10 +393,6 @@ class MOZ_STACK_CLASS OpIter : private Policy {
inline bool checkIsSubtypeOf(ValType lhs, ValType rhs);
inline bool checkIsSubtypeOf(ResultType params, ResultType results);
#ifdef ENABLE_WASM_EXCEPTIONS
[[nodiscard]] bool exceptionTypeHasRef(ResultType type);
#endif
public:
#ifdef DEBUG
explicit OpIter(const ModuleEnvironment& env, Decoder& decoder)
@ -1387,17 +1383,6 @@ inline bool OpIter<Policy>::readBrTable(Uint32Vector* depths,
#undef UNKNOWN_ARITY
#ifdef ENABLE_WASM_EXCEPTIONS
template <typename Policy>
inline bool OpIter<Policy>::exceptionTypeHasRef(ResultType type) {
for (size_t i = 0; i < type.length(); i++) {
if (type[i].isReference()) {
return true;
}
}
return false;
}
template <typename Policy>
inline bool OpIter<Policy>::readTry(ResultType* paramType) {
MOZ_ASSERT(Classify(op_) == OpKind::Try);
@ -1424,9 +1409,6 @@ inline bool OpIter<Policy>::readCatch(LabelKind* kind, uint32_t* eventIndex,
if (*eventIndex >= env_.events.length()) {
return fail("event index out of range");
}
if (exceptionTypeHasRef(env_.events[*eventIndex].resultType())) {
return fail("exception with reference types not supported");
}
Control& block = controlStack_.back();
if (block.kind() != LabelKind::Try && block.kind() != LabelKind::Catch) {
@ -1458,9 +1440,6 @@ inline bool OpIter<Policy>::readThrow(uint32_t* eventIndex,
if (*eventIndex >= env_.events.length()) {
return fail("event index out of range");
}
if (exceptionTypeHasRef(env_.events[*eventIndex].resultType())) {
return fail("exception with reference types not supported.");
}
if (!popWithType(env_.events[*eventIndex].resultType(), argValues)) {
return false;

Просмотреть файл

@ -3615,6 +3615,7 @@ enum class SymbolicAddress {
ExceptionNew,
ThrowException,
GetLocalExceptionIndex,
PushRefIntoExn,
#endif
ArrayNew,
InlineTypedObjectClass,