Bug 1715275 - Add support for a JS WebAssembly.Function object r=rhunt

This introduces a new Constructor WebAssembly.Function which is part of
the WASM JS-API type reflection work. This allows normal JS functions to
be wrapped with a type and used with WebAssembly objects (e.g. Tables).

All exported WebAssembly functions will now be of this type, which nows
provides a .type() method which will expose the arguments and results to
JS.

Differential Revision: https://phabricator.services.mozilla.com/D118712
This commit is contained in:
Jessica Tallon 2021-09-02 15:58:18 +00:00
Родитель 1acb393f08
Коммит 41f7cdd040
15 изменённых файлов: 513 добавлений и 25 удалений

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

@ -122,6 +122,7 @@
REAL(WasmTable, OCLASP(WasmTable)) \
REAL(WasmGlobal, OCLASP(WasmGlobal)) \
REAL(WasmTag, OCLASP(WasmTag)) \
REAL(WasmFunction, CLASP(WasmFunction)) \
REAL(WasmException, OCLASP(WasmException)) \
REAL(FinalizationRegistry, OCLASP(FinalizationRegistry)) \
REAL(WeakRef, OCLASP(WeakRef)) \

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

@ -470,6 +470,8 @@ MSG_DEF(JSMSG_WASM_BAD_EXN_ARG, 0, JSEXN_TYPEERR, "first argument mus
MSG_DEF(JSMSG_WASM_BAD_EXN_PAYLOAD, 0, JSEXN_TYPEERR, "second argument must be an object")
MSG_DEF(JSMSG_WASM_BAD_EXN_PAYLOAD_LEN, 2, JSEXN_TYPEERR, "expected {0} values but got {1}")
MSG_DEF(JSMSG_WASM_BAD_EXN_TAG, 0, JSEXN_TYPEERR, "exception's tag did not match the provided exception tag")
MSG_DEF(JSMSG_WASM_BAD_FUNCTION_VALUE, 0, JSEXN_TYPEERR, "second argument must be a function")
MSG_DEF(JSMSG_WASM_BAD_ARG_TYPE, 0, JSEXN_TYPEERR, "parameters and results must be an iterator of value types")
MSG_DEF(JSMSG_WASM_NO_TRANSFER, 0, JSEXN_TYPEERR, "cannot transfer WebAssembly/asm.js ArrayBuffer")
MSG_DEF(JSMSG_WASM_TEXT_FAIL, 1, JSEXN_SYNTAXERR, "wasm text error: {0}")
MSG_DEF(JSMSG_WASM_MISSING_MAXIMUM, 0, JSEXN_TYPEERR, "'shared' is true but maximum is not specified")

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

@ -405,6 +405,7 @@
"RequireObjectCoercible") \
MACRO_(resolve, resolve, "resolve") \
MACRO_(result, result, "result") \
MACRO_(results, results, "results") \
MACRO_(resumeGenerator, resumeGenerator, "resumeGenerator") \
MACRO_(return, return_, "return") \
MACRO_(revoke, revoke, "revoke") \

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

