FinalizationRegistryObject.cpp (27837B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 // Implementation of JS FinalizationRegistry objects. 8 9 #include "builtin/FinalizationRegistryObject.h" 10 11 #include "mozilla/ScopeExit.h" 12 13 #include "jsapi.h" 14 15 #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* 16 #include "vm/GlobalObject.h" 17 #include "vm/Interpreter.h" 18 19 #include "gc/GCContext-inl.h" 20 #include "gc/WeakMap-inl.h" 21 #include "vm/JSObject-inl.h" 22 #include "vm/NativeObject-inl.h" 23 24 using namespace js; 25 26 /////////////////////////////////////////////////////////////////////////// 27 // FinalizationRecordObject 28 29 const JSClassOps FinalizationRecordObject::classOps_ = { 30 nullptr, // addProperty 31 nullptr, // delProperty 32 nullptr, // enumerate 33 nullptr, // newEnumerate 34 nullptr, // resolve 35 nullptr, // mayResolve 36 finalize, // finalize 37 nullptr, // call 38 nullptr, // construct 39 nullptr, // trace 40 }; 41 42 const JSClass FinalizationRecordObject::class_ = { 43 "FinalizationRecord", 44 JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_FOREGROUND_FINALIZE, 45 &classOps_, 46 JS_NULL_CLASS_SPEC, 47 &classExtension_, 48 }; 49 50 /* static */ 51 FinalizationRecordObject* FinalizationRecordObject::create( 52 JSContext* cx, HandleFinalizationQueueObject queue, HandleValue heldValue) { 53 MOZ_ASSERT(queue); 54 55 auto record = NewObjectWithGivenProto<FinalizationRecordObject>(cx, nullptr); 56 if (!record) { 57 return nullptr; 58 } 59 60 MOZ_ASSERT(queue->compartment() == record->compartment()); 61 62 record->initReservedSlot(QueueSlot, ObjectValue(*queue)); 63 record->initReservedSlot(HeldValueSlot, heldValue); 64 65 return record; 66 } 67 68 /* static */ 69 void FinalizationRecordObject::finalize(JS::GCContext* gcx, JSObject* obj) { 70 auto* record = &obj->as<FinalizationRecordObject>(); 71 MOZ_ASSERT_IF(!record->isInRecordMap(), !record->isInList()); 72 record->unlink(); 73 } 74 75 FinalizationQueueObject* FinalizationRecordObject::queue() const { 76 Value value = getReservedSlot(QueueSlot); 77 if (value.isUndefined()) { 78 return nullptr; 79 } 80 return &value.toObject().as<FinalizationQueueObject>(); 81 } 82 83 Value FinalizationRecordObject::heldValue() const { 84 return getReservedSlot(HeldValueSlot); 85 } 86 87 bool FinalizationRecordObject::isRegistered() const { 88 MOZ_ASSERT_IF(!queue(), heldValue().isUndefined()); 89 return queue(); 90 } 91 92 #ifdef DEBUG 93 94 void FinalizationRecordObject::setState(State state) { 95 Value value; 96 if (state != Unknown) { 97 value = Int32Value(int32_t(state)); 98 } 99 setReservedSlot(DebugStateSlot, value); 100 } 101 102 FinalizationRecordObject::State FinalizationRecordObject::getState() const { 103 Value value = getReservedSlot(DebugStateSlot); 104 if (value.isUndefined()) { 105 return Unknown; 106 } 107 108 State state = State(value.toInt32()); 109 MOZ_ASSERT(state == InRecordMap || state == InQueue); 110 return state; 111 } 112 113 #endif 114 115 void FinalizationRecordObject::setInRecordMap(bool newValue) { 116 #ifdef DEBUG 117 State newState = newValue ? InRecordMap : Unknown; 118 MOZ_ASSERT(getState() != newState); 119 setState(newState); 120 #endif 121 } 122 123 void FinalizationRecordObject::setInQueue(bool newValue) { 124 #ifdef DEBUG 125 State newState = newValue ? InQueue : Unknown; 126 MOZ_ASSERT(getState() != newState); 127 setState(newState); 128 #endif 129 } 130 131 void FinalizationRecordObject::clear() { 132 MOZ_ASSERT(queue()); 133 setReservedSlot(QueueSlot, UndefinedValue()); 134 setReservedSlot(HeldValueSlot, UndefinedValue()); 135 MOZ_ASSERT(!isRegistered()); 136 } 137 138 /////////////////////////////////////////////////////////////////////////// 139 // FinalizationRegistryObject 140 141 // Bug 1600300: FinalizationRegistryObject is foreground finalized so that 142 // HeapPtr destructors never see referents with released arenas. When this is 143 // fixed we may be able to make this background finalized again. 144 const JSClass FinalizationRegistryObject::class_ = { 145 "FinalizationRegistry", 146 JSCLASS_HAS_CACHED_PROTO(JSProto_FinalizationRegistry) | 147 JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_FOREGROUND_FINALIZE, 148 &classOps_, 149 &classSpec_, 150 }; 151 152 const JSClass FinalizationRegistryObject::protoClass_ = { 153 "FinalizationRegistry.prototype", 154 JSCLASS_HAS_CACHED_PROTO(JSProto_FinalizationRegistry), 155 JS_NULL_CLASS_OPS, 156 &classSpec_, 157 }; 158 159 const JSClassOps FinalizationRegistryObject::classOps_ = { 160 nullptr, // addProperty 161 nullptr, // delProperty 162 nullptr, // enumerate 163 nullptr, // newEnumerate 164 nullptr, // resolve 165 nullptr, // mayResolve 166 FinalizationRegistryObject::finalize, // finalize 167 nullptr, // call 168 nullptr, // construct 169 FinalizationRegistryObject::trace, // trace 170 }; 171 172 const ClassSpec FinalizationRegistryObject::classSpec_ = { 173 GenericCreateConstructor<construct, 1, gc::AllocKind::FUNCTION>, 174 GenericCreatePrototype<FinalizationRegistryObject>, 175 nullptr, 176 nullptr, 177 methods_, 178 properties_, 179 }; 180 181 const JSFunctionSpec FinalizationRegistryObject::methods_[] = { 182 JS_FN("register", register_, 2, 0), 183 JS_FN("unregister", unregister, 1, 0), 184 JS_FN("cleanupSome", cleanupSome, 0, 0), 185 JS_FS_END, 186 }; 187 188 const JSPropertySpec FinalizationRegistryObject::properties_[] = { 189 JS_STRING_SYM_PS(toStringTag, "FinalizationRegistry", JSPROP_READONLY), 190 JS_PS_END, 191 }; 192 193 /* static */ 194 bool FinalizationRegistryObject::construct(JSContext* cx, unsigned argc, 195 Value* vp) { 196 CallArgs args = CallArgsFromVp(argc, vp); 197 198 if (!ThrowIfNotConstructing(cx, args, "FinalizationRegistry")) { 199 return false; 200 } 201 202 RootedObject cleanupCallback( 203 cx, ValueToCallable(cx, args.get(0), 1, NO_CONSTRUCT)); 204 if (!cleanupCallback) { 205 return false; 206 } 207 208 RootedObject proto(cx); 209 if (!GetPrototypeFromBuiltinConstructor( 210 cx, args, JSProto_FinalizationRegistry, &proto)) { 211 return false; 212 } 213 214 Rooted<UniquePtr<FinalizationRecordVector>> records( 215 cx, cx->make_unique<FinalizationRecordVector>(cx->zone())); 216 if (!records) { 217 return false; 218 } 219 220 Rooted<UniquePtr<RegistrationsMap>> registrations( 221 cx, cx->make_unique<RegistrationsMap>(cx)); 222 if (!registrations) { 223 return false; 224 } 225 226 RootedFinalizationQueueObject queue( 227 cx, FinalizationQueueObject::create(cx, cleanupCallback)); 228 if (!queue) { 229 return false; 230 } 231 232 RootedFinalizationRegistryObject registry( 233 cx, NewObjectWithClassProto<FinalizationRegistryObject>(cx, proto)); 234 if (!registry) { 235 return false; 236 } 237 238 registry->initReservedSlot(QueueSlot, ObjectValue(*queue)); 239 InitReservedSlot(registry, RecordsWithoutTokenSlot, records.release(), 240 MemoryUse::FinalizationRecordVector); 241 InitReservedSlot(registry, RegistrationsSlot, registrations.release(), 242 MemoryUse::FinalizationRegistryRegistrations); 243 244 if (!cx->runtime()->gc.addFinalizationRegistry(cx, registry)) { 245 return false; 246 } 247 248 queue->setHasRegistry(true); 249 250 args.rval().setObject(*registry); 251 return true; 252 } 253 254 /* static */ 255 void FinalizationRegistryObject::trace(JSTracer* trc, JSObject* obj) { 256 // Trace finalization records. 257 auto* registry = &obj->as<FinalizationRegistryObject>(); 258 if (FinalizationRecordVector* records = registry->recordsWithoutToken()) { 259 records->trace(trc); 260 } 261 262 // Trace the records referred to by the registrations map, but not its keys 263 // which are weakly held. 264 if (RegistrationsMap* registrations = registry->registrations()) { 265 for (auto iter = registrations->iter(); !iter.done(); iter.next()) { 266 iter.get().value().trace(trc); 267 } 268 } 269 } 270 271 void FinalizationRegistryObject::traceWeak(JSTracer* trc) { 272 // Trace and update the contents of the registrations map's keys, which 273 // are weakly held. 274 MOZ_ASSERT(registrations()); 275 276 for (auto iter = registrations()->modIter(); !iter.done(); iter.next()) { 277 auto result = TraceWeakEdge(trc, &iter.getMutable().mutableKey(), 278 "FinalizationRegistry unregister token"); 279 if (result.isDead()) { 280 // The unregister token has died and can no longer be used to unregister 281 // registrations. However those registrations remain valid. 282 AutoEnterOOMUnsafeRegion oomUnsafe; 283 if (!recordsWithoutToken()->appendAll(std::move(iter.get().value()))) { 284 oomUnsafe.crash("FinalizationRegistryObject::traceWeak"); 285 } 286 iter.remove(); 287 } 288 } 289 290 registrations()->compact(); 291 } 292 293 /* static */ 294 void FinalizationRegistryObject::finalize(JS::GCContext* gcx, JSObject* obj) { 295 auto registry = &obj->as<FinalizationRegistryObject>(); 296 297 // The queue's flag should have been updated by 298 // GCRuntime::sweepFinalizationRegistries. 299 MOZ_ASSERT_IF(registry->queue(), !registry->queue()->hasRegistry()); 300 301 gcx->delete_(obj, registry->recordsWithoutToken(), 302 MemoryUse::FinalizationRecordVector); 303 gcx->delete_(obj, registry->registrations(), 304 MemoryUse::FinalizationRegistryRegistrations); 305 } 306 307 FinalizationRecordVector* FinalizationRegistryObject::recordsWithoutToken() 308 const { 309 Value value = getReservedSlot(RecordsWithoutTokenSlot); 310 if (value.isUndefined()) { 311 return nullptr; 312 } 313 return static_cast<FinalizationRecordVector*>(value.toPrivate()); 314 } 315 316 FinalizationQueueObject* FinalizationRegistryObject::queue() const { 317 Value value = getReservedSlot(QueueSlot); 318 if (value.isUndefined()) { 319 return nullptr; 320 } 321 return &value.toObject().as<FinalizationQueueObject>(); 322 } 323 324 FinalizationRegistryObject::RegistrationsMap* 325 FinalizationRegistryObject::registrations() const { 326 Value value = getReservedSlot(RegistrationsSlot); 327 if (value.isUndefined()) { 328 return nullptr; 329 } 330 return static_cast<RegistrationsMap*>(value.toPrivate()); 331 } 332 333 // FinalizationRegistry.prototype.register(target, heldValue [, unregisterToken 334 // ]) 335 // https://tc39.es/ecma262/#sec-finalization-registry.prototype.register 336 /* static */ 337 bool FinalizationRegistryObject::register_(JSContext* cx, unsigned argc, 338 Value* vp) { 339 CallArgs args = CallArgsFromVp(argc, vp); 340 341 // 1. Let finalizationRegistry be the this value. 342 // 2. Perform ? RequireInternalSlot(finalizationRegistry, [[Cells]]). 343 if (!args.thisv().isObject() || 344 !args.thisv().toObject().is<FinalizationRegistryObject>()) { 345 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 346 JSMSG_NOT_A_FINALIZATION_REGISTRY, 347 "Receiver of FinalizationRegistry.register call"); 348 return false; 349 } 350 351 RootedFinalizationRegistryObject registry( 352 cx, &args.thisv().toObject().as<FinalizationRegistryObject>()); 353 354 // 3. If CanBeHeldWeakly(target) is false, throw a TypeError exception. 355 RootedValue target(cx, args.get(0)); 356 if (!CanBeHeldWeakly(target)) { 357 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 358 JSMSG_BAD_FINALIZATION_REGISTRY_TARGET); 359 return false; 360 } 361 362 // 4. If SameValue(target, heldValue) is true, throw a TypeError exception. 363 HandleValue heldValue = args.get(1); 364 if (heldValue == target) { 365 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 366 JSMSG_BAD_HELD_VALUE); 367 return false; 368 } 369 370 // 5. If CanBeHeldWeakly(unregisterToken) is false, then: 371 // a. If unregisterToken is not undefined, throw a TypeError exception. 372 // b. Set unregisterToken to empty. 373 RootedValue unregisterToken(cx, args.get(2)); 374 if (!CanBeHeldWeakly(unregisterToken) && !unregisterToken.isUndefined()) { 375 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 376 JSMSG_BAD_UNREGISTER_TOKEN, 377 "FinalizationRegistry.register"); 378 return false; 379 } 380 381 // Create the finalization record representing this target and heldValue. 382 Rooted<FinalizationQueueObject*> queue(cx, registry->queue()); 383 Rooted<FinalizationRecordObject*> record( 384 cx, FinalizationRecordObject::create(cx, queue, heldValue)); 385 if (!record) { 386 return false; 387 } 388 389 if (!addRegistration(cx, registry, unregisterToken, record)) { 390 return false; 391 } 392 auto registrationGuard = mozilla::MakeScopeExit( 393 [&] { removeRegistrationOnError(registry, unregisterToken, record); }); 394 395 bool isPermanent = false; 396 if (target.isObject()) { 397 // Fully unwrap the target to register it with the GC. 398 RootedObject object(cx, CheckedUnwrapDynamic(&target.toObject(), cx)); 399 if (!object) { 400 ReportAccessDenied(cx); 401 return false; 402 } 403 404 target = ObjectValue(*object); 405 406 // If the target is a DOM wrapper, preserve it. 407 if (!preserveDOMWrapper(cx, object)) { 408 return false; 409 } 410 } else { 411 JS::Symbol* symbol = target.toSymbol(); 412 isPermanent = symbol->isPermanentAndMayBeShared(); 413 } 414 415 // Register the record with the target, unless the target is permanent. 416 // (See the note following https://tc39.es/ecma262/#sec-canbeheldweakly) 417 if (!isPermanent) { 418 gc::GCRuntime* gc = &cx->runtime()->gc; 419 if (!gc->registerWithFinalizationRegistry(cx, target, record)) { 420 return false; 421 } 422 } 423 424 // 8. Return undefined. 425 registrationGuard.release(); 426 args.rval().setUndefined(); 427 return true; 428 } 429 430 /* static */ 431 bool FinalizationRegistryObject::preserveDOMWrapper(JSContext* cx, 432 HandleObject obj) { 433 if (!MaybePreserveDOMWrapper(cx, obj)) { 434 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 435 JSMSG_BAD_FINALIZATION_REGISTRY_OBJECT); 436 return false; 437 } 438 439 return true; 440 } 441 442 /* static */ 443 bool FinalizationRegistryObject::addRegistration( 444 JSContext* cx, HandleFinalizationRegistryObject registry, 445 HandleValue unregisterToken, HandleFinalizationRecordObject record) { 446 // Add the record to the list of records associated with this unregister 447 // token, or add it to the main list. 448 449 MOZ_ASSERT(registry->registrations()); 450 MOZ_ASSERT(unregisterToken.isUndefined() || CanBeHeldWeakly(unregisterToken)); 451 452 if (unregisterToken.isUndefined()) { 453 if (!registry->recordsWithoutToken()->append(record)) { 454 ReportOutOfMemory(cx); 455 return false; 456 } 457 return true; 458 } 459 460 auto& map = *registry->registrations(); 461 auto ptr = map.lookupForAdd(unregisterToken.get()); 462 if (!ptr.found() && 463 !map.add(ptr, unregisterToken, FinalizationRecordVector(cx->zone()))) { 464 ReportOutOfMemory(cx); 465 return false; 466 } 467 468 if (!ptr->value().append(record)) { 469 ReportOutOfMemory(cx); 470 return false; 471 } 472 473 return true; 474 } 475 476 /* static */ 477 void FinalizationRegistryObject::removeRegistrationOnError( 478 HandleFinalizationRegistryObject registry, HandleValue unregisterToken, 479 HandleFinalizationRecordObject record) { 480 // Remove a registration if something went wrong before we added it to the 481 // target zone's map. Note that this can't remove a registration after that 482 // point. 483 484 MOZ_ASSERT(registry->registrations()); 485 MOZ_ASSERT(unregisterToken.isUndefined() || CanBeHeldWeakly(unregisterToken)); 486 JS::AutoAssertNoGC nogc; 487 488 if (unregisterToken.isUndefined()) { 489 MOZ_ASSERT(registry->recordsWithoutToken()->back() == record); 490 registry->recordsWithoutToken()->popBack(); 491 return; 492 } 493 494 auto ptr = registry->registrations()->lookup(unregisterToken); 495 MOZ_ASSERT(ptr.found()); 496 FinalizationRecordVector& records = ptr->value(); 497 MOZ_ASSERT(records.back() == record); 498 records.popBack(); 499 if (records.empty()) { 500 registry->registrations()->remove(ptr); 501 } 502 } 503 504 // FinalizationRegistry.prototype.unregister ( unregisterToken ) 505 // https://tc39.es/proposal-weakrefs/#sec-finalization-registry.prototype.unregister 506 /* static */ 507 bool FinalizationRegistryObject::unregister(JSContext* cx, unsigned argc, 508 Value* vp) { 509 CallArgs args = CallArgsFromVp(argc, vp); 510 511 // 1. Let finalizationRegistry be the this value. 512 // 2. If Type(finalizationRegistry) is not Object, throw a TypeError 513 // exception. 514 // 3. If finalizationRegistry does not have a [[Cells]] internal slot, throw a 515 // TypeError exception. 516 if (!args.thisv().isObject() || 517 !args.thisv().toObject().is<FinalizationRegistryObject>()) { 518 JS_ReportErrorNumberASCII( 519 cx, GetErrorMessage, nullptr, JSMSG_NOT_A_FINALIZATION_REGISTRY, 520 "Receiver of FinalizationRegistry.unregister call"); 521 return false; 522 } 523 524 RootedFinalizationRegistryObject registry( 525 cx, &args.thisv().toObject().as<FinalizationRegistryObject>()); 526 527 // 4. If Type(unregisterToken) is not Object, throw a TypeError exception. 528 RootedValue unregisterToken(cx, args.get(0)); 529 if (!CanBeHeldWeakly(unregisterToken)) { 530 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 531 JSMSG_BAD_UNREGISTER_TOKEN, 532 "FinalizationRegistry.unregister"); 533 return false; 534 } 535 536 // 5. Let removed be false. 537 bool removed = false; 538 539 // 6. For each Record { [[Target]], [[HeldValue]], [[UnregisterToken]] } cell 540 // that is an element of finalizationRegistry.[[Cells]], do 541 // a. If SameValue(cell.[[UnregisterToken]], unregisterToken) is true, then 542 // i. Remove cell from finalizationRegistry.[[Cells]]. 543 // ii. Set removed to true. 544 545 RegistrationsMap* map = registry->registrations(); 546 auto ptr = map->lookup(unregisterToken); 547 if (ptr) { 548 FinalizationRecordVector& records = ptr->value(); 549 MOZ_ASSERT(!records.empty()); 550 for (FinalizationRecordObject* record : records) { 551 if (unregisterRecord(record)) { 552 removed = true; 553 } 554 } 555 map->remove(unregisterToken); 556 } 557 558 // 7. Return removed. 559 args.rval().setBoolean(removed); 560 return true; 561 } 562 563 /* static */ 564 bool FinalizationRegistryObject::unregisterRecord( 565 FinalizationRecordObject* record) { 566 if (!record->isRegistered()) { 567 MOZ_ASSERT(!record->isInList()); 568 return false; 569 } 570 571 // Remove record from the target list if present. 572 record->unlink(); 573 574 // Clear the fields of this record, marking it as unregistered. It will be 575 // removed from relevant data structures when they are next swept. 576 record->clear(); 577 MOZ_ASSERT(!record->isRegistered()); 578 579 return true; 580 } 581 582 // FinalizationRegistry.prototype.cleanupSome ( [ callback ] ) 583 // https://tc39.es/proposal-weakrefs/#sec-finalization-registry.prototype.cleanupSome 584 bool FinalizationRegistryObject::cleanupSome(JSContext* cx, unsigned argc, 585 Value* vp) { 586 CallArgs args = CallArgsFromVp(argc, vp); 587 588 // 1. Let finalizationRegistry be the this value. 589 // 2. Perform ? RequireInternalSlot(finalizationRegistry, [[Cells]]). 590 if (!args.thisv().isObject() || 591 !args.thisv().toObject().is<FinalizationRegistryObject>()) { 592 JS_ReportErrorNumberASCII( 593 cx, GetErrorMessage, nullptr, JSMSG_NOT_A_FINALIZATION_REGISTRY, 594 "Receiver of FinalizationRegistry.cleanupSome call"); 595 return false; 596 } 597 598 RootedFinalizationRegistryObject registry( 599 cx, &args.thisv().toObject().as<FinalizationRegistryObject>()); 600 601 // 3. If callback is not undefined and IsCallable(callback) is false, throw a 602 // TypeError exception. 603 RootedObject cleanupCallback(cx); 604 if (!args.get(0).isUndefined()) { 605 cleanupCallback = ValueToCallable(cx, args.get(0), -1, NO_CONSTRUCT); 606 if (!cleanupCallback) { 607 return false; 608 } 609 } 610 611 RootedFinalizationQueueObject queue(cx, registry->queue()); 612 if (!FinalizationQueueObject::cleanupQueuedRecords(cx, queue, 613 cleanupCallback)) { 614 return false; 615 } 616 617 args.rval().setUndefined(); 618 return true; 619 } 620 621 /////////////////////////////////////////////////////////////////////////// 622 // FinalizationQueueObject 623 624 // Bug 1600300: FinalizationQueueObject is foreground finalized so that 625 // HeapPtr destructors never see referents with released arenas. When this is 626 // fixed we may be able to make this background finalized again. 627 const JSClass FinalizationQueueObject::class_ = { 628 "FinalizationQueue", 629 JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_FOREGROUND_FINALIZE, 630 &classOps_, 631 }; 632 633 const JSClassOps FinalizationQueueObject::classOps_ = { 634 nullptr, // addProperty 635 nullptr, // delProperty 636 nullptr, // enumerate 637 nullptr, // newEnumerate 638 nullptr, // resolve 639 nullptr, // mayResolve 640 FinalizationQueueObject::finalize, // finalize 641 nullptr, // call 642 nullptr, // construct 643 FinalizationQueueObject::trace, // trace 644 }; 645 646 /* static */ 647 FinalizationQueueObject* FinalizationQueueObject::create( 648 JSContext* cx, HandleObject cleanupCallback) { 649 MOZ_ASSERT(cleanupCallback); 650 651 Rooted<UniquePtr<FinalizationRecordVector>> recordsToBeCleanedUp( 652 cx, cx->make_unique<FinalizationRecordVector>(cx->zone())); 653 if (!recordsToBeCleanedUp) { 654 return nullptr; 655 } 656 657 Handle<PropertyName*> funName = cx->names().empty_; 658 RootedFunction doCleanupFunction( 659 cx, NewNativeFunction(cx, doCleanup, 0, funName, 660 gc::AllocKind::FUNCTION_EXTENDED)); 661 if (!doCleanupFunction) { 662 return nullptr; 663 } 664 665 // It's problematic storing a CCW to a global in another compartment because 666 // you don't know how far to unwrap it to get the original object 667 // back. Instead store a CCW to a plain object in the same compartment as the 668 // global (this uses Object.prototype). 669 Rooted<JSObject*> hostDefinedData(cx); 670 if (!GetObjectFromHostDefinedData(cx, &hostDefinedData)) { 671 return nullptr; 672 } 673 674 FinalizationQueueObject* queue = 675 NewObjectWithGivenProto<FinalizationQueueObject>(cx, nullptr); 676 if (!queue) { 677 return nullptr; 678 } 679 680 queue->initReservedSlot(CleanupCallbackSlot, ObjectValue(*cleanupCallback)); 681 queue->initReservedSlot(HostDefinedDataSlot, 682 JS::ObjectOrNullValue(hostDefinedData)); 683 InitReservedSlot(queue, RecordsToBeCleanedUpSlot, 684 recordsToBeCleanedUp.release(), 685 MemoryUse::FinalizationRegistryRecordVector); 686 queue->initReservedSlot(IsQueuedForCleanupSlot, BooleanValue(false)); 687 queue->initReservedSlot(DoCleanupFunctionSlot, 688 ObjectValue(*doCleanupFunction)); 689 queue->initReservedSlot(HasRegistrySlot, BooleanValue(false)); 690 691 doCleanupFunction->setExtendedSlot(DoCleanupFunction_QueueSlot, 692 ObjectValue(*queue)); 693 694 return queue; 695 } 696 697 /* static */ 698 void FinalizationQueueObject::trace(JSTracer* trc, JSObject* obj) { 699 auto queue = &obj->as<FinalizationQueueObject>(); 700 701 if (FinalizationRecordVector* records = queue->recordsToBeCleanedUp()) { 702 records->trace(trc); 703 } 704 } 705 706 /* static */ 707 void FinalizationQueueObject::finalize(JS::GCContext* gcx, JSObject* obj) { 708 auto* queue = &obj->as<FinalizationQueueObject>(); 709 gcx->delete_(obj, queue->recordsToBeCleanedUp(), 710 MemoryUse::FinalizationRegistryRecordVector); 711 } 712 713 void FinalizationQueueObject::setHasRegistry(bool newValue) { 714 MOZ_ASSERT(hasRegistry() != newValue); 715 716 // Suppress our assertions about touching grey things. It's OK for us to set a 717 // boolean slot even if this object is gray. 718 AutoTouchingGrayThings atgt; 719 720 setReservedSlot(HasRegistrySlot, BooleanValue(newValue)); 721 } 722 723 bool FinalizationQueueObject::hasRegistry() const { 724 return getReservedSlot(HasRegistrySlot).toBoolean(); 725 } 726 727 inline JSObject* FinalizationQueueObject::cleanupCallback() const { 728 Value value = getReservedSlot(CleanupCallbackSlot); 729 if (value.isUndefined()) { 730 return nullptr; 731 } 732 return &value.toObject(); 733 } 734 735 JSObject* FinalizationQueueObject::getHostDefinedData() const { 736 Value value = getReservedSlot(HostDefinedDataSlot); 737 if (value.isUndefined()) { 738 return nullptr; 739 } 740 return value.toObjectOrNull(); 741 } 742 743 bool FinalizationQueueObject::hasRecordsToCleanUp() const { 744 FinalizationRecordVector* records = recordsToBeCleanedUp(); 745 return records && !records->empty(); 746 } 747 748 FinalizationRecordVector* FinalizationQueueObject::recordsToBeCleanedUp() 749 const { 750 Value value = getReservedSlot(RecordsToBeCleanedUpSlot); 751 if (value.isUndefined()) { 752 return nullptr; 753 } 754 return static_cast<FinalizationRecordVector*>(value.toPrivate()); 755 } 756 757 bool FinalizationQueueObject::isQueuedForCleanup() const { 758 return getReservedSlot(IsQueuedForCleanupSlot).toBoolean(); 759 } 760 761 JSFunction* FinalizationQueueObject::doCleanupFunction() const { 762 Value value = getReservedSlot(DoCleanupFunctionSlot); 763 if (value.isUndefined()) { 764 return nullptr; 765 } 766 return &value.toObject().as<JSFunction>(); 767 } 768 769 void FinalizationQueueObject::queueRecordToBeCleanedUp( 770 FinalizationRecordObject* record) { 771 MOZ_ASSERT(hasRegistry()); 772 773 MOZ_ASSERT(!record->isInQueue()); 774 record->setInQueue(true); 775 776 AutoEnterOOMUnsafeRegion oomUnsafe; 777 if (!recordsToBeCleanedUp()->append(record)) { 778 oomUnsafe.crash("FinalizationQueueObject::queueRecordsToBeCleanedUp"); 779 } 780 } 781 782 void FinalizationQueueObject::setQueuedForCleanup(bool value) { 783 MOZ_ASSERT(value != isQueuedForCleanup()); 784 setReservedSlot(IsQueuedForCleanupSlot, BooleanValue(value)); 785 } 786 787 /* static */ 788 bool FinalizationQueueObject::doCleanup(JSContext* cx, unsigned argc, 789 Value* vp) { 790 CallArgs args = CallArgsFromVp(argc, vp); 791 792 RootedFunction callee(cx, &args.callee().as<JSFunction>()); 793 794 Value value = callee->getExtendedSlot(DoCleanupFunction_QueueSlot); 795 RootedFinalizationQueueObject queue( 796 cx, &value.toObject().as<FinalizationQueueObject>()); 797 798 queue->setQueuedForCleanup(false); 799 return cleanupQueuedRecords(cx, queue); 800 } 801 802 // CleanupFinalizationRegistry ( finalizationRegistry [ , callback ] ) 803 // https://tc39.es/proposal-weakrefs/#sec-cleanup-finalization-registry 804 /* static */ 805 bool FinalizationQueueObject::cleanupQueuedRecords( 806 JSContext* cx, HandleFinalizationQueueObject queue, 807 HandleObject callbackArg) { 808 MOZ_ASSERT(cx->compartment() == queue->compartment()); 809 810 // 2. If callback is undefined, set callback to 811 // finalizationRegistry.[[CleanupCallback]]. 812 RootedValue callback(cx); 813 if (callbackArg) { 814 callback.setObject(*callbackArg); 815 } else { 816 JSObject* cleanupCallback = queue->cleanupCallback(); 817 MOZ_ASSERT(cleanupCallback); 818 callback.setObject(*cleanupCallback); 819 } 820 821 // 3. While finalizationRegistry.[[Cells]] contains a Record cell such that 822 // cell.[[WeakRefTarget]] is empty, then an implementation may perform the 823 // following steps, 824 // a. Choose any such cell. 825 // b. Remove cell from finalizationRegistry.[[Cells]]. 826 // c. Perform ? Call(callback, undefined, « cell.[[HeldValue]] »). 827 828 RootedValue heldValue(cx); 829 RootedValue rval(cx); 830 FinalizationRecordVector* records = queue->recordsToBeCleanedUp(); 831 while (!records->empty()) { 832 FinalizationRecordObject* record = records->popCopy(); 833 MOZ_ASSERT(!record->isInRecordMap()); 834 835 JS::ExposeObjectToActiveJS(record); 836 837 MOZ_ASSERT(record->isInQueue()); 838 record->setInQueue(false); 839 840 // Skip over records that have been unregistered. 841 if (!record->isRegistered()) { 842 continue; 843 } 844 845 heldValue.set(record->heldValue()); 846 847 record->clear(); 848 849 if (!Call(cx, callback, UndefinedHandleValue, heldValue, &rval)) { 850 return false; 851 } 852 } 853 854 return true; 855 }