diff options
author | Luke Wilde <lukew@serenityos.org> | 2022-02-06 03:32:26 +0000 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2022-02-08 17:47:44 +0000 |
commit | 17aeb99e9e92bd8bb47e9680385d9325f3321109 (patch) | |
tree | c2dc1052894a620884cc6c5d501985f242420344 /Userland/Libraries/LibWeb | |
parent | 4c1c6ef91c5339cced845ea63da4dc937e7ffb0e (diff) | |
download | serenity-17aeb99e9e92bd8bb47e9680385d9325f3321109.zip |
LibWeb: Implement the JS host hooks for promises, job callbacks and more
This overrides the JS host hooks to follow the spec for queuing
promises, making/calling job callbacks, unhandled promise rejection
handling and FinalizationRegistry queuing.
This also allows us to drop the on_call_stack_emptied hook in
Document::interpreter().
Diffstat (limited to 'Userland/Libraries/LibWeb')
7 files changed, 290 insertions, 20 deletions
diff --git a/Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp b/Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp index 4d19fbf803..0a20d0eecd 100644 --- a/Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp +++ b/Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp @@ -1,16 +1,47 @@ /* * Copyright (c) 2021, Andreas Kling <kling@serenityos.org> + * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ #include <LibJS/Module.h> #include <LibJS/Runtime/Environment.h> +#include <LibJS/Runtime/FinalizationRegistry.h> +#include <LibJS/Runtime/NativeFunction.h> #include <LibJS/Runtime/VM.h> #include <LibWeb/Bindings/MainThreadVM.h> +#include <LibWeb/DOM/Document.h> +#include <LibWeb/DOM/Window.h> +#include <LibWeb/HTML/PromiseRejectionEvent.h> +#include <LibWeb/HTML/Scripting/ClassicScript.h> +#include <LibWeb/HTML/Scripting/Environments.h> +#include <LibWeb/HTML/Scripting/ExceptionReporter.h> namespace Web::Bindings { +// https://html.spec.whatwg.org/multipage/webappapis.html#active-script +HTML::ClassicScript* active_script() +{ + // 1. Let record be GetActiveScriptOrModule(). + auto record = main_thread_vm().get_active_script_or_module(); + + // 2. If record is null, return null. + if (record.has<Empty>()) + return nullptr; + + // 3. Return record.[[HostDefined]]. + if (record.has<WeakPtr<JS::Module>>()) { + // FIXME: We don't currently have a module script. + TODO(); + } + + auto js_script = record.get<WeakPtr<JS::Script>>(); + VERIFY(js_script); + VERIFY(js_script->host_defined()); + return verify_cast<HTML::ClassicScript>(js_script->host_defined()); +} + JS::VM& main_thread_vm() { static RefPtr<JS::VM> vm; @@ -18,6 +49,239 @@ JS::VM& main_thread_vm() vm = JS::VM::create(make<WebEngineCustomData>()); static_cast<WebEngineCustomData*>(vm->custom_data())->event_loop.set_vm(*vm); + // FIXME: Implement 8.1.5.1 HostEnsureCanCompileStrings(callerRealm, calleeRealm), https://html.spec.whatwg.org/multipage/webappapis.html#hostensurecancompilestrings(callerrealm,-calleerealm) + + // 8.1.5.2 HostPromiseRejectionTracker(promise, operation), https://html.spec.whatwg.org/multipage/webappapis.html#the-hostpromiserejectiontracker-implementation + vm->host_promise_rejection_tracker = [](JS::Promise& promise, JS::Promise::RejectionOperation operation) { + // 1. Let script be the running script. + // The running script is the script in the [[HostDefined]] field in the ScriptOrModule component of the running JavaScript execution context. + // FIXME: This currently assumes there's only a ClassicScript. + HTML::ClassicScript* script { nullptr }; + vm->running_execution_context().script_or_module.visit( + [&script](WeakPtr<JS::Script>& js_script) { + script = verify_cast<HTML::ClassicScript>(js_script->host_defined()); + }, + [](WeakPtr<JS::Module>&) { + TODO(); + }, + [](Empty) { + }); + + // If there's no script, we're out of luck. Return. + // FIXME: This can happen from JS::NativeFunction, which makes its callee contexts [[ScriptOrModule]] null. + if (!script) { + dbgln("FIXME: Unable to process unhandled promise rejection in host_promise_rejection_tracker as the running script is null."); + return; + } + + // 2. If script's muted errors is true, terminate these steps. + if (script->muted_errors() == HTML::ClassicScript::MutedErrors::Yes) + return; + + // 3. Let settings object be script's settings object. + auto& settings_object = script->settings_object(); + + switch (operation) { + case JS::Promise::RejectionOperation::Reject: + // 4. If operation is "reject", + // 1. Add promise to settings object's about-to-be-notified rejected promises list. + settings_object.push_onto_about_to_be_notified_rejected_promises_list(JS::make_handle(&promise)); + break; + case JS::Promise::RejectionOperation::Handle: { + // 5. If operation is "handle", + // 1. If settings object's about-to-be-notified rejected promises list contains promise, then remove promise from that list and return. + bool removed_about_to_be_notified_rejected_promise = settings_object.remove_from_about_to_be_notified_rejected_promises_list(&promise); + if (removed_about_to_be_notified_rejected_promise) + return; + + // 3. Remove promise from settings object's outstanding rejected promises weak set. + bool removed_outstanding_rejected_promise = settings_object.remove_from_outstanding_rejected_promises_weak_set(&promise); + + // 2. If settings object's outstanding rejected promises weak set does not contain promise, then return. + // NOTE: This is done out of order because removed_outstanding_rejected_promise will be false if the promise wasn't in the set or true if it was and got removed. + if (!removed_outstanding_rejected_promise) + return; + + // 4. Let global be settings object's global object. + auto& global = settings_object.global_object(); + + // 5. Queue a global task on the DOM manipulation task source given global to fire an event named rejectionhandled at global, using PromiseRejectionEvent, + // with the promise attribute initialized to promise, and the reason attribute initialized to the value of promise's [[PromiseResult]] internal slot. + HTML::queue_global_task(HTML::Task::Source::DOMManipulation, global, [global = JS::make_handle(&global), promise = JS::make_handle(&promise)]() mutable { + // FIXME: This currently assumes that global is a WindowObject. + auto& window = verify_cast<Bindings::WindowObject>(*global.cell()); + + HTML::PromiseRejectionEventInit event_init { + {}, // Initialize the inherited DOM::EventInit + /* .promise = */ promise, + /* .reason = */ promise.cell()->result(), + }; + auto promise_rejection_event = HTML::PromiseRejectionEvent::create(HTML::EventNames::rejectionhandled, event_init); + window.impl().dispatch_event(move(promise_rejection_event)); + }); + break; + } + default: + VERIFY_NOT_REACHED(); + } + }; + + // 8.1.5.3.1 HostCallJobCallback(callback, V, argumentsList), https://html.spec.whatwg.org/multipage/webappapis.html#hostcalljobcallback + vm->host_call_job_callback = [](JS::GlobalObject& global_object, JS::JobCallback& callback, JS::Value this_value, JS::MarkedValueList arguments_list) { + auto& callback_host_defined = verify_cast<WebEngineCustomJobCallbackData>(*callback.custom_data); + + // 1. Let incumbent settings be callback.[[HostDefined]].[[IncumbentSettings]]. (NOTE: Not necessary) + // 2. Let script execution context be callback.[[HostDefined]].[[ActiveScriptContext]]. (NOTE: Not necessary) + + // 3. Prepare to run a callback with incumbent settings. + callback_host_defined.incumbent_settings.prepare_to_run_callback(); + + // 4. If script execution context is not null, then push script execution context onto the JavaScript execution context stack. + if (callback_host_defined.active_script_context) + MUST(vm->push_execution_context(*callback_host_defined.active_script_context, callback.callback.cell()->global_object())); + + // 5. Let result be Call(callback.[[Callback]], V, argumentsList). + auto result = JS::call(global_object, *callback.callback.cell(), this_value, move(arguments_list)); + + // 6. If script execution context is not null, then pop script execution context from the JavaScript execution context stack. + if (callback_host_defined.active_script_context) { + VERIFY(&vm->running_execution_context() == callback_host_defined.active_script_context.ptr()); + vm->pop_execution_context(); + } + + // 7. Clean up after running a callback with incumbent settings. + callback_host_defined.incumbent_settings.clean_up_after_running_callback(); + + // 8. Return result. + return result; + }; + + // 8.1.5.3.2 HostEnqueueFinalizationRegistryCleanupJob(finalizationRegistry), https://html.spec.whatwg.org/multipage/webappapis.html#hostenqueuefinalizationregistrycleanupjob + vm->host_enqueue_finalization_registry_cleanup_job = [](JS::FinalizationRegistry& finalization_registry) mutable { + // 1. Let global be finalizationRegistry.[[Realm]]'s global object. + auto& global = finalization_registry.realm().global_object(); + + // 2. Queue a global task on the JavaScript engine task source given global to perform the following steps: + HTML::queue_global_task(HTML::Task::Source::JavaScriptEngine, global, [finalization_registry = JS::make_handle(&finalization_registry)]() mutable { + // 1. Let entry be finalizationRegistry.[[CleanupCallback]].[[Callback]].[[Realm]]'s environment settings object. + auto& entry = verify_cast<HTML::EnvironmentSettingsObject>(*finalization_registry.cell()->cleanup_callback().callback.cell()->realm()->host_defined()); + + // 2. Check if we can run script with entry. If this returns "do not run", then return. + if (entry.can_run_script() == HTML::RunScriptDecision::DoNotRun) + return; + + // 3. Prepare to run script with entry. + entry.prepare_to_run_script(); + + // 4. Let result be the result of performing CleanupFinalizationRegistry(finalizationRegistry). + auto result = finalization_registry.cell()->cleanup(); + + // 5. Clean up after running script with entry. + entry.clean_up_after_running_script(); + + // 6. If result is an abrupt completion, then report the exception given by result.[[Value]]. + if (result.is_error()) + HTML::report_exception(result); + }); + }; + + // 8.1.5.3.3 HostEnqueuePromiseJob(job, realm), https://html.spec.whatwg.org/multipage/webappapis.html#hostenqueuepromisejob + vm->host_enqueue_promise_job = [](Function<JS::ThrowCompletionOr<JS::Value>()> job, JS::Realm* realm) { + // 1. If realm is not null, then let job settings be the settings object for realm. Otherwise, let job settings be null. + HTML::EnvironmentSettingsObject* job_settings { nullptr }; + if (realm) + job_settings = verify_cast<HTML::EnvironmentSettingsObject>(realm->host_defined()); + + // IMPLEMENTATION DEFINED: The JS spec says we must take implementation defined steps to make the currently active script or module at the time of HostEnqueuePromiseJob being invoked + // also be the active script or module of the job at the time of its invocation. + // This means taking it here now and passing it through to the lambda. + auto script_or_module = vm->get_active_script_or_module(); + + // 2. Queue a microtask on the surrounding agent's event loop to perform the following steps: + // This instance of "queue a microtask" uses the "implied document". The best fit for "implied document" here is "If the task is being queued by or for a script, then return the script's settings object's responsible document." + // Do note that "implied document" from the spec is handwavy and the spec authors are trying to get rid of it: https://github.com/whatwg/html/issues/4980 + auto* script = active_script(); + + // NOTE: This keeps job_settings alive by keeping realm alive, which is holding onto job_settings. + HTML::queue_a_microtask(script ? script->settings_object().responsible_document().ptr() : nullptr, [job_settings, job = move(job), realm = realm ? JS::make_handle(realm) : JS::Handle<JS::Realm> {}, script_or_module = move(script_or_module)]() mutable { + // The dummy execution context has to be kept up here to keep it alive for the duration of the function. + Optional<JS::ExecutionContext> dummy_execution_context; + + if (job_settings) { + // 1. If job settings is not null, then check if we can run script with job settings. If this returns "do not run" then return. + if (job_settings->can_run_script() == HTML::RunScriptDecision::DoNotRun) + return; + + // 2. If job settings is not null, then prepare to run script with job settings. + job_settings->prepare_to_run_script(); + + // IMPLEMENTATION DEFINED: Per the previous "implementation defined" comment, we must now make the script or module the active script or module. + // Since the only active execution context currently is the realm execution context of job settings, lets attach it here. + job_settings->realm_execution_context().script_or_module = script_or_module; + } else { + // FIXME: We need to setup a dummy execution context in case a JS::NativeFunction is called when processing the job. + // This is because JS::NativeFunction::call excepts something to be on the execution context stack to be able to get the caller context to initialize the environment. + // Since this requires pushing an execution context onto the stack, it also requires a global object. The only thing we can get a global object from in this case is the script or module. + // To do this, we must assume script or module is not Empty. We must also assume that it is a Script Record for now as we don't currently run modules. + // Do note that the JS spec gives _no_ guarantee that the execution context stack has something on it if HostEnqueuePromiseJob was called with a null realm: https://tc39.es/ecma262/#job-preparedtoevaluatecode + VERIFY(script_or_module.has<WeakPtr<JS::Script>>()); + auto script_record = script_or_module.get<WeakPtr<JS::Script>>(); + dummy_execution_context = JS::ExecutionContext { vm->heap() }; + dummy_execution_context->script_or_module = script_or_module; + vm->push_execution_context(dummy_execution_context.value(), script_record->realm().global_object()); + } + + // 3. Let result be job(). + [[maybe_unused]] auto result = job(); + + // 4. If job settings is not null, then clean up after running script with job settings. + if (job_settings) { + // IMPLEMENTATION DEFINED: Disassociate the realm execution context from the script or module. + job_settings->realm_execution_context().script_or_module = Empty {}; + + job_settings->clean_up_after_running_script(); + } else { + // Pop off the dummy execution context. See the above FIXME block about why this is done. + vm->pop_execution_context(); + } + + // 5. If result is an abrupt completion, then report the exception given by result.[[Value]]. + if (result.is_error()) + HTML::report_exception(result); + }); + }; + + // 8.1.5.3.4 HostMakeJobCallback(callable), https://html.spec.whatwg.org/multipage/webappapis.html#hostmakejobcallback + vm->host_make_job_callback = [](JS::FunctionObject& callable) -> JS::JobCallback { + // 1. Let incumbent settings be the incumbent settings object. + auto& incumbent_settings = HTML::incumbent_settings_object(); + + // 2. Let active script be the active script. + auto* script = active_script(); + + // 3. Let script execution context be null. + OwnPtr<JS::ExecutionContext> script_execution_context; + + // 4. If active script is not null, set script execution context to a new JavaScript execution context, with its Function field set to null, + // its Realm field set to active script's settings object's Realm, and its ScriptOrModule set to active script's record. + if (script) { + script_execution_context = adopt_own(*new JS::ExecutionContext(vm->heap())); + script_execution_context->function = nullptr; + script_execution_context->realm = &script->settings_object().realm(); + VERIFY(script->script_record()); + script_execution_context->script_or_module = script->script_record()->make_weak_ptr(); + } + + // 5. Return the JobCallback Record { [[Callback]]: callable, [[HostDefined]]: { [[IncumbentSettings]]: incumbent settings, [[ActiveScriptContext]]: script execution context } }. + auto host_defined = adopt_own(*new WebEngineCustomJobCallbackData(incumbent_settings, move(script_execution_context))); + return { JS::make_handle(&callable), move(host_defined) }; + }; + + // FIXME: Implement 8.1.5.4.1 HostGetImportMetaProperties(moduleRecord), https://html.spec.whatwg.org/multipage/webappapis.html#hostgetimportmetaproperties + // FIXME: Implement 8.1.5.4.2 HostImportModuleDynamically(referencingScriptOrModule, moduleRequest, promiseCapability), https://html.spec.whatwg.org/multipage/webappapis.html#hostimportmoduledynamically(referencingscriptormodule,-modulerequest,-promisecapability) + // FIXME: Implement 8.1.5.4.3 HostResolveImportedModule(referencingScriptOrModule, moduleRequest), https://html.spec.whatwg.org/multipage/webappapis.html#hostresolveimportedmodule(referencingscriptormodule,-modulerequest) + // FIXME: Implement 8.1.5.4.4 HostGetSupportedImportAssertions(), https://html.spec.whatwg.org/multipage/webappapis.html#hostgetsupportedimportassertions + vm->host_resolve_imported_module = [&](JS::ScriptOrModule, JS::ModuleRequest const&) -> JS::ThrowCompletionOr<NonnullRefPtr<JS::Module>> { return vm->throw_completion<JS::InternalError>(vm->current_realm()->global_object(), JS::ErrorType::NotImplemented, "Modules in the browser"); }; diff --git a/Userland/Libraries/LibWeb/Bindings/MainThreadVM.h b/Userland/Libraries/LibWeb/Bindings/MainThreadVM.h index e16ecf7d2c..ae7acfe2de 100644 --- a/Userland/Libraries/LibWeb/Bindings/MainThreadVM.h +++ b/Userland/Libraries/LibWeb/Bindings/MainThreadVM.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Andreas Kling <kling@serenityos.org> + * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ @@ -7,6 +8,7 @@ #pragma once #include <LibJS/Forward.h> +#include <LibJS/Runtime/JobCallback.h> #include <LibJS/Runtime/VM.h> #include <LibWeb/HTML/EventLoop/EventLoop.h> @@ -18,6 +20,20 @@ struct WebEngineCustomData final : public JS::VM::CustomData { HTML::EventLoop event_loop; }; +struct WebEngineCustomJobCallbackData final : public JS::JobCallback::CustomData { + WebEngineCustomJobCallbackData(HTML::EnvironmentSettingsObject& incumbent_settings, OwnPtr<JS::ExecutionContext> active_script_context) + : incumbent_settings(incumbent_settings) + , active_script_context(move(active_script_context)) + { + } + + virtual ~WebEngineCustomJobCallbackData() override { } + + HTML::EnvironmentSettingsObject& incumbent_settings; + OwnPtr<JS::ExecutionContext> active_script_context; +}; + +HTML::ClassicScript* active_script(); JS::VM& main_thread_vm(); } diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index e42d46cda9..e9423c0bac 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -674,22 +674,6 @@ JS::Interpreter& Document::interpreter() // FIXME: 9. Set up a window environment settings object with creationURL, realm execution context, navigationParams's reserved environment, topLevelCreationURL, and topLevelOrigin. // (This is missing reserved environment, topLevelCreationURL and topLevelOrigin. It also assumes creationURL is the document's URL, when it's really "navigationParams's response's URL.") HTML::WindowEnvironmentSettingsObject::setup(m_url, realm_execution_context); - - // NOTE: We must hook `on_call_stack_emptied` after the interpreter was created, as the initialization of the - // WindowsObject can invoke some internal calls, which will eventually lead to this hook being called without - // `m_interpreter` being fully initialized yet. - // TODO: Hook up vm.on_promise_unhandled_rejection and vm.on_promise_rejection_handled - // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises#promise_rejection_events - vm.on_call_stack_emptied = [this] { - auto& vm = m_interpreter->vm(); - vm.run_queued_promise_jobs(); - vm.run_queued_finalization_registry_cleanup_jobs(); - - // FIXME: This isn't exactly the right place for this. - HTML::main_thread_event_loop().perform_a_microtask_checkpoint(); - - vm.finish_execution_generation(); - }; } return *m_interpreter; } diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp index b4d8e57c78..259e7b4240 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp +++ b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp @@ -284,7 +284,8 @@ void EventLoop::perform_a_microtask_checkpoint() // FIXME: 5. Cleanup Indexed Database transactions. - // FIXME: 6. Perform ClearKeptObjects(). + // 6. Perform ClearKeptObjects(). + vm().finish_execution_generation(); // 7. Set the event loop's performing a microtask checkpoint to false. m_performing_a_microtask_checkpoint = false; diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h index 51947f58d3..22aaa8c10a 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h +++ b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h @@ -26,6 +26,7 @@ public: PostedMessage, Microtask, TimerTask, + JavaScriptEngine, }; static NonnullOwnPtr<Task> create(Source source, DOM::Document* document, Function<void()> steps) diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp b/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp index 8749048c74..1581c72ebc 100644 --- a/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp +++ b/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp @@ -43,7 +43,7 @@ NonnullRefPtr<ClassicScript> ClassicScript::create(String filename, StringView s // 10. Let result be ParseScript(source, settings's Realm, script). auto parse_timer = Core::ElapsedTimer::start_new(); - auto result = JS::Script::parse(source, environment_settings_object.realm(), script->filename()); + auto result = JS::Script::parse(source, environment_settings_object.realm(), script->filename(), script.ptr()); dbgln_if(HTML_SCRIPT_DEBUG, "ClassicScript: Parsed {} in {}ms", script->filename(), parse_timer.elapsed()); // 11. If result is a list of errors, then: diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.h b/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.h index 80affbec8f..ff8c635fc0 100644 --- a/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.h +++ b/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.h @@ -13,9 +13,11 @@ namespace Web::HTML { // https://html.spec.whatwg.org/multipage/webappapis.html#classic-script -class ClassicScript final : public Script { +class ClassicScript final + : public Script + , public JS::Script::HostDefined { public: - ~ClassicScript(); + virtual ~ClassicScript() override; enum class MutedErrors { No, @@ -34,6 +36,8 @@ public: }; JS::Completion run(RethrowErrors = RethrowErrors::No); + MutedErrors muted_errors() const { return m_muted_errors; } + private: ClassicScript(AK::URL base_url, String filename, EnvironmentSettingsObject& environment_settings_object); |