@ -162,6 +162,7 @@ bool GlobalObject::skipDeselectedConstructor(JSContext* cx, JSProtoKey key) {
case JSProto_WasmTable:
case JSProto_WasmGlobal:
case JSProto_WasmTag:
case JSProto_WasmFunction:
case JSProto_WasmException:
return false;

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

@ -46,6 +46,7 @@
#include "vm/GlobalObject.h" // js::GlobalObject
#include "vm/HelperThreadState.h" // js::PromiseHelperTask
#include "vm/Interpreter.h"
#include "vm/JSFunction.h"
#include "vm/PlainObject.h" // js::PlainObject
#include "vm/PromiseObject.h" // js::PromiseObject
#include "vm/StringType.h"
@ -1050,6 +1051,32 @@ static JSObject* GetWasmConstructorPrototype(JSContext* cx,
return proto;
}
[[nodiscard]] bool ParseValTypeArguments(JSContext* cx, HandleValue src,
ValTypeVector& dest) {
JS::ForOfIterator iterator(cx);
if (!iterator.init(src, JS::ForOfIterator::ThrowOnNonIterable)) {
return false;
}
RootedValue nextParam(cx);
while (true) {
bool done;
if (!iterator.next(&nextParam, &done)) {
return false;
}
if (done) {
break;
}
ValType valType;
if (!ToValType(cx, nextParam, &valType) || !dest.append(valType)) {
return false;
}
}
return true;
}
// ============================================================================
// WebAssembly.Module class and methods
@ -2201,10 +2228,16 @@ bool WasmInstanceObject::getExportedFunction(
if (!name) {
return false;
}
fun.set(NewNativeFunction(cx, WasmCall, numArgs, name,
gc::AllocKind::FUNCTION_EXTENDED, TenuredObject,
FunctionFlags::WASM));
RootedObject proto(cx);
#ifdef ENABLE_WASM_TYPE_REFLECTIONS
proto = GlobalObject::getOrCreatePrototype(cx, JSProto_WasmFunction);
if (!proto) {
return false;
}
#endif
fun.set(NewFunctionWithProto(
cx, WasmCall, numArgs, FunctionFlags::WASM, nullptr, name, proto,
gc::AllocKind::FUNCTION_EXTENDED, TenuredObject));
if (!fun) {
return false;
}
@ -3610,26 +3643,9 @@ bool WasmTagObject::construct(JSContext* cx, unsigned argc, Value* vp) {
return false;
}
JS::ForOfIterator iterator(cx);
if (!iterator.init(paramsVal, JS::ForOfIterator::ThrowOnNonIterable)) {
return false;
}
ValTypeVector params;
RootedValue nextParam(cx);
while (true) {
bool done;
if (!iterator.next(&nextParam, &done)) {
return false;
}
if (done) {
break;
}
ValType argType;
if (!ToValType(cx, nextParam, &argType) || !params.append(argType)) {
return false;
}
if (!ParseValTypeArguments(cx, paramsVal, params)) {
return false;
}
RootedObject proto(cx);
@ -4045,6 +4061,257 @@ ArrayObject& WasmExceptionObject::refs() const {
return getReservedSlot(REFS_SLOT).toObject().as<ArrayObject>();
}
// ============================================================================
// WebAssembly.Function and methods
#ifdef ENABLE_WASM_TYPE_REFLECTIONS
static JSObject* CreateWasmFunctionPrototype(JSContext* cx, JSProtoKey key) {
// WasmFunction's prototype should inherit from JSFunction's prototype.
RootedObject jsProto(
cx, GlobalObject::getOrCreatePrototype(cx, JSProto_Function));
if (!jsProto) {
return nullptr;
}
return GlobalObject::createBlankPrototypeInheriting(cx, &PlainObject::class_,
jsProto);
}
[[nodiscard]] static bool IsWasmFunction(HandleValue v) {
if (!v.isObject()) {
return false;
}
if (!v.toObject().is<JSFunction>()) {
return false;
}
return v.toObject().as<JSFunction>().isWasm();
}
/* static */
static bool ValTypesToArray(JSContext* cx, Handle<jsid> key,
const ValTypeVector& value,
MutableHandle<IdValueVector> dest) {
RootedArrayObject result(cx, NewDenseEmptyArray(cx));
for (ValType v : value) {
RootedString type(cx, UTF8CharsToString(cx, ToString(v).get()));
if (!type) {
return false;
}
if (!NewbornArrayPush(cx, result, StringValue(type))) {
return false;
}
}
return dest.append(IdValuePair(key, ObjectValue(*result)));
}
bool WasmFunctionTypeImpl(JSContext* cx, const CallArgs& args) {
RootedFunction function(cx, &args.thisv().toObject().as<JSFunction>());
// Lookup the type information.
RootedWasmInstanceObject instanceObj(
cx, ExportedFunctionToInstanceObject(function));
uint32_t funcIndex = ExportedFunctionToFuncIndex(function);
Instance& instance = instanceObj->instance();
const FuncType& ft = instance.metadata(instance.code().bestTier())
.lookupFuncExport(funcIndex)
.funcType();
const ValTypeVector& parameters = ft.args();
RootedId parametersId(cx, NameToId(cx->names().parameters));
Rooted<IdValueVector> props(cx, IdValueVector(cx));
if (!ValTypesToArray(cx, parametersId, parameters, &props)) {
return false;
}
const ValTypeVector& results = ft.results();
RootedId resultsId(cx, NameToId(cx->names().results));
if (!ValTypesToArray(cx, resultsId, results, &props)) {
return false;
}
RootedObject functionType(
cx, NewPlainObjectWithProperties(cx, props.begin(), props.length(),
GenericObject));
if (!functionType) {
return false;
}
args.rval().setObject(*functionType);
return true;
}
bool WasmFunctionType(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
return CallNonGenericMethod<IsWasmFunction, WasmFunctionTypeImpl>(cx, args);
}
JSFunction* WasmFunctionCreate(JSContext* cx, HandleFunction fun,
wasm::ValTypeVector&& params,
wasm::ValTypeVector&& results,
HandleObject proto) {
MOZ_RELEASE_ASSERT(!IsWasmExportedFunction(fun));
// We want to import the function to a wasm module and then export it again so
// that it behaves exactly like a normal wasm function and can be used like
// one in wasm tables. Below we create the wasm module with fun as it's import
// then exporting it.
FeatureOptions options;
ScriptedCaller scriptedCaller;
SharedCompileArgs compileArgs =
CompileArgs::build(cx, std::move(scriptedCaller), options);
ModuleEnvironment moduleEnv(compileArgs->features);
CompilerEnvironment compilerEnv(CompileMode::Once, Tier::Optimized,
OptimizedBackend::Ion, DebugEnabled::False);
compilerEnv.computeParameters();
// Add the Import for the function
FuncType funcType = FuncType(std::move(params), std::move(results));
TypeDef funcTypeDef = TypeDef(std::move(funcType));
if (!moduleEnv.types.append(std::move(funcTypeDef))) {
return nullptr;
}
if (!moduleEnv.typeIds.resize(1)) {
return nullptr;
}
FuncDesc funcDesc =
FuncDesc(&moduleEnv.types[0].funcType(), &moduleEnv.typeIds[0], 0);
if (!moduleEnv.funcs.append(funcDesc) ||
!moduleEnv.funcImportGlobalDataOffsets.resize(1)) {
return nullptr;
}
moduleEnv.declareFuncExported(0, false, false);
// We will be looking up and using the function in the future by index so the
// name doesn't matter.
CacheableChars fieldName = DuplicateString("");
if (!moduleEnv.exports.emplaceBack(std::move(fieldName), 0,
DefinitionKind::Function)) {
return nullptr;
}
ModuleGenerator mg(*compileArgs, &moduleEnv, &compilerEnv, nullptr, nullptr);
if (!mg.init(nullptr)) {
return nullptr;
}
ShareableBytes* sharableBytes = cx->new_<ShareableBytes>();
// We're not compiling any function definitions.
if (!mg.finishFuncDefs()) {
return nullptr;
}
SharedModule wasmModule = mg.finishModule(*sharableBytes);
// Instantiate the module.
Rooted<ImportValues> imports(cx);
imports.get().funcs.append(fun);
RootedWasmInstanceObject instanceObj(cx);
if (!wasmModule->instantiate(cx, imports.get(), nullptr, &instanceObj)) {
MOZ_ASSERT(cx->isThrowingOutOfMemory());
return nullptr;
}
// Get the exported function which wraps the JS function to return.
RootedFunction exportedFun(cx);
instanceObj->getExportedFunction(cx, instanceObj, 0, &exportedFun);
return exportedFun;
}
bool WasmFunctionConstruct(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!ThrowIfNotConstructing(cx, args, "WebAssembly.Function")) {
return false;
}
if (!args.requireAtLeast(cx, "WebAssembly.Function", 2)) {
return false;
}
ValTypeVector params;
ValTypeVector results;
if (!args[0].isObject()) {
return false;
}
RootedObject obj(cx, &args[0].toObject());
RootedId parametersId(cx, NameToId(cx->names().parameters));
RootedValue parametersVal(cx);
if (!GetProperty(cx, obj, obj, parametersId, &parametersVal)) {
return false;
}
if (!ParseValTypeArguments(cx, parametersVal, params)) {
JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
JSMSG_WASM_BAD_ARG_TYPE);
return false;
}
RootedId resultsId(cx, NameToId(cx->names().results));
RootedValue resultsVal(cx);
if (!GetProperty(cx, obj, obj, resultsId, &resultsVal)) {
return false;
}
if (!ParseValTypeArguments(cx, resultsVal, results)) {
JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
JSMSG_WASM_BAD_ARG_TYPE);
return false;
}
if (!args[1].isObject() || !args[1].toObject().is<JSFunction>()) {
JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
JSMSG_WASM_BAD_FUNCTION_VALUE);
return false;
}
RootedFunction funcObj(cx, &args[1].toObject().as<JSFunction>());
RootedObject proto(cx);
if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_WasmFunction,
&proto)) {
return false;
}
if (!proto) {
proto = GlobalObject::getOrCreatePrototype(cx, JSProto_WasmFunction);
}
RootedFunction function(cx, WasmFunctionCreate(cx, funcObj, std::move(params),
std::move(results), proto));
if (!function) {
return false;
}
args.rval().setObject(*function);
return true;
}
static JSObject* CreateWasmFunctionConstructor(JSContext* cx, JSProtoKey key) {
RootedObject proto(
cx, GlobalObject::getOrCreateFunctionConstructor(cx, cx->global()));
if (!proto) {
return nullptr;
}
HandlePropertyName name = cx->names().WasmFunction;
return NewFunctionWithProto(cx, WasmFunctionConstruct, 1,
FunctionFlags::NATIVE_CTOR, nullptr, name, proto,
gc::AllocKind::FUNCTION, TenuredObject);
}
const JSFunctionSpec WasmFunctionMethods[] = {
JS_FN("type", WasmFunctionType, 0, 0), JS_FS_END};
const ClassSpec WasmFunctionClassSpec = {CreateWasmFunctionConstructor,
CreateWasmFunctionPrototype,
nullptr,
nullptr,
WasmFunctionMethods,
nullptr,
nullptr,
ClassSpec::DontDefineConstructor};
const JSClass js::WasmFunctionClass = {
"WebAssembly.Function", 0, JS_NULL_CLASS_OPS, &WasmFunctionClassSpec};
#else
const JSClass js::WasmFunctionClass = {"WebAssembly.Function", 0,
JS_NULL_CLASS_OPS, JS_NULL_CLASS_SPEC};
#endif
// ============================================================================
// WebAssembly class and static methods
@ -5067,6 +5334,9 @@ static bool WebAssemblyClassFinish(JSContext* cx, HandleObject object,
{"CompileError", GetExceptionProtoKey(JSEXN_WASMCOMPILEERROR)},
{"LinkError", GetExceptionProtoKey(JSEXN_WASMLINKERROR)},
{"RuntimeError", GetExceptionProtoKey(JSEXN_WASMRUNTIMEERROR)},
#ifdef ENABLE_WASM_TYPE_REFLECTIONS
{"Function", JSProto_WasmFunction},
#endif
};
RootedValue ctorValue(cx);
RootedId id(cx);

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

