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.
This commit is contained in:
Idan Horowitz 2021-06-15 22:16:17 +03:00 committed by Linus Groh
parent 8c7fe8d6c8
commit de9fa6622a
Notes: sideshowbarker 2024-07-18 12:12:42 +09:00
14 changed files with 365 additions and 20 deletions

View file

@ -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

View file

@ -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 \

View file

@ -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();
}

View file

@ -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);
}
}
}

View file

@ -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;
};
}

View file

@ -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());
}
}

View file

@ -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; }
};
}

View file

@ -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);
}
}

View file

@ -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&);
}

View file

@ -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);

View file

@ -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
{

View file

@ -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] {};

View file

@ -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();
});
});

View file

@ -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