/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "WorkletModuleLoader.h" #include "js/CompileOptions.h" // JS::InstantiateOptions #include "js/experimental/JSStencil.h" // JS::CompileModuleScriptToStencil, JS::InstantiateModuleStencil #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/loader/ModuleLoadRequest.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPrefs_javascript.h" #include "mozilla/dom/StructuredCloneHolder.h" #include "mozilla/dom/Worklet.h" #include "mozilla/dom/WorkletFetchHandler.h" #include "nsStringBundle.h" using JS::loader::ModuleLoadRequest; using JS::loader::ResolveError; namespace mozilla::dom::loader { ////////////////////////////////////////////////////////////// // WorkletScriptLoader ////////////////////////////////////////////////////////////// NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkletScriptLoader) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTION(WorkletScriptLoader) NS_IMPL_CYCLE_COLLECTING_ADDREF(WorkletScriptLoader) NS_IMPL_CYCLE_COLLECTING_RELEASE(WorkletScriptLoader) nsresult WorkletScriptLoader::FillCompileOptionsForRequest( JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions, JS::MutableHandle aIntroductionScript) { aOptions->setIntroductionType("Worklet"); aOptions->setFileAndLine(aRequest->mURL.get(), 1); aOptions->setIsRunOnce(true); aOptions->setNoScriptRval(true); return NS_OK; } ////////////////////////////////////////////////////////////// // WorkletModuleLoader ////////////////////////////////////////////////////////////// NS_IMPL_ADDREF_INHERITED(WorkletModuleLoader, ModuleLoaderBase) NS_IMPL_RELEASE_INHERITED(WorkletModuleLoader, ModuleLoaderBase) NS_IMPL_CYCLE_COLLECTION_INHERITED(WorkletModuleLoader, ModuleLoaderBase, mFetchingRequests) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkletModuleLoader) NS_INTERFACE_MAP_END_INHERITING(ModuleLoaderBase) WorkletModuleLoader::WorkletModuleLoader(WorkletScriptLoader* aScriptLoader, nsIGlobalObject* aGlobalObject) : ModuleLoaderBase(aScriptLoader, aGlobalObject) { // This should be constructed on a worklet thread. MOZ_ASSERT(!NS_IsMainThread()); } already_AddRefed WorkletModuleLoader::CreateRequest( JSContext* aCx, nsIURI* aURI, JS::Handle aModuleRequest, JS::Handle aHostDefined, JS::Handle aPayload, bool aIsDynamicImport, ScriptFetchOptions* aOptions, dom::ReferrerPolicy aReferrerPolicy, nsIURI* aBaseURL, const dom::SRIMetadata& aSriMetadata) { JS::ModuleType moduleType = GetModuleRequestType(aCx, aModuleRequest); ModuleLoadRequest* root = nullptr; MOZ_ASSERT(!aHostDefined.isUndefined()); root = static_cast(aHostDefined.toPrivate()); MOZ_ASSERT(root); WorkletLoadContext* context = root->mLoadContext->AsWorkletContext(); const nsMainThreadPtrHandle& handlerRef = context->GetHandlerRef(); RefPtr loadContext = new WorkletLoadContext(handlerRef); RefPtr request = new ModuleLoadRequest(moduleType, SRIMetadata(), aBaseURL, loadContext, ModuleLoadRequest::Kind::StaticImport, this, root); request->mURL = aURI->GetSpecOrDefault(); request->NoCacheEntryFound(aReferrerPolicy, aOptions, aURI); return request.forget(); } bool WorkletModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) { return true; } nsresult WorkletModuleLoader::StartFetch(ModuleLoadRequest* aRequest) { InsertRequest(aRequest->URI(), aRequest); RefPtr runnable = new StartFetchRunnable(aRequest->GetWorkletLoadContext()->GetHandlerRef(), aRequest->URI(), aRequest->mReferrer); NS_DispatchToMainThread(runnable.forget()); return NS_OK; } nsresult WorkletModuleLoader::CompileFetchedModule( JSContext* aCx, JS::Handle aGlobal, JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, JS::MutableHandle aModuleScript) { switch (aRequest->mModuleType) { case JS::ModuleType::Unknown: case JS::ModuleType::Bytes: case JS::ModuleType::Text: MOZ_CRASH("Unexpected module type"); case JS::ModuleType::JavaScriptOrWasm: return CompileJavaScriptOrWasmModule(aCx, aOptions, aRequest, aModuleScript); case JS::ModuleType::JSON: return CompileJsonModule(aCx, aOptions, aRequest, aModuleScript); case JS::ModuleType::CSS: MOZ_CRASH("CSS modules are not supported in worklets"); } MOZ_CRASH("Unhandled module type"); } nsresult WorkletModuleLoader::CompileJavaScriptOrWasmModule( JSContext* aCx, JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, JS::MutableHandle aModuleScript) { #ifdef NIGHTLY_BUILD if (aRequest->HasWasmMimeTypeEssence()) { MOZ_ASSERT(aRequest->IsWasmBytes()); auto* wasmModule = JS::CompileWasmModule(aCx, aOptions, aRequest->WasmBytes()); if (!wasmModule) { return NS_ERROR_FAILURE; } aModuleScript.set(wasmModule); return NS_OK; } #endif MOZ_ASSERT(aRequest->IsTextSource()); MaybeSourceText maybeSource; nsresult rv = aRequest->GetScriptSource(aCx, &maybeSource, aRequest->mLoadContext.get()); NS_ENSURE_SUCCESS(rv, rv); RefPtr stencil; auto compile = [&](auto& source) { return JS::CompileModuleScriptToStencil(aCx, aOptions, source); }; stencil = maybeSource.mapNonEmpty(compile); if (!stencil) { return NS_ERROR_FAILURE; } JS::InstantiateOptions instantiateOptions(aOptions); aModuleScript.set( JS::InstantiateModuleStencil(aCx, instantiateOptions, stencil)); return aModuleScript ? NS_OK : NS_ERROR_FAILURE; } nsresult WorkletModuleLoader::CompileJsonModule( JSContext* aCx, JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, JS::MutableHandle aModuleScript) { MOZ_ASSERT(aRequest->IsTextSource()); MaybeSourceText maybeSource; nsresult rv = aRequest->GetScriptSource(aCx, &maybeSource, aRequest->mLoadContext.get()); NS_ENSURE_SUCCESS(rv, rv); auto compile = [&](auto& source) { return JS::CompileJsonModule(aCx, aOptions, source); }; auto* jsonModule = maybeSource.mapNonEmpty(compile); if (!jsonModule) { return NS_ERROR_FAILURE; } aModuleScript.set(jsonModule); return NS_OK; } // AddModuleResultRunnable is a Runnable which will notify the result of // Worklet::AddModule on the main thread. class AddModuleResultRunnable final : public Runnable { public: explicit AddModuleResultRunnable( const nsMainThreadPtrHandle& aHandlerRef, bool aSucceeded) : Runnable("Worklet::AddModuleResultRunnable"), mHandlerRef(aHandlerRef), mSucceeded(aSucceeded) { MOZ_ASSERT(!NS_IsMainThread()); } ~AddModuleResultRunnable() = default; NS_IMETHOD Run() override; private: nsMainThreadPtrHandle mHandlerRef; bool mSucceeded; }; NS_IMETHODIMP AddModuleResultRunnable::Run() { MOZ_ASSERT(NS_IsMainThread()); if (mSucceeded) { mHandlerRef->ExecutionSucceeded(); } else { mHandlerRef->ExecutionFailed(); } return NS_OK; } class AddModuleThrowErrorRunnable final : public Runnable, public StructuredCloneHolder { public: explicit AddModuleThrowErrorRunnable( const nsMainThreadPtrHandle& aHandlerRef) : Runnable("Worklet::AddModuleThrowErrorRunnable"), StructuredCloneHolder(CloningSupported, TransferringNotSupported, StructuredCloneScope::SameProcess), mHandlerRef(aHandlerRef) { MOZ_ASSERT(!NS_IsMainThread()); } ~AddModuleThrowErrorRunnable() = default; NS_IMETHOD Run() override; private: nsMainThreadPtrHandle mHandlerRef; }; NS_IMETHODIMP AddModuleThrowErrorRunnable::Run() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr global = do_QueryInterface(mHandlerRef->mWorklet->GetParentObject()); MOZ_ASSERT(global); AutoJSAPI jsapi; if (NS_WARN_IF(!jsapi.Init(global))) { mHandlerRef->ExecutionFailed(); return NS_ERROR_FAILURE; } JSContext* cx = jsapi.cx(); JS::Rooted error(cx); IgnoredErrorResult result; Read(cx, &error, result); (void)NS_WARN_IF(result.Failed()); mHandlerRef->ExecutionFailed(error); return NS_OK; } void WorkletModuleLoader::OnModuleLoadComplete(ModuleLoadRequest* aRequest) { if (aRequest->IsStaticImport()) { return; } const nsMainThreadPtrHandle& handlerRef = aRequest->GetWorkletLoadContext()->GetHandlerRef(); auto addModuleFailed = MakeScopeExit([&] { RefPtr runnable = new AddModuleResultRunnable(handlerRef, false); NS_DispatchToMainThread(runnable.forget()); }); if (!aRequest->mModuleScript) { return; } bool hasParseError = aRequest->mModuleScript->HasParseError(); bool hasError = aRequest->mModuleScript->HasErrorToRethrow(); if (!hasParseError && !hasError) { if (!aRequest->InstantiateModuleGraph()) { return; } nsresult rv = aRequest->EvaluateModule(); if (NS_FAILED(rv)) { return; } hasError = aRequest->mModuleScript->HasErrorToRethrow(); } if (hasParseError || hasError) { AutoJSAPI jsapi; if (NS_WARN_IF(!jsapi.Init(GetGlobalObject()))) { return; } JSContext* cx = jsapi.cx(); JS::Rooted error(cx); if (hasParseError) { error = aRequest->mModuleScript->ParseError(); } else { error = aRequest->mModuleScript->ErrorToRethrow(); } RefPtr runnable = new AddModuleThrowErrorRunnable(handlerRef); bool writeOk = [&] { IgnoredErrorResult result; runnable->Write(cx, error, result); return !result.Failed(); }(); JS_SetPendingException(cx, error); if (!writeOk) { return; } addModuleFailed.release(); NS_DispatchToMainThread(runnable.forget()); return; } addModuleFailed.release(); RefPtr runnable = new AddModuleResultRunnable(handlerRef, true); NS_DispatchToMainThread(runnable.forget()); } // TODO: Bug 1808301: Call FormatLocalizedString from a worklet thread. nsresult WorkletModuleLoader::GetResolveFailureMessage( ResolveError aError, const nsAString& aSpecifier, nsAString& aResult) { uint8_t index = static_cast(aError); MOZ_ASSERT(index < static_cast(ResolveError::Length)); MOZ_ASSERT(HasSetLocalizedStrings()); if (NS_WARN_IF(mLocalizedStrs.IsEmpty())) { return NS_ERROR_FAILURE; } const nsString& localizedStr = mLocalizedStrs.ElementAt(index); AutoTArray params; params.AppendElement(aSpecifier); nsStringBundleBase::FormatString(localizedStr.get(), params, aResult); return NS_OK; } void WorkletModuleLoader::InsertRequest(nsIURI* aURI, ModuleLoadRequest* aRequest) { mFetchingRequests.InsertOrUpdate(aURI, aRequest); } void WorkletModuleLoader::RemoveRequest(nsIURI* aURI) { MOZ_ASSERT(mFetchingRequests.Remove(aURI)); } ModuleLoadRequest* WorkletModuleLoader::GetRequest(nsIURI* aURI) const { RefPtr req; MOZ_ALWAYS_TRUE(mFetchingRequests.Get(aURI, getter_AddRefs(req))); return req; } } // namespace mozilla::dom::loader