@ -573,6 +573,8 @@ class WasmNamespaceObject : public NativeObject {
static const ClassSpec classSpec_;
};
extern const JSClass WasmFunctionClass;
} // namespace js
#endif // wasm_js_h

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

@ -0,0 +1,12 @@
[call.tentative.any.worker.html]
[test calling function]
expected:
if release_or_beta: FAIL
[call.tentative.any.html]
[test calling function]
expected:
if release_or_beta: FAIL
[call.tentative.any.js]
[test calling function]
expected:
if release_or_beta: FAIL

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

@ -0,0 +1,57 @@
[constructor.tentative.any.worker.html]
[construct with JS function]
expected:
if release_or_beta: FAIL
[fail with missing parameters]
expected:
if release_or_beta: FAIL
[fail with missing results]
expected:
if release_or_beta: FAIL
[fail with non-string parameters & results]
expected:
if release_or_beta: FAIL
[fail with non-existant parameter and result type]
expected:
if release_or_beta: FAIL
[fail with non-function object]
expected:
if release_or_beta: FAIL
[constructor.tentative.any.html]
[construct with JS function]
expected:
if release_or_beta: FAIL
[fail with missing parameters]
expected:
if release_or_beta: FAIL
[fail with missing results]
expected:
if release_or_beta: FAIL
[fail with non-string parameters & results]
expected:
if release_or_beta: FAIL
[fail with non-existant parameter and result type]
expected:
if release_or_beta: FAIL
[fail with non-function object]
expected:
if release_or_beta: FAIL
[constructor.tentative.any.js]
[construct with JS function]
expected:
if release_or_beta: FAIL
[fail with missing parameters]
expected:
if release_or_beta: FAIL
[fail with missing results]
expected:
if release_or_beta: FAIL
[fail with non-string parameters & results]
expected:
if release_or_beta: FAIL
[fail with non-existant parameter and result type]
expected:
if release_or_beta: FAIL
[fail with non-function object]
expected:
if release_or_beta: FAIL

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

