Bug 1284155 - Baldr: add Table.set support for the same-instance case (r=bbouvier)
authorLuke Wagner <luke@mozilla.com>
Tue, 19 Jul 2016 15:23:11 -0500
changeset 330811 42550e82d1d27075cc8fa2eb2f807afef7d0dbfc
parent 330810 e5408aaf65922ef9d792f711a5bb37562e5dc639
child 330812 dbbe25db2d61505c8a6c8561412b4b204ad30257
push id9858
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 14:37:10 +0000
treeherdermozilla-aurora@203106ef6cb6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbbouvier
bugs1284155
milestone50.0a1
Bug 1284155 - Baldr: add Table.set support for the same-instance case (r=bbouvier) MozReview-Commit-ID: 4QclLRTJ7KZ
js/src/asmjs/WasmCode.h
js/src/asmjs/WasmGenerator.cpp
js/src/asmjs/WasmInstance.cpp
js/src/asmjs/WasmJS.cpp
js/src/asmjs/WasmJS.h
js/src/jit-test/tests/wasm/import-export.js
js/src/jit-test/tests/wasm/jsapi.js
js/src/jit-test/tests/wasm/tables.js
js/src/js.msg
--- a/js/src/asmjs/WasmCode.h
+++ b/js/src/asmjs/WasmCode.h
@@ -120,41 +120,46 @@ typedef RefPtr<const ShareableBytes> Sha
 // O(log(n)) lookup of a FuncExport by function-index, the FuncExportVector
 // is stored sorted by function index.
 
 class FuncExport
 {
     Sig sig_;
     struct CacheablePod {
         uint32_t funcIndex_;
-        uint32_t stubOffset_;
+        uint32_t entryOffset_;
+        uint32_t tableEntryOffset_;
     } pod;
 
   public:
     FuncExport() = default;
-    explicit FuncExport(Sig&& sig, uint32_t funcIndex)
+    explicit FuncExport(Sig&& sig, uint32_t funcIndex, uint32_t tableEntryOffset)
       : sig_(Move(sig))
     {
         pod.funcIndex_ = funcIndex;
-        pod.stubOffset_ = UINT32_MAX;
+        pod.entryOffset_ = UINT32_MAX;
+        pod.tableEntryOffset_ = tableEntryOffset;
     }
-    void initStubOffset(uint32_t stubOffset) {
-        MOZ_ASSERT(pod.stubOffset_ == UINT32_MAX);
-        pod.stubOffset_ = stubOffset;
+    void initEntryOffset(uint32_t entryOffset) {
+        MOZ_ASSERT(pod.entryOffset_ == UINT32_MAX);
+        pod.entryOffset_ = entryOffset;
     }
 
     const Sig& sig() const {
         return sig_;
     }
     uint32_t funcIndex() const {
         return pod.funcIndex_;
     }
-    uint32_t stubOffset() const {
-        MOZ_ASSERT(pod.stubOffset_ != UINT32_MAX);
-        return pod.stubOffset_;
+    uint32_t entryOffset() const {
+        MOZ_ASSERT(pod.entryOffset_ != UINT32_MAX);
+        return pod.entryOffset_;
+    }
+    uint32_t tableEntryOffset() const {
+        return pod.tableEntryOffset_;
     }
 
     WASM_DECLARE_SERIALIZABLE(FuncExport)
 };
 
 typedef Vector<FuncExport, 0, SystemAllocPolicy> FuncExportVector;
 
 // An FuncImport contains the runtime metadata needed to implement a call to an
--- a/js/src/asmjs/WasmGenerator.cpp
+++ b/js/src/asmjs/WasmGenerator.cpp
@@ -356,17 +356,18 @@ ModuleGenerator::finishFuncExports()
     if (!metadata_->funcExports.reserve(exportedFuncs_.count()))
         return false;
 
     for (uint32_t funcIndex : funcIndices) {
         Sig sig;
         if (!sig.clone(funcSig(funcIndex)))
             return false;
 
-        metadata_->funcExports.infallibleEmplaceBack(Move(sig), funcIndex);
+        uint32_t tableEntry = funcCodeRange(funcIndex).funcTableEntry();
+        metadata_->funcExports.infallibleEmplaceBack(Move(sig), funcIndex, tableEntry);
     }
 
     return true;
 }
 
 bool
 ModuleGenerator::finishCodegen()
 {
@@ -412,17 +413,17 @@ ModuleGenerator::finishCodegen()
             return false;
     }
 
     // Adjust each of the resulting Offsets (to account for being merged into
     // masm_) and then create code ranges for all the stubs.
 
     for (uint32_t i = 0; i < numFuncExports; i++) {
         entries[i].offsetBy(offsetInWhole);
-        metadata_->funcExports[i].initStubOffset(entries[i].begin);
+        metadata_->funcExports[i].initEntryOffset(entries[i].begin);
         if (!metadata_->codeRanges.emplaceBack(CodeRange::Entry, entries[i]))
             return false;
     }
 
     for (uint32_t i = 0; i < numFuncImports(); i++) {
         interpExits[i].offsetBy(offsetInWhole);
         metadata_->funcImports[i].initInterpExitOffset(interpExits[i].begin);
         if (!metadata_->codeRanges.emplaceBack(CodeRange::ImportInterpExit, interpExits[i]))
--- a/js/src/asmjs/WasmInstance.cpp
+++ b/js/src/asmjs/WasmInstance.cpp
@@ -533,17 +533,17 @@ Instance::callExport(JSContext* cx, uint
         // when running this module. Additionally, push a JitActivation so that
         // the optimized wasm-to-Ion FFI call path (which we want to be very
         // fast) can avoid doing so. The JitActivation is marked as inactive so
         // stack iteration will skip over it.
         WasmActivation activation(cx, *this);
         JitActivation jitActivation(cx, /* active */ false);
 
         // Call the per-exported-function trampoline created by GenerateEntry.
-        auto funcPtr = JS_DATA_TO_FUNC_PTR(ExportFuncPtr, codeSegment_->code() + func.stubOffset());
+        auto funcPtr = JS_DATA_TO_FUNC_PTR(ExportFuncPtr, codeSegment_->code() + func.entryOffset());
         if (!CALL_GENERATED_2(funcPtr, exportArgs.begin(), codeSegment_->globalData()))
             return false;
     }
 
     if (args.isConstructing()) {
         // By spec, when a function is called as a constructor and this function
         // returns a primary type, which is the case for all wasm exported
         // functions, the returned value is discarded and an empty object is
--- a/js/src/asmjs/WasmJS.cpp
+++ b/js/src/asmjs/WasmJS.cpp
@@ -567,22 +567,42 @@ WasmInstanceObject::getExportedFunction(
 }
 
 bool
 wasm::IsExportedFunction(JSFunction* fun)
 {
     return fun->maybeNative() == WasmCall;
 }
 
+bool
+wasm::IsExportedFunction(const Value& v, MutableHandleFunction f)
+{
+    if (!v.isObject())
+        return false;
+
+    JSObject& obj = v.toObject();
+    if (!obj.is<JSFunction>() || !IsExportedFunction(&obj.as<JSFunction>()))
+        return false;
+
+    f.set(&obj.as<JSFunction>());
+    return true;
+}
+
 Instance&
 wasm::ExportedFunctionToInstance(JSFunction* fun)
 {
+    return ExportedFunctionToInstanceObject(fun)->instance();
+}
+
+WasmInstanceObject*
+wasm::ExportedFunctionToInstanceObject(JSFunction* fun)
+{
     MOZ_ASSERT(IsExportedFunction(fun));
     const Value& v = fun->getExtendedSlot(FunctionExtended::WASM_INSTANCE_SLOT);
-    return v.toObject().as<WasmInstanceObject>().instance();
+    return &v.toObject().as<WasmInstanceObject>();
 }
 
 uint32_t
 wasm::ExportedFunctionToIndex(JSFunction* fun)
 {
     MOZ_ASSERT(IsExportedFunction(fun));
     const Value& v = fun->getExtendedSlot(FunctionExtended::WASM_FUNC_INDEX_SLOT);
     return v.toInt32();
@@ -901,19 +921,94 @@ WasmTableObject::getImpl(JSContext* cx, 
 
 /* static */ bool
 WasmTableObject::get(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     return CallNonGenericMethod<IsTable, getImpl>(cx, args);
 }
 
+/* static */ bool
+WasmTableObject::setImpl(JSContext* cx, const CallArgs& args)
+{
+    RootedWasmTableObject tableObj(cx, &args.thisv().toObject().as<WasmTableObject>());
+    const Table& table = tableObj->table();
+
+    if (!args.requireAtLeast(cx, "set", 2))
+        return false;
+
+    double indexDbl;
+    if (!ToInteger(cx, args[0], &indexDbl))
+        return false;
+
+    if (indexDbl < 0 || indexDbl >= table.length()) {
+        JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX);
+        return false;
+    }
+
+    uint32_t index = uint32_t(indexDbl);
+    MOZ_ASSERT(double(index) == indexDbl);
+
+    RootedFunction value(cx);
+    if (!IsExportedFunction(args[1], &value) && !args[1].isNull()) {
+        JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_SET_VALUE);
+        return false;
+    }
+
+    if (!tableObj->initialized()) {
+        if (!value) {
+            args.rval().setUndefined();
+            return true;
+        }
+
+        RootedWasmInstanceObject instanceObj(cx, ExportedFunctionToInstanceObject(value));
+        if (!tableObj->init(cx, instanceObj))
+            return false;
+    }
+
+    const InstanceVector& instanceVector = tableObj->instanceVector();
+    MOZ_ASSERT(instanceVector.length() == table.length());
+
+    if (value) {
+        RootedWasmInstanceObject instanceObj(cx, ExportedFunctionToInstanceObject(value));
+        uint32_t funcIndex = ExportedFunctionToIndex(value);
+
+#ifdef DEBUG
+        RootedFunction f(cx);
+        MOZ_ASSERT(instanceObj->getExportedFunction(cx, instanceObj, funcIndex, &f));
+        MOZ_ASSERT(value == f);
+#endif
+
+        if (instanceVector[index] != instanceObj) {
+            JS_ReportError(cx, "cross-module Table.prototype.set NYI");
+            return false;
+        }
+
+        Instance& instance = instanceObj->instance();
+        const FuncExport& funcExport = instance.metadata().lookupFuncExport(funcIndex);
+        table.array()[index] = instance.codeSegment().code() + funcExport.tableEntryOffset();
+    } else {
+        table.array()[index] = instanceVector[index]->instance().codeSegment().badIndirectCallCode();
+    }
+
+    args.rval().setUndefined();
+    return true;
+}
+
+/* static */ bool
+WasmTableObject::set(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<IsTable, setImpl>(cx, args);
+}
+
 const JSFunctionSpec WasmTableObject::methods[] =
 {
     JS_FN("get", WasmTableObject::get, 1, 0),
+    JS_FN("set", WasmTableObject::set, 2, 0),
     JS_FS_END
 };
 
 Table&
 WasmTableObject::table() const
 {
     return *(Table*)getReservedSlot(TABLE_SLOT).toPrivate();
 }
--- a/js/src/asmjs/WasmJS.h
+++ b/js/src/asmjs/WasmJS.h
@@ -51,25 +51,31 @@ HasCompilerSupport(ExclusiveContext* cx)
 MOZ_MUST_USE bool
 Eval(JSContext* cx, Handle<TypedArrayObject*> code, HandleObject importObj,
      MutableHandle<WasmInstanceObject*> instanceObj);
 
 // The field name of the export object on the instance object.
 
 extern const char InstanceExportField[];
 
-// These accessors are used to implemented the special asm.js semantics of
-// exported wasm functions:
+// These accessors can be used to probe JS values for being an exported wasm
+// function.
 
 extern bool
 IsExportedFunction(JSFunction* fun);
 
+extern bool
+IsExportedFunction(const Value& v, MutableHandleFunction f);
+
 extern Instance&
 ExportedFunctionToInstance(JSFunction* fun);
 
+extern WasmInstanceObject*
+ExportedFunctionToInstanceObject(JSFunction* fun);
+
 extern uint32_t
 ExportedFunctionToIndex(JSFunction* fun);
 
 } // namespace wasm
 
 // 'Wasm' and its one function 'instantiateModule' are transitional APIs and
 // will be removed (replaced by 'WebAssembly') before release.
 
@@ -191,16 +197,18 @@ class WasmTableObject : public NativeObj
     static const unsigned INSTANCE_VECTOR_SLOT = 1;
     static const ClassOps classOps_;
     static void finalize(FreeOp* fop, JSObject* obj);
     static void trace(JSTracer* trc, JSObject* obj);
     static bool lengthGetterImpl(JSContext* cx, const CallArgs& args);
     static bool lengthGetter(JSContext* cx, unsigned argc, Value* vp);
     static bool getImpl(JSContext* cx, const CallArgs& args);
     static bool get(JSContext* cx, unsigned argc, Value* vp);
+    static bool setImpl(JSContext* cx, const CallArgs& args);
+    static bool set(JSContext* cx, unsigned argc, Value* vp);
 
     // InstanceVector has the same length as the Table and assigns, to each
     // element, the instance of the exported function stored in that element.
     using InstanceVector = GCVector<HeapPtr<WasmInstanceObject*>, 0, SystemAllocPolicy>;
     InstanceVector& instanceVector() const;
 
   public:
     static const unsigned RESERVED_SLOTS = 2;
--- a/js/src/jit-test/tests/wasm/import-export.js
+++ b/js/src/jit-test/tests/wasm/import-export.js
@@ -220,16 +220,23 @@ assertEq(e.tbl1, e.tbl2);
 assertEq(e.tbl1.get(0), e.f1);
 assertEq(e.tbl1.get(0), e.tbl1.get(0));
 assertEq(e.tbl1.get(0)(), 1);
 assertEq(e.tbl1.get(1), null);
 assertEq(e.tbl1.get(2), e.tbl1.get(2));
 assertEq(e.tbl1.get(2)(), 2);
 assertEq(e.tbl1.get(3), null);
 assertErrorMessage(() => e.tbl1.get(4), RangeError, /out-of-range index/);
+assertEq(e.tbl1.get(1), null);
+e.tbl1.set(1, e.f3);
+assertEq(e.tbl1.get(1), e.f3);
+e.tbl1.set(1, null);
+assertEq(e.tbl1.get(1), null);
+e.tbl1.set(3, e.f1);
+assertEq(e.tbl1.get(0), e.tbl1.get(3));
 
 // Re-exports:
 
 var code = textToBinary('(module (import "a" "b" (memory 1 1)) (export "foo" memory) (export "bar" memory))');
 var mem = new Memory({initial:1});
 var e = new Instance(new Module(code), {a:{b:mem}}).exports;
 assertEq(mem, e.foo);
 assertEq(mem, e.bar);
--- a/js/src/jit-test/tests/wasm/jsapi.js
+++ b/js/src/jit-test/tests/wasm/jsapi.js
@@ -200,31 +200,56 @@ assertEq(Object.getPrototypeOf(tbl1), ta
 const lengthDesc = Object.getOwnPropertyDescriptor(tableProto, 'length');
 assertEq(typeof lengthDesc.get, "function");
 assertEq(lengthDesc.set, undefined);
 assertEq(lengthDesc.enumerable, false);
 assertEq(lengthDesc.configurable, true);
 
 // 'WebAssembly.Table.prototype.length' getter
 const lengthGetter = lengthDesc.get;
+assertEq(lengthGetter.length, 0);
 assertErrorMessage(() => lengthGetter.call(), TypeError, /called on incompatible undefined/);
 assertErrorMessage(() => lengthGetter.call({}), TypeError, /called on incompatible Object/);
 assertEq(typeof lengthGetter.call(tbl1), "number");
 assertEq(lengthGetter.call(tbl1), 2);
 
 // 'WebAssembly.Table.prototype.get' property
 const getDesc = Object.getOwnPropertyDescriptor(tableProto, 'get');
 assertEq(typeof getDesc.value, "function");
 assertEq(getDesc.enumerable, false);
 assertEq(getDesc.configurable, true);
 
 // 'WebAssembly.Table.prototype.get' method
 const get = getDesc.value;
+assertEq(get.length, 1);
 assertErrorMessage(() => get.call(), TypeError, /called on incompatible undefined/);
 assertErrorMessage(() => get.call({}), TypeError, /called on incompatible Object/);
 assertEq(get.call(tbl1, 0), null);
 assertEq(get.call(tbl1, 1), null);
 assertEq(get.call(tbl1, 1.5), null);
 assertErrorMessage(() => get.call(tbl1, 2), RangeError, /out-of-range index/);
 assertErrorMessage(() => get.call(tbl1, 2.5), RangeError, /out-of-range index/);
 assertErrorMessage(() => get.call(tbl1, -1), RangeError, /out-of-range index/);
 assertErrorMessage(() => get.call(tbl1, Math.pow(2,33)), RangeError, /out-of-range index/);
 assertErrorMessage(() => get.call(tbl1, {valueOf() { throw new Error("hi") }}), Error, "hi");
+
+// 'WebAssembly.Table.prototype.set' property
+const setDesc = Object.getOwnPropertyDescriptor(tableProto, 'set');
+assertEq(typeof setDesc.value, "function");
+assertEq(setDesc.enumerable, false);
+assertEq(setDesc.configurable, true);
+
+// 'WebAssembly.Table.prototype.set' method
+const set = setDesc.value;
+assertEq(set.length, 2);
+assertErrorMessage(() => set.call(), TypeError, /called on incompatible undefined/);
+assertErrorMessage(() => set.call({}), TypeError, /called on incompatible Object/);
+assertErrorMessage(() => set.call(tbl1, 0), TypeError, /requires more than 1 argument/);
+assertErrorMessage(() => set.call(tbl1, 2, null), RangeError, /out-of-range index/);
+assertErrorMessage(() => set.call(tbl1, -1, null), RangeError, /out-of-range index/);
+assertErrorMessage(() => set.call(tbl1, Math.pow(2,33), null), RangeError, /out-of-range index/);
+assertErrorMessage(() => set.call(tbl1, 0, undefined), TypeError, /second argument must be null or an exported WebAssembly Function object/);
+assertErrorMessage(() => set.call(tbl1, 0, {}), TypeError, /second argument must be null or an exported WebAssembly Function object/);
+assertErrorMessage(() => set.call(tbl1, 0, function() {}), TypeError, /second argument must be null or an exported WebAssembly Function object/);
+assertErrorMessage(() => set.call(tbl1, 0, Math.sin), TypeError, /second argument must be null or an exported WebAssembly Function object/);
+assertErrorMessage(() => set.call(tbl1, {valueOf() { throw Error("hai") }}, null), Error, "hai");
+assertEq(set.call(tbl1, 0, null), undefined);
+assertEq(set.call(tbl1, 1, null), undefined);
--- a/js/src/jit-test/tests/wasm/tables.js
+++ b/js/src/jit-test/tests/wasm/tables.js
@@ -135,8 +135,56 @@ e = null;
 gc();
 assertEq(finalizeCount(), 1);
 i = null;
 gc();
 assertEq(finalizeCount(), 1);
 t = null;
 gc();
 assertEq(finalizeCount(), 3);
+
+// Before initialization, a table is not bound to any instance.
+resetFinalizeCount();
+var i = evalText(`(module (func $f0 (result i32) (i32.const 0)) (export "f0" $f0))`);
+var t = new Table({initial:4});
+i.edge = makeFinalizeObserver();
+t.edge = makeFinalizeObserver();
+gc();
+assertEq(finalizeCount(), 0);
+i = null;
+gc();
+assertEq(finalizeCount(), 1);
+t = null;
+gc();
+assertEq(finalizeCount(), 2);
+
+// When a Table is created (uninitialized) and then first assigned, it keeps the
+// first element's Instance alive (as above).
+resetFinalizeCount();
+var i = evalText(`(module (func $f (result i32) (i32.const 42)) (export "f" $f))`);
+var f = i.exports.f;
+var t = new Table({initial:1});
+i.edge = makeFinalizeObserver();
+f.edge = makeFinalizeObserver();
+t.edge = makeFinalizeObserver();
+t.set(0, f);
+assertEq(t.get(0), f);
+assertEq(t.get(0)(), 42);
+gc();
+assertEq(finalizeCount(), 0);
+f = null;
+i.exports = null;
+gc();
+assertEq(finalizeCount(), 1);
+assertEq(t.get(0)(), 42);
+t.get(0).edge = makeFinalizeObserver();
+gc();
+assertEq(finalizeCount(), 2);
+i = null;
+gc();
+assertEq(finalizeCount(), 2);
+t.set(0, null);
+assertEq(t.get(0), null);
+gc();
+assertEq(finalizeCount(), 2);
+t = null;
+gc();
+assertEq(finalizeCount(), 4);
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -346,16 +346,17 @@ MSG_DEF(JSMSG_WASM_DECODE_FAIL,        2
 MSG_DEF(JSMSG_WASM_TEXT_FAIL,          1, JSEXN_SYNTAXERR,   "wasm text error: {0}")
 MSG_DEF(JSMSG_WASM_BAD_IND_CALL,       0, JSEXN_ERR,         "bad wasm indirect call")
 MSG_DEF(JSMSG_WASM_BAD_BUF_ARG,        0, JSEXN_TYPEERR,     "first argument must be an ArrayBuffer or typed array object")
 MSG_DEF(JSMSG_WASM_BAD_MOD_ARG,        0, JSEXN_TYPEERR,     "first argument must be a WebAssembly.Module")
 MSG_DEF(JSMSG_WASM_BAD_DESC_ARG,       1, JSEXN_TYPEERR,     "first argument must be a {0} descriptor")
 MSG_DEF(JSMSG_WASM_BAD_MEM_IMP_SIZE,   0, JSEXN_TYPEERR,     "imported Memory with incompatible size")
 MSG_DEF(JSMSG_WASM_BAD_SIZE,           2, JSEXN_TYPEERR,     "bad {0} {1} size")
 MSG_DEF(JSMSG_WASM_BAD_IMPORT_ARG,     0, JSEXN_TYPEERR,     "second argument, if present, must be an object")
+MSG_DEF(JSMSG_WASM_BAD_SET_VALUE,      0, JSEXN_TYPEERR,     "second argument must be null or an exported WebAssembly Function object")
 MSG_DEF(JSMSG_WASM_UNREACHABLE,        0, JSEXN_ERR,         "unreachable executed")
 MSG_DEF(JSMSG_WASM_INTEGER_OVERFLOW,   0, JSEXN_ERR,         "integer overflow")
 MSG_DEF(JSMSG_WASM_INVALID_CONVERSION, 0, JSEXN_ERR,         "invalid conversion to integer")
 MSG_DEF(JSMSG_WASM_INT_DIVIDE_BY_ZERO, 0, JSEXN_ERR,         "integer divide by zero")
 MSG_DEF(JSMSG_WASM_UNALIGNED_ACCESS,   0, JSEXN_ERR,         "unaligned memory access")
 MSG_DEF(JSMSG_WASM_OVERRECURSED,       0, JSEXN_INTERNALERR, "call stack exhausted")
 
 // Proxy