From de9fa6622a52ff71ad59c8de01af7b1e9274a10d Mon Sep 17 00:00:00 2001 From: Idan Horowitz Date: Tue, 15 Jun 2021 22:16:17 +0300 Subject: [PATCH] LibJS: Add the FinalizationRegistry built-in object As well as the needed functionality in VM to enqueue and run cleanup jobs for the FinalizationRegistry instances. --- Userland/Libraries/LibJS/CMakeLists.txt | 3 + Userland/Libraries/LibJS/Forward.h | 41 ++++----- Userland/Libraries/LibJS/Interpreter.cpp | 2 + .../LibJS/Runtime/FinalizationRegistry.cpp | 86 +++++++++++++++++++ .../LibJS/Runtime/FinalizationRegistry.h | 48 +++++++++++ .../FinalizationRegistryConstructor.cpp | 56 ++++++++++++ .../Runtime/FinalizationRegistryConstructor.h | 28 ++++++ .../Runtime/FinalizationRegistryPrototype.cpp | 41 +++++++++ .../Runtime/FinalizationRegistryPrototype.h | 23 +++++ .../Libraries/LibJS/Runtime/GlobalObject.cpp | 3 + Userland/Libraries/LibJS/Runtime/VM.cpp | 15 ++++ Userland/Libraries/LibJS/Runtime/VM.h | 5 ++ .../FinalizationRegistry.js | 33 +++++++ Userland/Libraries/LibWeb/DOM/Document.cpp | 1 + 14 files changed, 365 insertions(+), 20 deletions(-) create mode 100644 Userland/Libraries/LibJS/Runtime/FinalizationRegistry.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/FinalizationRegistry.h create mode 100644 Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.h create mode 100644 Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.h create mode 100644 Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.js diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index a7369a24409..d218068f40e 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -53,6 +53,9 @@ set(SOURCES Runtime/ErrorPrototype.cpp Runtime/ErrorTypes.cpp Runtime/Exception.cpp + Runtime/FinalizationRegistry.cpp + Runtime/FinalizationRegistryConstructor.cpp + Runtime/FinalizationRegistryPrototype.cpp Runtime/FunctionConstructor.cpp Runtime/Function.cpp Runtime/FunctionPrototype.cpp diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index 20efeb85493..cba564d68bc 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -25,26 +25,27 @@ void name([[maybe_unused]] JS::VM& vm, [[maybe_unused]] JS::GlobalObject& global_object, [[maybe_unused]] JS::Value value) // NOTE: Proxy is not included here as it doesn't have a prototype - m_proxy_constructor is initialized separately. -#define JS_ENUMERATE_NATIVE_OBJECTS_EXCLUDING_TEMPLATES \ - __JS_ENUMERATE(AggregateError, aggregate_error, AggregateErrorPrototype, AggregateErrorConstructor, void) \ - __JS_ENUMERATE(Array, array, ArrayPrototype, ArrayConstructor, void) \ - __JS_ENUMERATE(ArrayBuffer, array_buffer, ArrayBufferPrototype, ArrayBufferConstructor, void) \ - __JS_ENUMERATE(BigIntObject, bigint, BigIntPrototype, BigIntConstructor, void) \ - __JS_ENUMERATE(BooleanObject, boolean, BooleanPrototype, BooleanConstructor, void) \ - __JS_ENUMERATE(DataView, data_view, DataViewPrototype, DataViewConstructor, void) \ - __JS_ENUMERATE(Date, date, DatePrototype, DateConstructor, void) \ - __JS_ENUMERATE(Error, error, ErrorPrototype, ErrorConstructor, void) \ - __JS_ENUMERATE(Function, function, FunctionPrototype, FunctionConstructor, void) \ - __JS_ENUMERATE(Map, map, MapPrototype, MapConstructor, void) \ - __JS_ENUMERATE(NumberObject, number, NumberPrototype, NumberConstructor, void) \ - __JS_ENUMERATE(Object, object, ObjectPrototype, ObjectConstructor, void) \ - __JS_ENUMERATE(Promise, promise, PromisePrototype, PromiseConstructor, void) \ - __JS_ENUMERATE(RegExpObject, regexp, RegExpPrototype, RegExpConstructor, void) \ - __JS_ENUMERATE(Set, set, SetPrototype, SetConstructor, void) \ - __JS_ENUMERATE(StringObject, string, StringPrototype, StringConstructor, void) \ - __JS_ENUMERATE(SymbolObject, symbol, SymbolPrototype, SymbolConstructor, void) \ - __JS_ENUMERATE(WeakMap, weak_map, WeakMapPrototype, WeakMapConstructor, void) \ - __JS_ENUMERATE(WeakRef, weak_ref, WeakRefPrototype, WeakRefConstructor, void) \ +#define JS_ENUMERATE_NATIVE_OBJECTS_EXCLUDING_TEMPLATES \ + __JS_ENUMERATE(AggregateError, aggregate_error, AggregateErrorPrototype, AggregateErrorConstructor, void) \ + __JS_ENUMERATE(Array, array, ArrayPrototype, ArrayConstructor, void) \ + __JS_ENUMERATE(ArrayBuffer, array_buffer, ArrayBufferPrototype, ArrayBufferConstructor, void) \ + __JS_ENUMERATE(BigIntObject, bigint, BigIntPrototype, BigIntConstructor, void) \ + __JS_ENUMERATE(BooleanObject, boolean, BooleanPrototype, BooleanConstructor, void) \ + __JS_ENUMERATE(DataView, data_view, DataViewPrototype, DataViewConstructor, void) \ + __JS_ENUMERATE(Date, date, DatePrototype, DateConstructor, void) \ + __JS_ENUMERATE(Error, error, ErrorPrototype, ErrorConstructor, void) \ + __JS_ENUMERATE(FinalizationRegistry, finalization_registry, FinalizationRegistryPrototype, FinalizationRegistryConstructor, void) \ + __JS_ENUMERATE(Function, function, FunctionPrototype, FunctionConstructor, void) \ + __JS_ENUMERATE(Map, map, MapPrototype, MapConstructor, void) \ + __JS_ENUMERATE(NumberObject, number, NumberPrototype, NumberConstructor, void) \ + __JS_ENUMERATE(Object, object, ObjectPrototype, ObjectConstructor, void) \ + __JS_ENUMERATE(Promise, promise, PromisePrototype, PromiseConstructor, void) \ + __JS_ENUMERATE(RegExpObject, regexp, RegExpPrototype, RegExpConstructor, void) \ + __JS_ENUMERATE(Set, set, SetPrototype, SetConstructor, void) \ + __JS_ENUMERATE(StringObject, string, StringPrototype, StringConstructor, void) \ + __JS_ENUMERATE(SymbolObject, symbol, SymbolPrototype, SymbolConstructor, void) \ + __JS_ENUMERATE(WeakMap, weak_map, WeakMapPrototype, WeakMapConstructor, void) \ + __JS_ENUMERATE(WeakRef, weak_ref, WeakRefPrototype, WeakRefConstructor, void) \ __JS_ENUMERATE(WeakSet, weak_set, WeakSetPrototype, WeakSetConstructor, void) #define JS_ENUMERATE_NATIVE_OBJECTS \ diff --git a/Userland/Libraries/LibJS/Interpreter.cpp b/Userland/Libraries/LibJS/Interpreter.cpp index 63acef125cd..db50839e22e 100644 --- a/Userland/Libraries/LibJS/Interpreter.cpp +++ b/Userland/Libraries/LibJS/Interpreter.cpp @@ -64,6 +64,8 @@ void Interpreter::run(GlobalObject& global_object, const Program& program) // in which case this is a no-op. vm.run_queued_promise_jobs(); + vm.run_queued_finalization_registry_cleanup_jobs(); + vm.finish_execution_generation(); } diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.cpp b/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.cpp new file mode 100644 index 00000000000..d64380ba388 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace JS { + +FinalizationRegistry* FinalizationRegistry::create(GlobalObject& global_object, Function& cleanup_callback) +{ + return global_object.heap().allocate(global_object, *global_object.finalization_registry_prototype(), cleanup_callback); +} + +FinalizationRegistry::FinalizationRegistry(Object& prototype, Function& cleanup_callback) + : Object(prototype) + , WeakContainer(heap()) + , m_cleanup_callback(&cleanup_callback) +{ +} + +FinalizationRegistry::~FinalizationRegistry() +{ +} + +void FinalizationRegistry::add_finalization_record(Cell& target, Value held_value, Object* unregister_token) +{ + VERIFY(!held_value.is_empty()); + m_records.append({ &target, held_value, unregister_token }); +} + +bool FinalizationRegistry::remove_by_token(Object& unregister_token) +{ + auto removed = false; + for (auto it = m_records.begin(); it != m_records.end(); ++it) { + if (it->unregister_token == &unregister_token) { + it.remove(m_records); + removed = true; + } + } + return removed; +} + +void FinalizationRegistry::remove_sweeped_cells(Badge, Vector& cells) +{ + auto any_cells_were_sweeped = false; + for (auto cell : cells) { + for (auto& record : m_records) { + if (record.target != cell) + continue; + record.target = nullptr; + any_cells_were_sweeped = true; + break; + } + } + if (any_cells_were_sweeped) + vm().enqueue_finalization_registry_cleanup_job(*this); +} + +// 9.13 CleanupFinalizationRegistry ( finalizationRegistry ), https://tc39.es/ecma262/#sec-cleanup-finalization-registry +void FinalizationRegistry::cleanup(Function* callback) +{ + auto& vm = this->vm(); + auto cleanup_callback = callback ?: m_cleanup_callback; + for (auto it = m_records.begin(); it != m_records.end(); ++it) { + if (it->target != nullptr) + continue; + (void)vm.call(*cleanup_callback, js_undefined(), it->held_value); + it.remove(m_records); + if (vm.exception()) + return; + } +} + +void FinalizationRegistry::visit_edges(Cell::Visitor& visitor) +{ + Object::visit_edges(visitor); + visitor.visit(m_cleanup_callback); + for (auto& record : m_records) { + visitor.visit(record.held_value); + visitor.visit(record.unregister_token); + } +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.h b/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.h new file mode 100644 index 00000000000..7d7eae3e5dd --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistry.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace JS { + +class FinalizationRegistry final + : public Object + , public WeakContainer { + JS_OBJECT(FinalizationRegistry, Object); + +public: + static FinalizationRegistry* create(GlobalObject&, Function&); + + explicit FinalizationRegistry(Object& prototype, Function&); + virtual ~FinalizationRegistry() override; + + void add_finalization_record(Cell& target, Value held_value, Object* unregister_token); + bool remove_by_token(Object& unregister_token); + void cleanup(Function* callback = nullptr); + + virtual void remove_sweeped_cells(Badge, Vector&) override; + +private: + virtual void visit_edges(Visitor& visitor) override; + + Function* m_cleanup_callback { nullptr }; + + struct FinalizationRecord { + Cell* target { nullptr }; + Value held_value; + Object* unregister_token { nullptr }; + }; + SinglyLinkedList m_records; +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.cpp b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.cpp new file mode 100644 index 00000000000..2f89ce0d779 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace JS { + +FinalizationRegistryConstructor::FinalizationRegistryConstructor(GlobalObject& global_object) + : NativeFunction(vm().names.FinalizationRegistry, *global_object.function_prototype()) +{ +} + +void FinalizationRegistryConstructor::initialize(GlobalObject& global_object) +{ + auto& vm = this->vm(); + NativeFunction::initialize(global_object); + + // 26.2.2.1 FinalizationRegistry.prototype, https://tc39.es/ecma262/#sec-finalization-registry.prototype + define_property(vm.names.prototype, global_object.finalization_registry_prototype(), 0); + + define_property(vm.names.length, Value(1), Attribute::Configurable); +} + +FinalizationRegistryConstructor::~FinalizationRegistryConstructor() +{ +} + +// 26.2.1.1 FinalizationRegistry ( cleanupCallback ), https://tc39.es/ecma262/#sec-finalization-registry-cleanup-callback +Value FinalizationRegistryConstructor::call() +{ + auto& vm = this->vm(); + vm.throw_exception(global_object(), ErrorType::ConstructorWithoutNew, vm.names.FinalizationRegistry); + return {}; +} + +// 26.2.1.1 FinalizationRegistry ( cleanupCallback ), https://tc39.es/ecma262/#sec-finalization-registry-cleanup-callback +Value FinalizationRegistryConstructor::construct(Function&) +{ + auto& vm = this->vm(); + auto cleanup_callback = vm.argument(0); + if (!cleanup_callback.is_function()) { + vm.throw_exception(global_object(), ErrorType::NotAFunction, cleanup_callback.to_string_without_side_effects()); + return {}; + } + + // FIXME: Use OrdinaryCreateFromConstructor(NewTarget, "%FinalizationRegistry.prototype%") + return FinalizationRegistry::create(global_object(), cleanup_callback.as_function()); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.h b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.h new file mode 100644 index 00000000000..d72f5bf5ba0 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS { + +class FinalizationRegistryConstructor final : public NativeFunction { + JS_OBJECT(FinalizationRegistryConstructor, NativeFunction); + +public: + explicit FinalizationRegistryConstructor(GlobalObject&); + virtual void initialize(GlobalObject&) override; + virtual ~FinalizationRegistryConstructor() override; + + virtual Value call() override; + virtual Value construct(Function&) override; + +private: + virtual bool has_constructor() const override { return true; } +}; + +} diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp new file mode 100644 index 00000000000..13bf816a87e --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace JS { + +FinalizationRegistryPrototype::FinalizationRegistryPrototype(GlobalObject& global_object) + : Object(*global_object.object_prototype()) +{ +} + +void FinalizationRegistryPrototype::initialize(GlobalObject& global_object) +{ + auto& vm = this->vm(); + Object::initialize(global_object); + + // 26.2.3.4 FinalizationRegistry.prototype [ @@toStringTag ], https://tc39.es/ecma262/#sec-finalization-registry.prototype-@@tostringtag + define_property(vm.well_known_symbol_to_string_tag(), js_string(global_object.heap(), vm.names.FinalizationRegistry.as_string()), Attribute::Configurable); +} + +FinalizationRegistryPrototype::~FinalizationRegistryPrototype() +{ +} + +FinalizationRegistry* FinalizationRegistryPrototype::typed_this(VM& vm, GlobalObject& global_object) +{ + auto* this_object = vm.this_value(global_object).to_object(global_object); + if (!this_object) + return nullptr; + if (!is(this_object)) { + vm.throw_exception(global_object, ErrorType::NotA, "FinalizationRegistry"); + return nullptr; + } + return static_cast(this_object); +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.h b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.h new file mode 100644 index 00000000000..a03d96aaee0 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS { + +class FinalizationRegistryPrototype final : public Object { + JS_OBJECT(FinalizationRegistryPrototype, Object); + +public: + FinalizationRegistryPrototype(GlobalObject&); + virtual void initialize(GlobalObject&) override; + virtual ~FinalizationRegistryPrototype() override; + +private: + static FinalizationRegistry* typed_this(VM&, GlobalObject&); +} diff --git a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp index ce79290fe5a..59cb867fc2f 100644 --- a/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/GlobalObject.cpp @@ -33,6 +33,8 @@ #include #include #include +#include +#include #include #include #include @@ -155,6 +157,7 @@ void GlobalObject::initialize_global_object() add_constructor(vm.names.DataView, m_data_view_constructor, m_data_view_prototype); add_constructor(vm.names.Date, m_date_constructor, m_date_prototype); add_constructor(vm.names.Error, m_error_constructor, m_error_prototype); + add_constructor(vm.names.FinalizationRegistry, m_finalization_registry_constructor, m_finalization_registry_prototype); add_constructor(vm.names.Function, m_function_constructor, m_function_prototype); add_constructor(vm.names.Map, m_map_constructor, m_map_prototype); add_constructor(vm.names.Number, m_number_constructor, m_number_prototype); diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index e818614db56..5e1500b04a8 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -557,6 +558,20 @@ void VM::enqueue_promise_job(NativeFunction& job) m_promise_jobs.append(&job); } +void VM::run_queued_finalization_registry_cleanup_jobs() +{ + while (!m_finalization_registry_cleanup_jobs.is_empty()) { + auto* registry = m_finalization_registry_cleanup_jobs.take_first(); + registry->cleanup(); + } +} + +// 9.10.4.1 HostEnqueueFinalizationRegistryCleanupJob ( finalizationRegistry ), https://tc39.es/ecma262/#sec-host-cleanup-finalization-registry +void VM::enqueue_finalization_registry_cleanup_job(FinalizationRegistry& registry) +{ + m_finalization_registry_cleanup_jobs.append(®istry); +} + // 27.2.1.9 HostPromiseRejectionTracker ( promise, operation ), https://tc39.es/ecma262/#sec-host-promise-rejection-tracker void VM::promise_rejection_tracker(const Promise& promise, Promise::RejectionOperation operation) const { diff --git a/Userland/Libraries/LibJS/Runtime/VM.h b/Userland/Libraries/LibJS/Runtime/VM.h index e0609e1c193..8f70ffb6147 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.h +++ b/Userland/Libraries/LibJS/Runtime/VM.h @@ -244,6 +244,9 @@ public: void run_queued_promise_jobs(); void enqueue_promise_job(NativeFunction&); + void run_queued_finalization_registry_cleanup_jobs(); + void enqueue_finalization_registry_cleanup_job(FinalizationRegistry&); + void promise_rejection_tracker(const Promise&, Promise::RejectionOperation) const; AK::Function on_call_stack_emptied; @@ -272,6 +275,8 @@ private: Vector m_promise_jobs; + Vector m_finalization_registry_cleanup_jobs; + PrimitiveString* m_empty_string { nullptr }; PrimitiveString* m_single_ascii_character_strings[128] {}; diff --git a/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.js b/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.js new file mode 100644 index 00000000000..d4aacbbc248 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.js @@ -0,0 +1,33 @@ +test("constructor properties", () => { + expect(FinalizationRegistry).toHaveLength(1); + expect(FinalizationRegistry.name).toBe("FinalizationRegistry"); +}); + +describe("errors", () => { + test("invalid callbacks", () => { + [-100, Infinity, NaN, 152n, undefined].forEach(value => { + expect(() => { + new FinalizationRegistry(value); + }).toThrowWithMessage(TypeError, "is not a function"); + }); + }); + test("called without new", () => { + expect(() => { + FinalizationRegistry(); + }).toThrowWithMessage( + TypeError, + "FinalizationRegistry constructor must be called with 'new'" + ); + }); +}); + +describe("normal behavior", () => { + test("typeof", () => { + expect(typeof new FinalizationRegistry(() => {})).toBe("object"); + }); + + test("constructor with single callback argument", () => { + var a = new FinalizationRegistry(() => {}); + expect(a instanceof FinalizationRegistry).toBeTrue(); + }); +}); diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index 98cc56969c8..f1c3d239e13 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -630,6 +630,7 @@ JS::Interpreter& Document::interpreter() vm.on_call_stack_emptied = [this] { auto& vm = m_interpreter->vm(); vm.run_queued_promise_jobs(); + vm.run_queued_finalization_registry_cleanup_jobs(); // Note: This is not an exception check for the promise jobs, they will just leave any // exception that already exists intact and never throw a new one (without cleaning it // up, that is). Taking care of any previous unhandled exception just happens to be the