WasmBuiltinModule.cpp (16204B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * 4 * Copyright 2021 Mozilla Foundation 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 #include "wasm/WasmBuiltinModule.h" 20 21 #include "util/Text.h" 22 #include "vm/GlobalObject.h" 23 24 #include "wasm/WasmBuiltinModuleGenerated.h" 25 #include "wasm/WasmFeatures.h" 26 #include "wasm/WasmGenerator.h" 27 #include "wasm/WasmJS.h" 28 #include "wasm/WasmModule.h" 29 #include "wasm/WasmOpIter.h" 30 #include "wasm/WasmStaticTypeDefs.h" 31 #include "wasm/WasmValidate.h" 32 33 using namespace js; 34 using namespace js::wasm; 35 36 using mozilla::Maybe; 37 using mozilla::Nothing; 38 using mozilla::Some; 39 40 BuiltinModuleFuncs* BuiltinModuleFuncs::singleton_ = nullptr; 41 42 [[nodiscard]] bool BuiltinModuleFunc::init( 43 const RefPtr<TypeContext>& types, mozilla::Span<const ValType> params, 44 Maybe<ValType> result, bool usesMemory, const SymbolicAddressSignature* sig, 45 BuiltinInlineOp inlineOp, const char* exportName) { 46 // This builtin must not have been initialized yet. 47 MOZ_ASSERT(!recGroup_); 48 49 // Initialize the basic fields 50 exportName_ = exportName; 51 sig_ = sig; 52 usesMemory_ = usesMemory; 53 inlineOp_ = inlineOp; 54 55 // Create a function type for the given params and result 56 ValTypeVector paramVec; 57 if (!paramVec.append(params.data(), params.data() + params.size())) { 58 return false; 59 } 60 ValTypeVector resultVec; 61 if (result.isSome() && !resultVec.append(*result)) { 62 return false; 63 } 64 const TypeDef* typeDef = 65 types->addType(FuncType(std::move(paramVec), std::move(resultVec))); 66 if (!typeDef) { 67 return false; 68 } 69 recGroup_ = &typeDef->recGroup(); 70 return true; 71 } 72 73 bool BuiltinModuleFuncs::init() { 74 singleton_ = js_new<BuiltinModuleFuncs>(); 75 if (!singleton_) { 76 return false; 77 } 78 79 RefPtr<TypeContext> types = js_new<TypeContext>(); 80 if (!types) { 81 return false; 82 } 83 84 #define VISIT_BUILTIN_FUNC(op, export, sa_name, abitype, needs_thunk, entry, \ 85 uses_memory, inline_op, ...) \ 86 const ValType op##Params[] = \ 87 DECLARE_BUILTIN_MODULE_FUNC_PARAM_VALTYPES_##op; \ 88 Maybe<ValType> op##Result = DECLARE_BUILTIN_MODULE_FUNC_RESULT_VALTYPE_##op; \ 89 if (!singleton_->funcs_[BuiltinModuleFuncId::op].init( \ 90 types, mozilla::Span<const ValType>(op##Params), op##Result, \ 91 uses_memory, &SASig##sa_name, inline_op, export)) { \ 92 return false; \ 93 } 94 FOR_EACH_BUILTIN_MODULE_FUNC(VISIT_BUILTIN_FUNC) 95 #undef VISIT_BUILTIN_FUNC 96 97 return true; 98 } 99 100 void BuiltinModuleFuncs::destroy() { 101 if (!singleton_) { 102 return; 103 } 104 js_delete(singleton_); 105 singleton_ = nullptr; 106 } 107 108 bool EncodeFuncBody(const BuiltinModuleFunc& builtinModuleFunc, 109 BuiltinModuleFuncId id, Bytes* body) { 110 Encoder encoder(*body); 111 if (!EncodeLocalEntries(encoder, ValTypeVector())) { 112 return false; 113 } 114 const FuncType* funcType = builtinModuleFunc.funcType(); 115 for (uint32_t i = 0; i < funcType->args().length(); i++) { 116 if (!encoder.writeOp(Op::LocalGet) || !encoder.writeVarU32(i)) { 117 return false; 118 } 119 } 120 if (!encoder.writeOp(MozOp::CallBuiltinModuleFunc)) { 121 return false; 122 } 123 if (!encoder.writeVarU32(uint32_t(id))) { 124 return false; 125 } 126 return encoder.writeOp(Op::End); 127 } 128 129 // Descriptor of how a builtin should use memory. 130 struct BuiltinMemory { 131 // Whether the memory is shared or not. 132 Shareable shared; 133 // Optional import name for the memory. If not provided, will fall back to 134 // "" "memory" as the import name. 135 const Import* import; 136 137 BuiltinMemory(Shareable shared, const Import* import) 138 : shared(shared), import(import) {} 139 }; 140 141 bool CompileBuiltinModule(JSContext* cx, 142 const mozilla::Span<BuiltinModuleFuncId> ids, 143 mozilla::Maybe<BuiltinMemory> memory, 144 MutableHandle<WasmModuleObject*> result) { 145 // Create the options manually, enabling intrinsics 146 FeatureOptions featureOptions; 147 featureOptions.isBuiltinModule = true; 148 149 // Initialize the compiler environment, choosing the best tier possible 150 SharedCompileArgs compileArgs = CompileArgs::buildAndReport( 151 cx, ScriptedCaller(), featureOptions, /* reportOOM */ true); 152 if (!compileArgs) { 153 return false; 154 } 155 CompilerEnvironment compilerEnv( 156 CompileMode::Once, IonAvailable(cx) ? Tier::Optimized : Tier::Baseline, 157 DebugEnabled::False); 158 compilerEnv.computeParameters(); 159 160 // Build a module metadata struct 161 MutableModuleMetadata moduleMeta = js_new<ModuleMetadata>(); 162 if (!moduleMeta || !moduleMeta->init(*compileArgs)) { 163 ReportOutOfMemory(cx); 164 return false; 165 } 166 MutableCodeMetadata codeMeta = moduleMeta->codeMeta; 167 168 if (memory.isSome()) { 169 // Add (import (memory 0)) using the specified import name, or else fall 170 // back to "" "memory" if no import was specified. 171 CacheableName moduleString; 172 CacheableName fieldString; 173 if (!memory->import) { 174 // Keep moduleString empty, using "memory" for the fieldString 175 if (!CacheableName::fromUTF8Chars("memory", &fieldString)) { 176 ReportOutOfMemory(cx); 177 return false; 178 } 179 } else { 180 // The provided import name must be a memory import. 181 MOZ_ASSERT(memory->import->kind == DefinitionKind::Memory); 182 if (!memory->import->module.clone(&moduleString) || 183 !memory->import->field.clone(&fieldString)) { 184 ReportOutOfMemory(cx); 185 return false; 186 } 187 } 188 189 if (!moduleMeta->imports.append(Import(std::move(moduleString), 190 std::move(fieldString), 191 DefinitionKind::Memory))) { 192 ReportOutOfMemory(cx); 193 return false; 194 } 195 if (!codeMeta->memories.append(MemoryDesc( 196 Limits(0, Nothing(), memory->shared, PageSize::Standard)))) { 197 ReportOutOfMemory(cx); 198 return false; 199 } 200 } 201 202 // Add (type (func (params ...))) for each func. The function types will 203 // be deduplicated by the runtime. 204 for (uint32_t funcIndex = 0; funcIndex < ids.size(); funcIndex++) { 205 const BuiltinModuleFuncId& id = ids[funcIndex]; 206 const BuiltinModuleFunc& builtinModuleFunc = 207 BuiltinModuleFuncs::getFromId(id); 208 209 SharedRecGroup recGroup = builtinModuleFunc.recGroup(); 210 MOZ_ASSERT(recGroup->numTypes() == 1); 211 if (!codeMeta->types->addRecGroup(recGroup)) { 212 ReportOutOfMemory(cx); 213 return false; 214 } 215 } 216 217 // Add all static type defs to the type context so that we can always look up 218 // their index. This must come after the func types so as not to interfere 219 // with funcIndex. 220 if (!StaticTypeDefs::addAllToTypeContext(codeMeta->types)) { 221 ReportOutOfMemory(cx); 222 return false; 223 } 224 225 // Add (func (type $i)) declarations. Do this after all types have been added 226 // as the function declaration metadata uses pointers into the type vectors 227 // that must be stable. 228 for (uint32_t funcIndex = 0; funcIndex < ids.size(); funcIndex++) { 229 FuncDesc decl(funcIndex); 230 if (!codeMeta->funcs.append(decl)) { 231 ReportOutOfMemory(cx); 232 return false; 233 } 234 codeMeta->funcs[funcIndex].declareFuncExported(/* eager */ true, 235 /* canRefFunc */ true); 236 } 237 238 // Add (export "$name" (func $i)) declarations. 239 for (uint32_t funcIndex = 0; funcIndex < ids.size(); funcIndex++) { 240 const BuiltinModuleFunc& builtinModuleFunc = 241 BuiltinModuleFuncs::getFromId(ids[funcIndex]); 242 243 CacheableName exportName; 244 if (!CacheableName::fromUTF8Chars(builtinModuleFunc.exportName(), 245 &exportName) || 246 !moduleMeta->exports.append(Export(std::move(exportName), funcIndex, 247 DefinitionKind::Function))) { 248 ReportOutOfMemory(cx); 249 return false; 250 } 251 } 252 253 if (!moduleMeta->prepareForCompile(compilerEnv.mode())) { 254 return false; 255 } 256 257 // Compile the module functions 258 UniqueChars error; 259 ModuleGenerator mg(*codeMeta, compilerEnv, compilerEnv.initialState(), 260 nullptr, &error, nullptr); 261 if (!mg.initializeCompleteTier()) { 262 ReportOutOfMemory(cx); 263 return false; 264 } 265 266 // Prepare and compile function bodies 267 Vector<Bytes, 1, SystemAllocPolicy> bodies; 268 if (!bodies.reserve(ids.size())) { 269 ReportOutOfMemory(cx); 270 return false; 271 } 272 uint32_t funcBytecodeOffset = CallSite::FIRST_VALID_BYTECODE_OFFSET; 273 for (uint32_t funcIndex = 0; funcIndex < ids.size(); funcIndex++) { 274 BuiltinModuleFuncId id = ids[funcIndex]; 275 const BuiltinModuleFunc& builtinModuleFunc = 276 BuiltinModuleFuncs::getFromId(ids[funcIndex]); 277 278 // Compilation may be done using other threads, ModuleGenerator requires 279 // that function bodies live until after finishFuncDefs(). 280 bodies.infallibleAppend(Bytes()); 281 Bytes& bytecode = bodies.back(); 282 283 // Encode function body that will call the builtinModuleFunc using our 284 // builtin opcode, and launch a compile task 285 if (!EncodeFuncBody(builtinModuleFunc, id, &bytecode) || 286 !mg.compileFuncDef(funcIndex, funcBytecodeOffset, bytecode.begin(), 287 bytecode.begin() + bytecode.length())) { 288 // This must be an OOM and will be reported by the caller 289 MOZ_ASSERT(!error); 290 ReportOutOfMemory(cx); 291 return false; 292 } 293 funcBytecodeOffset += bytecode.length(); 294 } 295 296 // Finish and block on function compilation 297 if (!mg.finishFuncDefs()) { 298 // This must be an OOM and will be reported by the caller 299 MOZ_ASSERT(!error); 300 ReportOutOfMemory(cx); 301 return false; 302 } 303 304 // Finish the module 305 SharedModule module = mg.finishModule(BytecodeBufferOrSource(), *moduleMeta, 306 /*maybeCompleteTier2Listener=*/nullptr); 307 if (!module) { 308 ReportOutOfMemory(cx); 309 return false; 310 } 311 312 // Create a WasmModuleObject for the module, and return it 313 RootedObject proto( 314 cx, GlobalObject::getOrCreatePrototype(cx, JSProto_WasmModule)); 315 if (!proto) { 316 ReportOutOfMemory(cx); 317 return false; 318 } 319 result.set(WasmModuleObject::create(cx, *module, proto)); 320 return !!result; 321 } 322 323 static BuiltinModuleFuncId SelfTestFuncs[] = {BuiltinModuleFuncId::I8VecMul}; 324 325 #ifdef ENABLE_WASM_MOZ_INTGEMM 326 static BuiltinModuleFuncId IntGemmFuncs[] = { 327 BuiltinModuleFuncId::I8PrepareB, 328 BuiltinModuleFuncId::I8PrepareBFromTransposed, 329 BuiltinModuleFuncId::I8PrepareBFromQuantizedTransposed, 330 BuiltinModuleFuncId::I8PrepareA, 331 BuiltinModuleFuncId::I8PrepareBias, 332 BuiltinModuleFuncId::I8MultiplyAndAddBias, 333 BuiltinModuleFuncId::I8SelectColumnsOfB}; 334 // Name chosen to maintain compatibility with existing wasm files, so nothing 335 // needs to be rebuilt. 336 static const char* IntGemmModuleName = "wasm_gemm"; 337 #endif // ENABLE_WASM_MOZ_INTGEMM 338 339 static BuiltinModuleFuncId JSStringFuncs[] = { 340 BuiltinModuleFuncId::StringTest, 341 BuiltinModuleFuncId::StringCast, 342 BuiltinModuleFuncId::StringFromCharCodeArray, 343 BuiltinModuleFuncId::StringIntoCharCodeArray, 344 BuiltinModuleFuncId::StringFromCharCode, 345 BuiltinModuleFuncId::StringFromCodePoint, 346 BuiltinModuleFuncId::StringCharCodeAt, 347 BuiltinModuleFuncId::StringCodePointAt, 348 BuiltinModuleFuncId::StringLength, 349 BuiltinModuleFuncId::StringConcat, 350 BuiltinModuleFuncId::StringSubstring, 351 BuiltinModuleFuncId::StringEquals, 352 BuiltinModuleFuncId::StringCompare}; 353 static const char* JSStringModuleName = "wasm:js-string"; 354 355 Maybe<BuiltinModuleId> wasm::ImportMatchesBuiltinModule( 356 mozilla::Span<const char> importName, 357 const BuiltinModuleIds& enabledBuiltins) { 358 if (enabledBuiltins.jsString && 359 importName == mozilla::MakeStringSpan(JSStringModuleName)) { 360 return Some(BuiltinModuleId::JSString); 361 } 362 if (enabledBuiltins.jsStringConstants && 363 importName == 364 mozilla::MakeStringSpan( 365 enabledBuiltins.jsStringConstantsNamespace->chars.get())) { 366 return Some(BuiltinModuleId::JSStringConstants); 367 } 368 #ifdef ENABLE_WASM_MOZ_INTGEMM 369 if (enabledBuiltins.intGemm && 370 importName == mozilla::MakeStringSpan(IntGemmModuleName)) { 371 return Some(BuiltinModuleId::IntGemm); 372 } 373 #endif // ENABLE_WASM_MOZ_INTGEMM 374 // Not supported for implicit instantiation yet 375 MOZ_RELEASE_ASSERT(!enabledBuiltins.selfTest); 376 return Nothing(); 377 } 378 379 bool wasm::ImportMatchesBuiltinModuleFunc(mozilla::Span<const char> importName, 380 BuiltinModuleId module, 381 const BuiltinModuleFunc** matchedFunc, 382 BuiltinModuleFuncId* matchedFuncId) { 383 // Imported string constants don't define any functions 384 if (module == BuiltinModuleId::JSStringConstants) { 385 return false; 386 } 387 388 #ifdef ENABLE_WASM_MOZ_INTGEMM 389 if (module == BuiltinModuleId::IntGemm) { 390 for (BuiltinModuleFuncId funcId : IntGemmFuncs) { 391 const BuiltinModuleFunc& func = BuiltinModuleFuncs::getFromId(funcId); 392 if (importName == mozilla::MakeStringSpan(func.exportName())) { 393 *matchedFunc = &func; 394 *matchedFuncId = funcId; 395 return true; 396 } 397 } 398 } 399 #endif 400 401 // That leaves only the wasm:js-string module that defines functions at this 402 // point, and is supported by implicit instantiation. 403 MOZ_RELEASE_ASSERT(module == BuiltinModuleId::JSString); 404 for (BuiltinModuleFuncId funcId : JSStringFuncs) { 405 const BuiltinModuleFunc& func = BuiltinModuleFuncs::getFromId(funcId); 406 if (importName == mozilla::MakeStringSpan(func.exportName())) { 407 *matchedFunc = &func; 408 *matchedFuncId = funcId; 409 return true; 410 } 411 } 412 return false; 413 } 414 415 bool wasm::CompileBuiltinModule(JSContext* cx, BuiltinModuleId module, 416 const Import* moduleMemoryImport, 417 MutableHandle<WasmModuleObject*> result) { 418 switch (module) { 419 case BuiltinModuleId::SelfTest: 420 return CompileBuiltinModule( 421 cx, SelfTestFuncs, Some(BuiltinMemory(Shareable::False, nullptr)), 422 result); 423 #ifdef ENABLE_WASM_MOZ_INTGEMM 424 case BuiltinModuleId::IntGemm: 425 return CompileBuiltinModule( 426 cx, IntGemmFuncs, 427 Some(BuiltinMemory(Shareable::False, moduleMemoryImport)), result); 428 #endif // ENABLE_WASM_MOZ_INTGEMM 429 case BuiltinModuleId::JSString: 430 return CompileBuiltinModule(cx, JSStringFuncs, Nothing(), result); 431 case BuiltinModuleId::JSStringConstants: 432 MOZ_CRASH(); 433 default: 434 MOZ_CRASH(); 435 } 436 } 437 438 bool wasm::InstantiateBuiltinModule(JSContext* cx, BuiltinModuleId module, 439 const Import* moduleMemoryImport, 440 HandleObject importObj, 441 MutableHandleObject result) { 442 Rooted<WasmModuleObject*> moduleObj(cx); 443 if (!CompileBuiltinModule(cx, module, moduleMemoryImport, &moduleObj)) { 444 ReportOutOfMemory(cx); 445 return false; 446 } 447 Rooted<ImportValues> imports(cx); 448 if (!wasm::GetImports(cx, moduleObj->module(), importObj, 449 imports.address())) { 450 return false; 451 } 452 453 Rooted<WasmInstanceObject*> instanceObj(cx); 454 RootedObject instanceProto(cx); 455 if (!moduleObj->module().instantiate(cx, *imports.address(), instanceProto, 456 &instanceObj)) { 457 MOZ_RELEASE_ASSERT(cx->isThrowingOutOfMemory()); 458 return false; 459 } 460 result.set(&instanceObj->exportsObj()); 461 return true; 462 }