Parcourir la source

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.
Idan Horowitz il y a 4 ans
Parent
commit
de9fa6622a

+ 3 - 0
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

+ 21 - 20
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                 \

+ 2 - 0
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();
 }
 

+ 86 - 0
Userland/Libraries/LibJS/Runtime/FinalizationRegistry.cpp

@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibJS/Runtime/FinalizationRegistry.h>
+
+namespace JS {
+
+FinalizationRegistry* FinalizationRegistry::create(GlobalObject& global_object, Function& cleanup_callback)
+{
+    return global_object.heap().allocate<FinalizationRegistry>(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<Heap>, Vector<Cell*>& 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);
+    }
+}
+
+}

+ 48 - 0
Userland/Libraries/LibJS/Runtime/FinalizationRegistry.h

@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/SinglyLinkedList.h>
+#include <LibJS/Runtime/Function.h>
+#include <LibJS/Runtime/GlobalObject.h>
+#include <LibJS/Runtime/Object.h>
+#include <LibJS/Runtime/Value.h>
+#include <LibJS/Runtime/WeakContainer.h>
+
+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<Heap>, Vector<Cell*>&) 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<FinalizationRecord> m_records;
+};
+
+}

+ 56 - 0
Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.cpp

@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibJS/Runtime/Error.h>
+#include <LibJS/Runtime/FinalizationRegistry.h>
+#include <LibJS/Runtime/FinalizationRegistryConstructor.h>
+#include <LibJS/Runtime/GlobalObject.h>
+
+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<TypeError>(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<TypeError>(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());
+}
+
+}

+ 28 - 0
Userland/Libraries/LibJS/Runtime/FinalizationRegistryConstructor.h

@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibJS/Runtime/NativeFunction.h>
+
+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; }
+};
+
+}

+ 41 - 0
Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp

@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibJS/Runtime/FinalizationRegistryPrototype.h>
+
+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<FinalizationRegistry>(this_object)) {
+        vm.throw_exception<TypeError>(global_object, ErrorType::NotA, "FinalizationRegistry");
+        return nullptr;
+    }
+    return static_cast<FinalizationRegistry*>(this_object);
+}
+
+}

+ 23 - 0
Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.h

@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibJS/Runtime/FinalizationRegistry.h>
+
+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&);
+}

+ 3 - 0
Userland/Libraries/LibJS/Runtime/GlobalObject.cpp

@@ -33,6 +33,8 @@
 #include <LibJS/Runtime/DatePrototype.h>
 #include <LibJS/Runtime/ErrorConstructor.h>
 #include <LibJS/Runtime/ErrorPrototype.h>
+#include <LibJS/Runtime/FinalizationRegistryConstructor.h>
+#include <LibJS/Runtime/FinalizationRegistryPrototype.h>
 #include <LibJS/Runtime/FunctionConstructor.h>
 #include <LibJS/Runtime/FunctionPrototype.h>
 #include <LibJS/Runtime/GlobalObject.h>
@@ -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);

+ 15 - 0
Userland/Libraries/LibJS/Runtime/VM.cpp

@@ -11,6 +11,7 @@
 #include <LibJS/Interpreter.h>
 #include <LibJS/Runtime/Array.h>
 #include <LibJS/Runtime/Error.h>
+#include <LibJS/Runtime/FinalizationRegistry.h>
 #include <LibJS/Runtime/GlobalObject.h>
 #include <LibJS/Runtime/IteratorOperations.h>
 #include <LibJS/Runtime/NativeFunction.h>
@@ -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(&registry);
+}
+
 // 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
 {

+ 5 - 0
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<void()> on_call_stack_emptied;
@@ -272,6 +275,8 @@ private:
 
     Vector<NativeFunction*> m_promise_jobs;
 
+    Vector<FinalizationRegistry*> m_finalization_registry_cleanup_jobs;
+
     PrimitiveString* m_empty_string { nullptr };
     PrimitiveString* m_single_ascii_character_strings[128] {};
 

+ 33 - 0
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();
+    });
+});

+ 1 - 0
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