diff --git a/js/src/jit-test/tests/wasm/exceptions/reftypes.js b/js/src/jit-test/tests/wasm/exceptions/reftypes.js new file mode 100644 index 000000000000..ed71965cb619 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/reftypes.js @@ -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"); +} diff --git a/js/src/jit-test/tests/wasm/exceptions/validation.js b/js/src/jit-test/tests/wasm/exceptions/validation.js index 4c99fa5a6961..779218f80393 100644 --- a/js/src/jit-test/tests/wasm/exceptions/validation.js +++ b/js/src/jit-test/tests/wasm/exceptions/validation.js @@ -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(); diff --git a/js/src/wasm/WasmBaselineCompile.cpp b/js/src/wasm/WasmBaselineCompile.cpp index 30a467e3e396..84379713b48b 100644 --- a/js/src/wasm/WasmBaselineCompile.cpp +++ b/js/src/wasm/WasmBaselineCompile.cpp @@ -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); diff --git a/js/src/wasm/WasmBuiltins.cpp b/js/src/wasm/WasmBuiltins.cpp index e08c37897593..7758274fbf12 100644 --- a/js/src/wasm/WasmBuiltins.cpp +++ b/js/src/wasm/WasmBuiltins.cpp @@ -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: diff --git a/js/src/wasm/WasmBuiltins.h b/js/src/wasm/WasmBuiltins.h index 13983a3b7453..28e701672ba7 100644 --- a/js/src/wasm/WasmBuiltins.h +++ b/js/src/wasm/WasmBuiltins.h @@ -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; diff --git a/js/src/wasm/WasmFrameIter.cpp b/js/src/wasm/WasmFrameIter.cpp index 436a55a4845d..eca2b3156b8d 100644 --- a/js/src/wasm/WasmFrameIter.cpp +++ b/js/src/wasm/WasmFrameIter.cpp @@ -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)"; diff --git a/js/src/wasm/WasmInstance.cpp b/js/src/wasm/WasmInstance.cpp index 81977343346e..6145929dfc55 100644 --- a/js/src/wasm/WasmInstance.cpp +++ b/js/src/wasm/WasmInstance.cpp @@ -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()); + RootedWasmRuntimeExceptionObject exnObj( + cx, &exn->as()); + + // 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, diff --git a/js/src/wasm/WasmInstance.h b/js/src/wasm/WasmInstance.h index ea2f4fb03325..0a5e39af0f0a 100644 --- a/js/src/wasm/WasmInstance.h +++ b/js/src/wasm/WasmInstance.h @@ -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); diff --git a/js/src/wasm/WasmJS.cpp b/js/src/wasm/WasmJS.cpp index ebf815a71176..1637e6be39a8 100644 --- a/js/src/wasm/WasmJS.cpp +++ b/js/src/wasm/WasmJS.cpp @@ -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()); - 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(); +} + // ============================================================================ // WebAssembly class and static methods diff --git a/js/src/wasm/WasmJS.h b/js/src/wasm/WasmJS.h index 1f97396a3957..adef0effc93e 100644 --- a/js/src/wasm/WasmJS.h +++ b/js/src/wasm/WasmJS.h @@ -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 values); + Handle 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. diff --git a/js/src/wasm/WasmOpIter.h b/js/src/wasm/WasmOpIter.h index a81136c832cf..0203bfed8ad8 100644 --- a/js/src/wasm/WasmOpIter.h +++ b/js/src/wasm/WasmOpIter.h @@ -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::readBrTable(Uint32Vector* depths, #undef UNKNOWN_ARITY #ifdef ENABLE_WASM_EXCEPTIONS -template -inline bool OpIter::exceptionTypeHasRef(ResultType type) { - for (size_t i = 0; i < type.length(); i++) { - if (type[i].isReference()) { - return true; - } - } - - return false; -} - template inline bool OpIter::readTry(ResultType* paramType) { MOZ_ASSERT(Classify(op_) == OpKind::Try); @@ -1424,9 +1409,6 @@ inline bool OpIter::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::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; diff --git a/js/src/wasm/WasmTypes.h b/js/src/wasm/WasmTypes.h index 4728b8d3aa95..0fef01dcbaa6 100644 --- a/js/src/wasm/WasmTypes.h +++ b/js/src/wasm/WasmTypes.h @@ -3615,6 +3615,7 @@ enum class SymbolicAddress { ExceptionNew, ThrowException, GetLocalExceptionIndex, + PushRefIntoExn, #endif ArrayNew, InlineTypedObjectClass,