/* -*- 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 "gc/GCContext-inl.h" #include "gc/WeakMap-inl.h" #include "vm/JSObject-inl.h" #include "vm/NativeObject-inl.h" using namespace js; /////////////////////////////////////////////////////////////////////////// // FinalizationRecordObject const JSClassOps FinalizationRecordObject::classOps_ = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve finalize, // finalize nullptr, // call nullptr, // construct nullptr, // trace }; const JSClass FinalizationRecordObject::class_ = { "FinalizationRecord", JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_FOREGROUND_FINALIZE, &classOps_, JS_NULL_CLASS_SPEC, &classExtension_, }; /* static */ FinalizationRecordObject* FinalizationRecordObject::create( JSContext* cx, HandleFinalizationQueueObject queue, HandleValue heldValue) { MOZ_ASSERT(queue); auto record = NewObjectWithGivenProto(cx, nullptr); if (!record) { return nullptr; } MOZ_ASSERT(queue->compartment() == record->compartment()); record->initReservedSlot(QueueSlot, ObjectValue(*queue)); record->initReservedSlot(HeldValueSlot, heldValue); return record; } /* static */ void FinalizationRecordObject::finalize(JS::GCContext* gcx, JSObject* obj) { auto* record = &obj->as(); MOZ_ASSERT_IF(!record->isInRecordMap(), !record->isInList()); record->unlink(); } FinalizationQueueObject* FinalizationRecordObject::queue() const { Value value = getReservedSlot(QueueSlot); if (value.isUndefined()) { return nullptr; } return &value.toObject().as(); } Value FinalizationRecordObject::heldValue() const { return getReservedSlot(HeldValueSlot); } bool FinalizationRecordObject::isRegistered() const { MOZ_ASSERT_IF(!queue(), heldValue().isUndefined()); return queue(); } #ifdef DEBUG void FinalizationRecordObject::setState(State state) { Value value; if (state != Unknown) { value = Int32Value(int32_t(state)); } setReservedSlot(DebugStateSlot, value); } FinalizationRecordObject::State FinalizationRecordObject::getState() const { Value value = getReservedSlot(DebugStateSlot); if (value.isUndefined()) { return Unknown; } State state = State(value.toInt32()); MOZ_ASSERT(state == InRecordMap || state == InQueue); return state; } #endif void FinalizationRecordObject::setInRecordMap(bool newValue) { #ifdef DEBUG State newState = newValue ? InRecordMap : Unknown; MOZ_ASSERT(getState() != newState); setState(newState); #endif } void FinalizationRecordObject::setInQueue(bool newValue) { #ifdef DEBUG State newState = newValue ? InQueue : Unknown; MOZ_ASSERT(getState() != newState); setState(newState); #endif } void FinalizationRecordObject::clear() { MOZ_ASSERT(queue()); setReservedSlot(QueueSlot, UndefinedValue()); setReservedSlot(HeldValueSlot, UndefinedValue()); MOZ_ASSERT(!isRegistered()); } /////////////////////////////////////////////////////////////////////////// // 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, GenericCreatePrototype, nullptr, nullptr, methods_, properties_, }; const JSFunctionSpec FinalizationRegistryObject::methods_[] = { JS_FN("register", register_, 2, 0), JS_FN("unregister", unregister, 1, 0), JS_FN("cleanupSome", 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> records( cx, cx->make_unique(cx->zone())); if (!records) { return false; } Rooted> registrations( cx, cx->make_unique(cx)); if (!registrations) { return false; } RootedFinalizationQueueObject queue( cx, FinalizationQueueObject::create(cx, cleanupCallback)); if (!queue) { return false; } RootedFinalizationRegistryObject registry( cx, NewObjectWithClassProto(cx, proto)); if (!registry) { return false; } registry->initReservedSlot(QueueSlot, ObjectValue(*queue)); InitReservedSlot(registry, RecordsWithoutTokenSlot, records.release(), MemoryUse::FinalizationRecordVector); 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 finalization records. auto* registry = &obj->as(); if (FinalizationRecordVector* records = registry->recordsWithoutToken()) { records->trace(trc); } // Trace the records referred to by the registrations map, but not its keys // which are weakly held. if (RegistrationsMap* registrations = registry->registrations()) { for (auto iter = registrations->iter(); !iter.done(); iter.next()) { iter.get().value().trace(trc); } } } void FinalizationRegistryObject::traceWeak(JSTracer* trc) { // Trace and update the contents of the registrations map's keys, which // are weakly held. MOZ_ASSERT(registrations()); for (auto iter = registrations()->modIter(); !iter.done(); iter.next()) { auto result = TraceWeakEdge(trc, &iter.getMutable().mutableKey(), "FinalizationRegistry unregister token"); if (result.isDead()) { // The unregister token has died and can no longer be used to unregister // registrations. However those registrations remain valid. AutoEnterOOMUnsafeRegion oomUnsafe; if (!recordsWithoutToken()->appendAll(std::move(iter.get().value()))) { oomUnsafe.crash("FinalizationRegistryObject::traceWeak"); } iter.remove(); } } } /* static */ void FinalizationRegistryObject::finalize(JS::GCContext* gcx, JSObject* obj) { auto registry = &obj->as(); // The queue's flag should have been updated by // GCRuntime::sweepFinalizationRegistries. MOZ_ASSERT_IF(registry->queue(), !registry->queue()->hasRegistry()); gcx->delete_(obj, registry->recordsWithoutToken(), MemoryUse::FinalizationRecordVector); gcx->delete_(obj, registry->registrations(), MemoryUse::FinalizationRegistryRegistrations); } FinalizationRecordVector* FinalizationRegistryObject::recordsWithoutToken() const { Value value = getReservedSlot(RecordsWithoutTokenSlot); if (value.isUndefined()) { return nullptr; } return static_cast(value.toPrivate()); } FinalizationQueueObject* FinalizationRegistryObject::queue() const { Value value = getReservedSlot(QueueSlot); if (value.isUndefined()) { return nullptr; } return &value.toObject().as(); } FinalizationRegistryObject::RegistrationsMap* FinalizationRegistryObject::registrations() const { Value value = getReservedSlot(RegistrationsSlot); if (value.isUndefined()) { return nullptr; } return static_cast(value.toPrivate()); } // FinalizationRegistry.prototype.register(target, heldValue [, unregisterToken // ]) // https://tc39.es/ecma262/#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. Perform ? RequireInternalSlot(finalizationRegistry, [[Cells]]). if (!args.thisv().isObject() || !args.thisv().toObject().is()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_A_FINALIZATION_REGISTRY, "Receiver of FinalizationRegistry.register call"); return false; } RootedFinalizationRegistryObject registry( cx, &args.thisv().toObject().as()); // 3. If CanBeHeldWeakly(target) is false, throw a TypeError exception. RootedValue target(cx, args.get(0)); if (!CanBeHeldWeakly(target)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_FINALIZATION_REGISTRY_TARGET); return false; } // 4. If SameValue(target, heldValue) is true, throw a TypeError exception. HandleValue heldValue = args.get(1); if (heldValue == target) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_HELD_VALUE); return false; } // 5. If CanBeHeldWeakly(unregisterToken) is false, then: // a. If unregisterToken is not undefined, throw a TypeError exception. // b. Set unregisterToken to empty. RootedValue unregisterToken(cx, args.get(2)); if (!CanBeHeldWeakly(unregisterToken) && !unregisterToken.isUndefined()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_UNREGISTER_TOKEN, "FinalizationRegistry.register"); return false; } // Create the finalization record representing this target and heldValue. Rooted queue(cx, registry->queue()); Rooted record( cx, FinalizationRecordObject::create(cx, queue, heldValue)); if (!record) { return false; } if (!addRegistration(cx, registry, unregisterToken, record)) { return false; } auto registrationGuard = mozilla::MakeScopeExit( [&] { removeRegistrationOnError(registry, unregisterToken, record); }); bool isPermanent = false; if (target.isObject()) { // Fully unwrap the target to register it with the GC. RootedObject object(cx, CheckedUnwrapDynamic(&target.toObject(), cx)); if (!object) { ReportAccessDenied(cx); return false; } target = ObjectValue(*object); // If the target is a DOM wrapper, preserve it. if (!preserveDOMWrapper(cx, object)) { return false; } } else { JS::Symbol* symbol = target.toSymbol(); isPermanent = symbol->isPermanentAndMayBeShared(); } // Register the record with the target, unless the target is permanent. // (See the note following https://tc39.es/ecma262/#sec-canbeheldweakly) if (!isPermanent) { gc::GCRuntime* gc = &cx->runtime()->gc; if (!gc->registerWithFinalizationRegistry(cx, target, record)) { return false; } } // 8. Return undefined. registrationGuard.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, HandleValue unregisterToken, HandleFinalizationRecordObject record) { // Add the record to the list of records associated with this unregister // token, or add it to the main list. MOZ_ASSERT(registry->registrations()); MOZ_ASSERT(unregisterToken.isUndefined() || CanBeHeldWeakly(unregisterToken)); if (unregisterToken.isUndefined()) { if (!registry->recordsWithoutToken()->append(record)) { ReportOutOfMemory(cx); return false; } return true; } auto& map = *registry->registrations(); auto ptr = map.lookupForAdd(unregisterToken.get()); if (!ptr.found() && !map.add(ptr, unregisterToken, FinalizationRecordVector(cx->zone()))) { ReportOutOfMemory(cx); return false; } if (!ptr->value().append(record)) { ReportOutOfMemory(cx); return false; } return true; } /* static */ void FinalizationRegistryObject::removeRegistrationOnError( HandleFinalizationRegistryObject registry, HandleValue 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(registry->registrations()); MOZ_ASSERT(unregisterToken.isUndefined() || CanBeHeldWeakly(unregisterToken)); JS::AutoAssertNoGC nogc; if (unregisterToken.isUndefined()) { MOZ_ASSERT(registry->recordsWithoutToken()->back() == record); registry->recordsWithoutToken()->popBack(); return; } auto ptr = registry->registrations()->lookup(unregisterToken); MOZ_ASSERT(ptr.found()); FinalizationRecordVector& records = ptr->value(); MOZ_ASSERT(records.back() == record); records.popBack(); if (records.empty()) { registry->registrations()->remove(ptr); } } // 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()) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_NOT_A_FINALIZATION_REGISTRY, "Receiver of FinalizationRegistry.unregister call"); return false; } RootedFinalizationRegistryObject registry( cx, &args.thisv().toObject().as()); // 4. If Type(unregisterToken) is not Object, throw a TypeError exception. RootedValue unregisterToken(cx, args.get(0)); if (!CanBeHeldWeakly(unregisterToken)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_UNREGISTER_TOKEN, "FinalizationRegistry.unregister"); return false; } // 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. RegistrationsMap* map = registry->registrations(); auto ptr = map->lookup(unregisterToken); if (ptr) { FinalizationRecordVector& records = ptr->value(); MOZ_ASSERT(!records.empty()); for (FinalizationRecordObject* record : records) { if (unregisterRecord(record)) { removed = true; } } map->remove(unregisterToken); } // 7. Return removed. args.rval().setBoolean(removed); return true; } /* static */ bool FinalizationRegistryObject::unregisterRecord( FinalizationRecordObject* record) { if (!record->isRegistered()) { MOZ_ASSERT(!record->isInList()); return false; } // Remove record from the target list if present. record->unlink(); // Clear the fields of this record, marking it as unregistered. It will be // removed from relevant data structures when they are next swept. record->clear(); MOZ_ASSERT(!record->isRegistered()); 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()) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_NOT_A_FINALIZATION_REGISTRY, "Receiver of FinalizationRegistry.cleanupSome call"); return false; } RootedFinalizationRegistryObject registry( cx, &args.thisv().toObject().as()); // 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> recordsToBeCleanedUp( cx, cx->make_unique(cx->zone())); if (!recordsToBeCleanedUp) { return nullptr; } Handle 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). Rooted hostDefinedData(cx); if (!GetObjectFromHostDefinedData(cx, &hostDefinedData)) { return nullptr; } FinalizationQueueObject* queue = NewObjectWithGivenProto(cx, nullptr); if (!queue) { return nullptr; } queue->initReservedSlot(CleanupCallbackSlot, ObjectValue(*cleanupCallback)); queue->initReservedSlot(HostDefinedDataSlot, JS::ObjectOrNullValue(hostDefinedData)); 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(); if (FinalizationRecordVector* records = queue->recordsToBeCleanedUp()) { records->trace(trc); } } /* static */ void FinalizationQueueObject::finalize(JS::GCContext* gcx, JSObject* obj) { auto* queue = &obj->as(); 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::getHostDefinedData() const { Value value = getReservedSlot(HostDefinedDataSlot); if (value.isUndefined()) { return nullptr; } return value.toObjectOrNull(); } bool FinalizationQueueObject::hasRecordsToCleanUp() const { FinalizationRecordVector* records = recordsToBeCleanedUp(); return records && !records->empty(); } FinalizationRecordVector* FinalizationQueueObject::recordsToBeCleanedUp() const { Value value = getReservedSlot(RecordsToBeCleanedUpSlot); if (value.isUndefined()) { return nullptr; } return static_cast(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(); } void FinalizationQueueObject::queueRecordToBeCleanedUp( FinalizationRecordObject* record) { MOZ_ASSERT(hasRegistry()); MOZ_ASSERT(!record->isInQueue()); record->setInQueue(true); 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()); Value value = callee->getExtendedSlot(DoCleanupFunction_QueueSlot); RootedFinalizationQueueObject queue( cx, &value.toObject().as()); 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(); MOZ_ASSERT(!record->isInRecordMap()); JS::ExposeObjectToActiveJS(record); MOZ_ASSERT(record->isInQueue()); record->setInQueue(false); // 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; }