@ -0,0 +1,12 @@
[table.tentative.any.worker.html]
[Test insertion into table]
expected:
if release_or_beta: FAIL
[table.tentative.any.html]
[Test insertion into table]
expected:
if release_or_beta: FAIL
[table.tentative.any.js]
[Test insertion into table]
expected:
if release_or_beta: FAIL

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

@ -0,0 +1,21 @@
[type.tentative.any.worker.html]
[Check empty results and parameters]
expected:
if release_or_beta: FAIL
[Check all types]
expected:
if release_or_beta: FAIL
[type.tentative.any.html]
[Check empty results and parameters]
expected:
if release_or_beta: FAIL
[Check all types]
expected:
if release_or_beta: FAIL
[type.tentative.any.js]
[Check empty results and parameters]
expected:
if release_or_beta: FAIL
[Check all types]
expected:
if release_or_beta: FAIL

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

@ -17,8 +17,13 @@ function assert_function_length(fn, length, description) {
}
function assert_exported_function(fn, { name, length }, description) {
assert_equals(Object.getPrototypeOf(fn), Function.prototype,
`${description}: prototype`);
if (WebAssembly.Function === undefined) {
assert_equals(Object.getPrototypeOf(fn), Function.prototype,
`${description}: prototype`);
} else {
assert_equals(Object.getPrototypeOf(fn), WebAssembly.Function.prototype,
`${description}: prototype`);
}
assert_function_name(fn, name, description);
assert_function_length(fn, length, description);

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

@ -0,0 +1,11 @@
// META: global=window,dedicatedworker,jsshell
// META: script=/wasm/jsapi/assertions.js
function addxy(x, y) {
return x + y
}
test(() => {
var fun = new WebAssembly.Function({parameters: ["i32", "i32"], results: ["i32"]}, addxy);
assert_equals(fun(1, 2), 3)
}, "test calling function")

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

@ -0,0 +1,35 @@
// META: global=window,dedicatedworker,jsshell
// META: script=/wasm/jsapi/assertions.js
function addxy(x, y) {
return x + y
}
test(() => {
var fun = new WebAssembly.Function({parameters: ["i32", "i32"], results: ["i32"]}, addxy);
assert_true(fun instanceof WebAssembly.Function)
}, "construct with JS function")
test(() => {
assert_throws_js(TypeError, () => new WebAssembly.Function({parameters: []}, addxy))
}, "fail with missing results")
test(() => {
assert_throws_js(TypeError, () => new WebAssembly.Function({results: []}, addxy))
}, "fail with missing parameters")
test(() => {
assert_throws_js(TypeError, () => new WebAssembly.Function({parameters: [1], results: [true]}, addxy))
}, "fail with non-string parameters & results")
test(() => {
assert_throws_js(TypeError, () => new WebAssembly.Function({parameters: ["invalid"], results: ["invalid"]}, addxy))
}, "fail with non-existent parameter and result type")
test(() => {
assert_throws_js(TypeError, () => new WebAssembly.Function({parameters: [], results: []}, 72))
}, "fail with non-function object")
test(() => {
assert_throws_js(TypeError, () => new WebAssembly.Function({parameters: [], results: []}, {}))
}, "fail to construct with non-callable object")

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

@ -0,0 +1,30 @@
// META: global=window,dedicatedworker,jsshell
// META: script=/wasm/jsapi/assertions.js
function testfunc(n) {}
test(() => {
var table = new WebAssembly.Table({element: "anyfunc", initial: 3})
var func1 = new WebAssembly.Function({parameters: ["i32"], results: []}, testfunc)
table.set(0, func1)
var func2 = new WebAssembly.Function({parameters: ["f32"], results: []}, testfunc)
table.set(1, func2)
var func3 = new WebAssembly.Function({parameters: ["i64"], results: []}, testfunc)
table.set(2, func3)
var first = table.get(0)
assert_true(first instanceof WebAssembly.Function)
assert_equals(first, func1)
assert_equals(first.type().parameters[0], func1.type().parameters[0])
var second = table.get(1)
assert_true(second instanceof WebAssembly.Function)
assert_equals(second, func2)
assert_equals(second.type().parameters[0], func2.type().parameters[0])
var third = table.get(2)
assert_true(third instanceof WebAssembly.Function)
assert_equals(third, func3)
assert_equals(third.type().parameters[0], func3.type().parameters[0])
}, "Test insertion into table")

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

@ -0,0 +1,28 @@
// META: global=window,dedicatedworker,jsshell
// META: script=/wasm/jsapi/assertions.js
function addNumbers(x, y, z) {
return x+y+z;
}
function doNothing() {}
function assert_function(functype, func) {
var wasmFunc = new WebAssembly.Function(functype, func);
assert_equals(functype.parameters.length, wasmFunc.type().parameters.length);
for(let i = 0; i < functype.parameters.length; i++) {
assert_equals(functype.parameters[i], wasmFunc.type().parameters[i]);
}
assert_equals(functype.results.length, wasmFunc.type().results.length);
for(let i = 0; i < functype.results.length; i++) {
assert_equals(functype.results[i], wasmFunc.type().results[i]);
}
}
test(() => {
assert_function({results: [], parameters: []}, doNothing);
}, "Check empty results and parameters")
test(() => {
assert_function({results: ["f64"], parameters: ["i32", "i64", "f32"]}, addNumbers)
}, "Check all types")