summaryrefslogtreecommitdiffstats
path: root/js/src/builtin/FinalizationRegistryObject.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/builtin/FinalizationRegistryObject.cpp')
-rw-r--r--js/src/builtin/FinalizationRegistryObject.cpp849
1 files changed, 849 insertions, 0 deletions
diff --git a/js/src/builtin/FinalizationRegistryObject.cpp b/js/src/builtin/FinalizationRegistryObject.cpp
new file mode 100644
index 0000000000..168c58aaf4
--- /dev/null
+++ b/js/src/builtin/FinalizationRegistryObject.cpp
@@ -0,0 +1,849 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * 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/. */
+
+// Implementation of JS FinalizationRegistry objects.
+
+#include "builtin/FinalizationRegistryObject.h"
+
+#include "mozilla/ScopeExit.h"
+
+#include "jsapi.h"
+
+#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
+#include "vm/GlobalObject.h"
+#include "vm/Interpreter.h"
+#include "vm/WellKnownAtom.h" // js_*_str
+
+#include "gc/GCContext-inl.h"
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+
+///////////////////////////////////////////////////////////////////////////
+// FinalizationRecordObject
+
+const JSClass FinalizationRecordObject::class_ = {
+ "FinalizationRecord", JSCLASS_HAS_RESERVED_SLOTS(SlotCount)};
+
+/* static */
+FinalizationRecordObject* FinalizationRecordObject::create(
+ JSContext* cx, HandleFinalizationQueueObject queue, HandleValue heldValue) {
+ MOZ_ASSERT(queue);
+
+ auto record = NewObjectWithGivenProto<FinalizationRecordObject>(cx, nullptr);
+ if (!record) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(queue->compartment() == record->compartment());
+
+ record->initReservedSlot(QueueSlot, ObjectValue(*queue));
+ record->initReservedSlot(HeldValueSlot, heldValue);
+ record->initReservedSlot(InMapSlot, BooleanValue(false));
+
+ return record;
+}
+
+FinalizationQueueObject* FinalizationRecordObject::queue() const {
+ Value value = getReservedSlot(QueueSlot);
+ if (value.isUndefined()) {
+ return nullptr;
+ }
+ return &value.toObject().as<FinalizationQueueObject>();
+}
+
+Value FinalizationRecordObject::heldValue() const {
+ return getReservedSlot(HeldValueSlot);
+}
+
+bool FinalizationRecordObject::isRegistered() const {
+ MOZ_ASSERT_IF(!queue(), heldValue().isUndefined());
+ return queue();
+}
+
+bool FinalizationRecordObject::isInRecordMap() const {
+ return getReservedSlot(InMapSlot).toBoolean();
+}
+
+void FinalizationRecordObject::setInRecordMap(bool newValue) {
+ MOZ_ASSERT(newValue != isInRecordMap());
+ setReservedSlot(InMapSlot, BooleanValue(newValue));
+}
+
+void FinalizationRecordObject::clear() {
+ MOZ_ASSERT(queue());
+ setReservedSlot(QueueSlot, UndefinedValue());
+ setReservedSlot(HeldValueSlot, UndefinedValue());
+ MOZ_ASSERT(!isRegistered());
+}
+
+///////////////////////////////////////////////////////////////////////////
+// FinalizationRegistrationsObject
+
+const JSClass FinalizationRegistrationsObject::class_ = {
+ "FinalizationRegistrations",
+ JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_BACKGROUND_FINALIZE,
+ &classOps_, JS_NULL_CLASS_SPEC};
+
+const JSClassOps FinalizationRegistrationsObject::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ FinalizationRegistrationsObject::finalize, // finalize
+ nullptr, // call
+ nullptr, // construct
+ FinalizationRegistrationsObject::trace, // trace
+};
+
+/* static */
+FinalizationRegistrationsObject* FinalizationRegistrationsObject::create(
+ JSContext* cx) {
+ auto records = cx->make_unique<WeakFinalizationRecordVector>(cx->zone());
+ if (!records) {
+ return nullptr;
+ }
+
+ auto object =
+ NewObjectWithGivenProto<FinalizationRegistrationsObject>(cx, nullptr);
+ if (!object) {
+ return nullptr;
+ }
+
+ InitReservedSlot(object, RecordsSlot, records.release(),
+ MemoryUse::FinalizationRecordVector);
+
+ return object;
+}
+
+/* static */
+void FinalizationRegistrationsObject::trace(JSTracer* trc, JSObject* obj) {
+ if (!trc->traceWeakEdges()) {
+ return;
+ }
+
+ auto* self = &obj->as<FinalizationRegistrationsObject>();
+ if (WeakFinalizationRecordVector* records = self->records()) {
+ TraceRange(trc, records->length(), records->begin(),
+ "FinalizationRegistrationsObject records");
+ }
+}
+
+/* static */
+void FinalizationRegistrationsObject::finalize(JS::GCContext* gcx,
+ JSObject* obj) {
+ auto* self = &obj->as<FinalizationRegistrationsObject>();
+ gcx->delete_(obj, self->records(), MemoryUse::FinalizationRecordVector);
+}
+
+inline WeakFinalizationRecordVector*
+FinalizationRegistrationsObject::records() {
+ return static_cast<WeakFinalizationRecordVector*>(privatePtr());
+}
+
+inline const WeakFinalizationRecordVector*
+FinalizationRegistrationsObject::records() const {
+ return static_cast<const WeakFinalizationRecordVector*>(privatePtr());
+}
+
+inline void* FinalizationRegistrationsObject::privatePtr() const {
+ Value value = getReservedSlot(RecordsSlot);
+ if (value.isUndefined()) {
+ return nullptr;
+ }
+ void* ptr = value.toPrivate();
+ MOZ_ASSERT(ptr);
+ return ptr;
+}
+
+inline bool FinalizationRegistrationsObject::isEmpty() const {
+ MOZ_ASSERT(records());
+ return records()->empty();
+}
+
+inline bool FinalizationRegistrationsObject::append(
+ HandleFinalizationRecordObject record) {
+ MOZ_ASSERT(records());
+ return records()->append(record);
+}
+
+inline void FinalizationRegistrationsObject::remove(
+ HandleFinalizationRecordObject record) {
+ MOZ_ASSERT(records());
+ records()->eraseIfEqual(record);
+}
+
+inline bool FinalizationRegistrationsObject::traceWeak(JSTracer* trc) {
+ MOZ_ASSERT(records());
+ return records()->traceWeak(trc);
+}
+
+///////////////////////////////////////////////////////////////////////////
+// FinalizationRegistryObject
+
+// Bug 1600300: FinalizationRegistryObject is foreground finalized so that
+// HeapPtr destructors never see referents with released arenas. When this is
+// fixed we may be able to make this background finalized again.
+const JSClass FinalizationRegistryObject::class_ = {
+ "FinalizationRegistry",
+ JSCLASS_HAS_CACHED_PROTO(JSProto_FinalizationRegistry) |
+ JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_FOREGROUND_FINALIZE,
+ &classOps_, &classSpec_};
+
+const JSClass FinalizationRegistryObject::protoClass_ = {
+ "FinalizationRegistry.prototype",
+ JSCLASS_HAS_CACHED_PROTO(JSProto_FinalizationRegistry), JS_NULL_CLASS_OPS,
+ &classSpec_};
+
+const JSClassOps FinalizationRegistryObject::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ FinalizationRegistryObject::finalize, // finalize
+ nullptr, // call
+ nullptr, // construct
+ FinalizationRegistryObject::trace, // trace
+};
+
+const ClassSpec FinalizationRegistryObject::classSpec_ = {
+ GenericCreateConstructor<construct, 1, gc::AllocKind::FUNCTION>,
+ GenericCreatePrototype<FinalizationRegistryObject>,
+ nullptr,
+ nullptr,
+ methods_,
+ properties_};
+
+const JSFunctionSpec FinalizationRegistryObject::methods_[] = {
+ JS_FN(js_register_str, register_, 2, 0),
+ JS_FN(js_unregister_str, unregister, 1, 0),
+ JS_FN(js_cleanupSome_str, cleanupSome, 0, 0), JS_FS_END};
+
+const JSPropertySpec FinalizationRegistryObject::properties_[] = {
+ JS_STRING_SYM_PS(toStringTag, "FinalizationRegistry", JSPROP_READONLY),
+ JS_PS_END};
+
+/* static */
+bool FinalizationRegistryObject::construct(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!ThrowIfNotConstructing(cx, args, "FinalizationRegistry")) {
+ return false;
+ }
+
+ RootedObject cleanupCallback(
+ cx, ValueToCallable(cx, args.get(0), 1, NO_CONSTRUCT));
+ if (!cleanupCallback) {
+ return false;
+ }
+
+ RootedObject proto(cx);
+ if (!GetPrototypeFromBuiltinConstructor(
+ cx, args, JSProto_FinalizationRegistry, &proto)) {
+ return false;
+ }
+
+ Rooted<UniquePtr<ObjectWeakMap>> registrations(
+ cx, cx->make_unique<ObjectWeakMap>(cx));
+ if (!registrations) {
+ return false;
+ }
+
+ RootedFinalizationQueueObject queue(
+ cx, FinalizationQueueObject::create(cx, cleanupCallback));
+ if (!queue) {
+ return false;
+ }
+
+ RootedFinalizationRegistryObject registry(
+ cx, NewObjectWithClassProto<FinalizationRegistryObject>(cx, proto));
+ if (!registry) {
+ return false;
+ }
+
+ registry->initReservedSlot(QueueSlot, ObjectValue(*queue));
+ InitReservedSlot(registry, RegistrationsSlot, registrations.release(),
+ MemoryUse::FinalizationRegistryRegistrations);
+
+ if (!cx->runtime()->gc.addFinalizationRegistry(cx, registry)) {
+ return false;
+ }
+
+ queue->setHasRegistry(true);
+
+ args.rval().setObject(*registry);
+ return true;
+}
+
+/* static */
+void FinalizationRegistryObject::trace(JSTracer* trc, JSObject* obj) {
+ // Trace the registrations weak map. At most this traces the
+ // FinalizationRegistrationsObject values of the map; the contents of those
+ // objects are weakly held and are not traced by this method.
+
+ auto* registry = &obj->as<FinalizationRegistryObject>();
+ if (ObjectWeakMap* registrations = registry->registrations()) {
+ registrations->trace(trc);
+ }
+}
+
+void FinalizationRegistryObject::traceWeak(JSTracer* trc) {
+ // Trace and update the contents of the registrations weak map's values, which
+ // are weakly held.
+
+ MOZ_ASSERT(registrations());
+ for (ObjectValueWeakMap::Enum e(registrations()->valueMap()); !e.empty();
+ e.popFront()) {
+ auto* registrations =
+ &e.front().value().toObject().as<FinalizationRegistrationsObject>();
+ if (!registrations->traceWeak(trc)) {
+ e.removeFront();
+ }
+ }
+}
+
+/* static */
+void FinalizationRegistryObject::finalize(JS::GCContext* gcx, JSObject* obj) {
+ auto registry = &obj->as<FinalizationRegistryObject>();
+
+ // The queue's flag should have been updated by
+ // GCRuntime::sweepFinalizationRegistries.
+ MOZ_ASSERT_IF(registry->queue(), !registry->queue()->hasRegistry());
+
+ gcx->delete_(obj, registry->registrations(),
+ MemoryUse::FinalizationRegistryRegistrations);
+}
+
+FinalizationQueueObject* FinalizationRegistryObject::queue() const {
+ Value value = getReservedSlot(QueueSlot);
+ if (value.isUndefined()) {
+ return nullptr;
+ }
+ return &value.toObject().as<FinalizationQueueObject>();
+}
+
+ObjectWeakMap* FinalizationRegistryObject::registrations() const {
+ Value value = getReservedSlot(RegistrationsSlot);
+ if (value.isUndefined()) {
+ return nullptr;
+ }
+ return static_cast<ObjectWeakMap*>(value.toPrivate());
+}
+
+// FinalizationRegistry.prototype.register(target, heldValue [, unregisterToken
+// ])
+// https://tc39.es/proposal-weakrefs/#sec-finalization-registry.prototype.register
+/* static */
+bool FinalizationRegistryObject::register_(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ // 1. Let finalizationRegistry be the this value.
+ // 2. If Type(finalizationRegistry) is not Object, throw a TypeError
+ // exception.
+ // 3. If finalizationRegistry does not have a [[Cells]] internal slot, throw a
+ // TypeError exception.
+ if (!args.thisv().isObject() ||
+ !args.thisv().toObject().is<FinalizationRegistryObject>()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_NOT_A_FINALIZATION_REGISTRY,
+ "Receiver of FinalizationRegistry.register call");
+ return false;
+ }
+
+ RootedFinalizationRegistryObject registry(
+ cx, &args.thisv().toObject().as<FinalizationRegistryObject>());
+
+ // 4. If Type(target) is not Object, throw a TypeError exception.
+ if (!args.get(0).isObject()) {
+ JS_ReportErrorNumberASCII(
+ cx, GetErrorMessage, nullptr, JSMSG_OBJECT_REQUIRED,
+ "target argument to FinalizationRegistry.register");
+ return false;
+ }
+
+ RootedObject target(cx, &args[0].toObject());
+
+ // 5. If SameValue(target, heldValue), throw a TypeError exception.
+ if (args.get(1).isObject() && &args.get(1).toObject() == target) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_BAD_HELD_VALUE);
+ return false;
+ }
+
+ HandleValue heldValue = args.get(1);
+
+ // 6. If Type(unregisterToken) is not Object,
+ // a. If unregisterToken is not undefined, throw a TypeError exception.
+ if (!args.get(2).isUndefined() && !args.get(2).isObject()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_BAD_UNREGISTER_TOKEN,
+ "FinalizationRegistry.register");
+ return false;
+ }
+
+ RootedObject unregisterToken(cx);
+ if (!args.get(2).isUndefined()) {
+ unregisterToken = &args[2].toObject();
+ }
+
+ // Create the finalization record representing this target and heldValue.
+ Rooted<FinalizationQueueObject*> queue(cx, registry->queue());
+ Rooted<FinalizationRecordObject*> record(
+ cx, FinalizationRecordObject::create(cx, queue, heldValue));
+ if (!record) {
+ return false;
+ }
+
+ // Add the record to the registrations if an unregister token was supplied.
+ if (unregisterToken &&
+ !addRegistration(cx, registry, unregisterToken, record)) {
+ return false;
+ }
+
+ auto registrationsGuard = mozilla::MakeScopeExit([&] {
+ if (unregisterToken) {
+ removeRegistrationOnError(registry, unregisterToken, record);
+ }
+ });
+
+ // Fully unwrap the target to pass it to the GC.
+ RootedObject unwrappedTarget(cx);
+ unwrappedTarget = CheckedUnwrapDynamic(target, cx);
+ if (!unwrappedTarget) {
+ ReportAccessDenied(cx);
+ return false;
+ }
+
+ // If the target is a DOM wrapper, preserve it.
+ if (!preserveDOMWrapper(cx, target)) {
+ return false;
+ }
+
+ // Wrap the record into the compartment of the target.
+ RootedObject wrappedRecord(cx, record);
+ AutoRealm ar(cx, unwrappedTarget);
+ if (!JS_WrapObject(cx, &wrappedRecord)) {
+ return false;
+ }
+
+ if (JS_IsDeadWrapper(wrappedRecord)) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT);
+ return false;
+ }
+
+ // Register the record with the target.
+ gc::GCRuntime* gc = &cx->runtime()->gc;
+ if (!gc->registerWithFinalizationRegistry(cx, unwrappedTarget,
+ wrappedRecord)) {
+ return false;
+ }
+
+ registrationsGuard.release();
+ args.rval().setUndefined();
+ return true;
+}
+
+/* static */
+bool FinalizationRegistryObject::preserveDOMWrapper(JSContext* cx,
+ HandleObject obj) {
+ if (!MaybePreserveDOMWrapper(cx, obj)) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_BAD_FINALIZATION_REGISTRY_OBJECT);
+ return false;
+ }
+
+ return true;
+}
+
+/* static */
+bool FinalizationRegistryObject::addRegistration(
+ JSContext* cx, HandleFinalizationRegistryObject registry,
+ HandleObject unregisterToken, HandleFinalizationRecordObject record) {
+ // Add the record to the list of records associated with this unregister
+ // token.
+
+ MOZ_ASSERT(unregisterToken);
+ MOZ_ASSERT(registry->registrations());
+
+ auto& map = *registry->registrations();
+ Rooted<FinalizationRegistrationsObject*> recordsObject(cx);
+ JSObject* obj = map.lookup(unregisterToken);
+ if (obj) {
+ recordsObject = &obj->as<FinalizationRegistrationsObject>();
+ } else {
+ recordsObject = FinalizationRegistrationsObject::create(cx);
+ if (!recordsObject || !map.add(cx, unregisterToken, recordsObject)) {
+ return false;
+ }
+ }
+
+ if (!recordsObject->append(record)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+
+ return true;
+}
+
+/* static */ void FinalizationRegistryObject::removeRegistrationOnError(
+ HandleFinalizationRegistryObject registry, HandleObject unregisterToken,
+ HandleFinalizationRecordObject record) {
+ // Remove a registration if something went wrong before we added it to the
+ // target zone's map. Note that this can't remove a registration after that
+ // point.
+
+ MOZ_ASSERT(unregisterToken);
+ MOZ_ASSERT(registry->registrations());
+ JS::AutoAssertNoGC nogc;
+
+ auto& map = *registry->registrations();
+ JSObject* obj = map.lookup(unregisterToken);
+ MOZ_ASSERT(obj);
+ auto records = &obj->as<FinalizationRegistrationsObject>();
+ records->remove(record);
+
+ if (records->empty()) {
+ map.remove(unregisterToken);
+ }
+}
+
+// FinalizationRegistry.prototype.unregister ( unregisterToken )
+// https://tc39.es/proposal-weakrefs/#sec-finalization-registry.prototype.unregister
+/* static */
+bool FinalizationRegistryObject::unregister(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ // 1. Let finalizationRegistry be the this value.
+ // 2. If Type(finalizationRegistry) is not Object, throw a TypeError
+ // exception.
+ // 3. If finalizationRegistry does not have a [[Cells]] internal slot, throw a
+ // TypeError exception.
+ if (!args.thisv().isObject() ||
+ !args.thisv().toObject().is<FinalizationRegistryObject>()) {
+ JS_ReportErrorNumberASCII(
+ cx, GetErrorMessage, nullptr, JSMSG_NOT_A_FINALIZATION_REGISTRY,
+ "Receiver of FinalizationRegistry.unregister call");
+ return false;
+ }
+
+ RootedFinalizationRegistryObject registry(
+ cx, &args.thisv().toObject().as<FinalizationRegistryObject>());
+
+ // 4. If Type(unregisterToken) is not Object, throw a TypeError exception.
+ if (!args.get(0).isObject()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_BAD_UNREGISTER_TOKEN,
+ "FinalizationRegistry.unregister");
+ return false;
+ }
+
+ RootedObject unregisterToken(cx, &args[0].toObject());
+
+ // 5. Let removed be false.
+ bool removed = false;
+
+ // 6. For each Record { [[Target]], [[HeldValue]], [[UnregisterToken]] } cell
+ // that is an element of finalizationRegistry.[[Cells]], do
+ // a. If SameValue(cell.[[UnregisterToken]], unregisterToken) is true, then
+ // i. Remove cell from finalizationRegistry.[[Cells]].
+ // ii. Set removed to true.
+
+ RootedObject obj(cx, registry->registrations()->lookup(unregisterToken));
+ if (obj) {
+ auto* records = obj->as<FinalizationRegistrationsObject>().records();
+ MOZ_ASSERT(records);
+ MOZ_ASSERT(!records->empty());
+ for (FinalizationRecordObject* record : *records) {
+ if (unregisterRecord(record)) {
+ removed = true;
+ }
+ }
+ registry->registrations()->remove(unregisterToken);
+ }
+
+ // 7. Return removed.
+ args.rval().setBoolean(removed);
+ return true;
+}
+
+/* static */
+bool FinalizationRegistryObject::unregisterRecord(
+ FinalizationRecordObject* record) {
+ if (!record->isRegistered()) {
+ return false;
+ }
+
+ // Clear the fields of this record; it will be removed from the target's
+ // list when it is next swept.
+ record->clear();
+ return true;
+}
+
+// FinalizationRegistry.prototype.cleanupSome ( [ callback ] )
+// https://tc39.es/proposal-weakrefs/#sec-finalization-registry.prototype.cleanupSome
+bool FinalizationRegistryObject::cleanupSome(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ // 1. Let finalizationRegistry be the this value.
+ // 2. Perform ? RequireInternalSlot(finalizationRegistry, [[Cells]]).
+ if (!args.thisv().isObject() ||
+ !args.thisv().toObject().is<FinalizationRegistryObject>()) {
+ JS_ReportErrorNumberASCII(
+ cx, GetErrorMessage, nullptr, JSMSG_NOT_A_FINALIZATION_REGISTRY,
+ "Receiver of FinalizationRegistry.cleanupSome call");
+ return false;
+ }
+
+ RootedFinalizationRegistryObject registry(
+ cx, &args.thisv().toObject().as<FinalizationRegistryObject>());
+
+ // 3. If callback is not undefined and IsCallable(callback) is false, throw a
+ // TypeError exception.
+ RootedObject cleanupCallback(cx);
+ if (!args.get(0).isUndefined()) {
+ cleanupCallback = ValueToCallable(cx, args.get(0), -1, NO_CONSTRUCT);
+ if (!cleanupCallback) {
+ return false;
+ }
+ }
+
+ RootedFinalizationQueueObject queue(cx, registry->queue());
+ if (!FinalizationQueueObject::cleanupQueuedRecords(cx, queue,
+ cleanupCallback)) {
+ return false;
+ }
+
+ args.rval().setUndefined();
+ return true;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// FinalizationQueueObject
+
+// Bug 1600300: FinalizationQueueObject is foreground finalized so that
+// HeapPtr destructors never see referents with released arenas. When this is
+// fixed we may be able to make this background finalized again.
+const JSClass FinalizationQueueObject::class_ = {
+ "FinalizationQueue",
+ JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_FOREGROUND_FINALIZE,
+ &classOps_};
+
+const JSClassOps FinalizationQueueObject::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ FinalizationQueueObject::finalize, // finalize
+ nullptr, // call
+ nullptr, // construct
+ FinalizationQueueObject::trace, // trace
+};
+
+/* static */
+FinalizationQueueObject* FinalizationQueueObject::create(
+ JSContext* cx, HandleObject cleanupCallback) {
+ MOZ_ASSERT(cleanupCallback);
+
+ Rooted<UniquePtr<FinalizationRecordVector>> recordsToBeCleanedUp(
+ cx, cx->make_unique<FinalizationRecordVector>(cx->zone()));
+ if (!recordsToBeCleanedUp) {
+ return nullptr;
+ }
+
+ Handle<PropertyName*> funName = cx->names().empty;
+ RootedFunction doCleanupFunction(
+ cx, NewNativeFunction(cx, doCleanup, 0, funName,
+ gc::AllocKind::FUNCTION_EXTENDED));
+ if (!doCleanupFunction) {
+ return nullptr;
+ }
+
+ // It's problematic storing a CCW to a global in another compartment because
+ // you don't know how far to unwrap it to get the original object
+ // back. Instead store a CCW to a plain object in the same compartment as the
+ // global (this uses Object.prototype).
+ RootedObject incumbentObject(cx);
+ if (!GetObjectFromIncumbentGlobal(cx, &incumbentObject) || !incumbentObject) {
+ return nullptr;
+ }
+
+ FinalizationQueueObject* queue =
+ NewObjectWithGivenProto<FinalizationQueueObject>(cx, nullptr);
+ if (!queue) {
+ return nullptr;
+ }
+
+ queue->initReservedSlot(CleanupCallbackSlot, ObjectValue(*cleanupCallback));
+ queue->initReservedSlot(IncumbentObjectSlot, ObjectValue(*incumbentObject));
+ InitReservedSlot(queue, RecordsToBeCleanedUpSlot,
+ recordsToBeCleanedUp.release(),
+ MemoryUse::FinalizationRegistryRecordVector);
+ queue->initReservedSlot(IsQueuedForCleanupSlot, BooleanValue(false));
+ queue->initReservedSlot(DoCleanupFunctionSlot,
+ ObjectValue(*doCleanupFunction));
+ queue->initReservedSlot(HasRegistrySlot, BooleanValue(false));
+
+ doCleanupFunction->setExtendedSlot(DoCleanupFunction_QueueSlot,
+ ObjectValue(*queue));
+
+ return queue;
+}
+
+/* static */
+void FinalizationQueueObject::trace(JSTracer* trc, JSObject* obj) {
+ auto queue = &obj->as<FinalizationQueueObject>();
+
+ if (FinalizationRecordVector* records = queue->recordsToBeCleanedUp()) {
+ records->trace(trc);
+ }
+}
+
+/* static */
+void FinalizationQueueObject::finalize(JS::GCContext* gcx, JSObject* obj) {
+ auto queue = &obj->as<FinalizationQueueObject>();
+
+ gcx->delete_(obj, queue->recordsToBeCleanedUp(),
+ MemoryUse::FinalizationRegistryRecordVector);
+}
+
+void FinalizationQueueObject::setHasRegistry(bool newValue) {
+ MOZ_ASSERT(hasRegistry() != newValue);
+
+ // Suppress our assertions about touching grey things. It's OK for us to set a
+ // boolean slot even if this object is gray.
+ AutoTouchingGrayThings atgt;
+
+ setReservedSlot(HasRegistrySlot, BooleanValue(newValue));
+}
+
+bool FinalizationQueueObject::hasRegistry() const {
+ return getReservedSlot(HasRegistrySlot).toBoolean();
+}
+
+inline JSObject* FinalizationQueueObject::cleanupCallback() const {
+ Value value = getReservedSlot(CleanupCallbackSlot);
+ if (value.isUndefined()) {
+ return nullptr;
+ }
+ return &value.toObject();
+}
+
+JSObject* FinalizationQueueObject::incumbentObject() const {
+ Value value = getReservedSlot(IncumbentObjectSlot);
+ if (value.isUndefined()) {
+ return nullptr;
+ }
+ return &value.toObject();
+}
+
+FinalizationRecordVector* FinalizationQueueObject::recordsToBeCleanedUp()
+ const {
+ Value value = getReservedSlot(RecordsToBeCleanedUpSlot);
+ if (value.isUndefined()) {
+ return nullptr;
+ }
+ return static_cast<FinalizationRecordVector*>(value.toPrivate());
+}
+
+bool FinalizationQueueObject::isQueuedForCleanup() const {
+ return getReservedSlot(IsQueuedForCleanupSlot).toBoolean();
+}
+
+JSFunction* FinalizationQueueObject::doCleanupFunction() const {
+ Value value = getReservedSlot(DoCleanupFunctionSlot);
+ if (value.isUndefined()) {
+ return nullptr;
+ }
+ return &value.toObject().as<JSFunction>();
+}
+
+void FinalizationQueueObject::queueRecordToBeCleanedUp(
+ FinalizationRecordObject* record) {
+ AutoEnterOOMUnsafeRegion oomUnsafe;
+ if (!recordsToBeCleanedUp()->append(record)) {
+ oomUnsafe.crash("FinalizationQueueObject::queueRecordsToBeCleanedUp");
+ }
+}
+
+void FinalizationQueueObject::setQueuedForCleanup(bool value) {
+ MOZ_ASSERT(value != isQueuedForCleanup());
+ setReservedSlot(IsQueuedForCleanupSlot, BooleanValue(value));
+}
+
+/* static */
+bool FinalizationQueueObject::doCleanup(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ RootedFunction callee(cx, &args.callee().as<JSFunction>());
+
+ Value value = callee->getExtendedSlot(DoCleanupFunction_QueueSlot);
+ RootedFinalizationQueueObject queue(
+ cx, &value.toObject().as<FinalizationQueueObject>());
+
+ queue->setQueuedForCleanup(false);
+ return cleanupQueuedRecords(cx, queue);
+}
+
+// CleanupFinalizationRegistry ( finalizationRegistry [ , callback ] )
+// https://tc39.es/proposal-weakrefs/#sec-cleanup-finalization-registry
+/* static */
+bool FinalizationQueueObject::cleanupQueuedRecords(
+ JSContext* cx, HandleFinalizationQueueObject queue,
+ HandleObject callbackArg) {
+ MOZ_ASSERT(cx->compartment() == queue->compartment());
+
+ // 2. If callback is undefined, set callback to
+ // finalizationRegistry.[[CleanupCallback]].
+ RootedValue callback(cx);
+ if (callbackArg) {
+ callback.setObject(*callbackArg);
+ } else {
+ JSObject* cleanupCallback = queue->cleanupCallback();
+ MOZ_ASSERT(cleanupCallback);
+ callback.setObject(*cleanupCallback);
+ }
+
+ // 3. While finalizationRegistry.[[Cells]] contains a Record cell such that
+ // cell.[[WeakRefTarget]] is empty, then an implementation may perform the
+ // following steps,
+ // a. Choose any such cell.
+ // b. Remove cell from finalizationRegistry.[[Cells]].
+ // c. Perform ? Call(callback, undefined, « cell.[[HeldValue]] »).
+
+ RootedValue heldValue(cx);
+ RootedValue rval(cx);
+ FinalizationRecordVector* records = queue->recordsToBeCleanedUp();
+ while (!records->empty()) {
+ FinalizationRecordObject* record = records->popCopy();
+
+ // Skip over records that have been unregistered.
+ if (!record->isRegistered()) {
+ continue;
+ }
+
+ heldValue.set(record->heldValue());
+
+ record->clear();
+
+ if (!Call(cx, callback, UndefinedHandleValue, heldValue, &rval)) {
+ return false;
+ }
+ }
+
+ return true;
+}