File
Blob: src/workerd/jsg/wrappable.c++
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #include "wrappable.h" |
| 6 | |
| 7 | #include "jsg.h" |
| 8 | #include "setup.h" |
| 9 | |
| 10 | #include <workerd/util/thread-scopes.h> |
| 11 | |
| 12 | #include <cppgc/allocation.h> |
| 13 | #include <cppgc/garbage-collected.h> |
| 14 | #include <v8-cppgc.h> |
| 15 | |
| 16 | #include <kj/async.h> |
| 17 | #include <kj/debug.h> |
| 18 | #if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__) |
| 19 | #include <sanitizer/asan_interface.h> |
| 20 | #endif |
| 21 | |
| 22 | namespace workerd::jsg { |
| 23 | |
| 24 | namespace { |
| 25 | |
| 26 | static thread_local bool inCppgcShimDestructor = false; |
| 27 | |
| 28 | }; |
| 29 | |
| 30 | bool HeapTracer::isInCppgcDestructor() { |
| 31 | return inCppgcShimDestructor; |
| 32 | } |
| 33 | |
| 34 | void HeapTracer::clearWrappers() { |
| 35 | // When clearing wrappers (at isolate shutdown), we may be destroying objects that were recently |
| 36 | // determined to be unreachable, but the CppgcShim destructors haven't been run yet. We need to |
| 37 | // treat this case as if we are running CppgcShim destructors, that is, assume any |
| 38 | // TracedReferences we destroy have already been collected so cannot be touched. |
| 39 | // TODO(cleanup): Rename `inCppgcShimDestructor` to `possiblyCollectingUnreachableObject`? |
| 40 | KJ_ASSERT(!inCppgcShimDestructor); |
| 41 | inCppgcShimDestructor = true; |
| 42 | KJ_DEFER(inCppgcShimDestructor = false); |
| 43 | |
| 44 | while (!wrappers.empty()) { |
| 45 | // Don't freelist the shim because we're shutting down anyway. |
| 46 | auto& wrappable = wrappers.front(); |
| 47 | auto ownWrappable = wrappable.detachWrapper(false); |
| 48 | // Clear the isolate pointer so that any code that later tries to use it (e.g., |
| 49 | // maybeDeferDestruction() or GcVisitor) will see that the isolate is gone and skip |
| 50 | // V8 operations. Without this, objects that outlive their isolate (like WebSockets |
| 51 | // stored in a HibernationManager) would have a dangling isolate pointer and crash |
| 52 | // when trying to check v8::Locker::IsLocked() or create V8 handles. |
| 53 | ownWrappable->isolate = nullptr; |
| 54 | } |
| 55 | clearFreelistedShims(); |
| 56 | } |
| 57 | |
| 58 | using JSGWrappable = workerd::jsg::Wrappable; |
| 59 | |
| 60 | // V8's GC integrates with cppgc, aka "oilpan", a garbage collector for C++ objects. We want to |
| 61 | // integrate with the GC in order to receive GC visitation callbacks, so that the GC is able to |
| 62 | // trace through our C++ objects to find what is reachable through them. The only way for us to |
| 63 | // support this is by integrating with cppgc. |
| 64 | // |
| 65 | // However, workerd was written using KJ idioms long before cppgc existed. Rewriting all our code |
| 66 | // to use cppgc allocation instead would be a highly invasive change. Maybe we'll do it someday, |
| 67 | // but today is not the day. So, our API objects continue to be allocated on the regular (non-GC) |
| 68 | // C++ heap. |
| 69 | // |
| 70 | // CppgcShim provides a compromise. For each API object that has been wrapped for use from JS, |
| 71 | // we create a CppgcShim object on the cppgc heap. This basically just contains a pointer to the |
| 72 | // regular old C++ object. This lets us get our GC visitation without fully integrating with |
| 73 | // cppgc. |
| 74 | // |
| 75 | // There is an additional trick here: As of this writing, cppgc objects cannot be collected |
| 76 | // during V8's minor GC passes ("scavenge" passes). Only full GCs ("trace" passes) can collect |
| 77 | // them. But we do want our API objects to be collectable during minor GC. We integrate with V8's |
| 78 | // EmbedderRootsHandler to get notification when these objects can be collected. But when they |
| 79 | // are, what happens to the CppgcShim object we allocated? We can't force it to be collected |
| 80 | // early. We could just discard it and let it be collected during the next major GC, but that |
| 81 | // would mean accumulating a lot of garbage shims. Instead, we freelist the objects: when a |
| 82 | // wrapper is collected during minor GC, the CppgcShim is placed in a freelist and can be |
| 83 | // reused for a future allocation, if that allocation occurs before the next major GC. When a |
| 84 | // major GC occurs, the freelist is cleared, since any unreachable CppgcShim objects are likely |
| 85 | // condemned after that point and will be deleted shortly thereafter. |
| 86 | class Wrappable::CppgcShim final: public v8::Object::Wrappable { |
| 87 | public: |
| 88 | CppgcShim(JSGWrappable& wrappable): state(Active{kj::addRef(wrappable)}) { |
| 89 | KJ_DASSERT(wrappable.cppgcShim == kj::none); |
| 90 | wrappable.cppgcShim = *this; |
| 91 | } |
| 92 | |
| 93 | ~CppgcShim() { |
| 94 | // (Unlike most KJ destructors, we don't mark this noexcept(false) because it's called from |
| 95 | // V8 which doesn't support exceptions.) |
| 96 | |
| 97 | KJ_DASSERT(!inCppgcShimDestructor); |
| 98 | inCppgcShimDestructor = true; |
| 99 | KJ_DEFER(inCppgcShimDestructor = false); |
| 100 | |
| 101 | KJ_SWITCH_ONEOF(state) { |
| 102 | KJ_CASE_ONEOF(active, Active) { |
| 103 | KJ_DASSERT(&KJ_ASSERT_NONNULL(active.wrappable->cppgcShim) == this); |
| 104 | KJ_DASSERT(active.wrappable->strongWrapper.IsEmpty()); |
| 105 | active.wrappable->detachWrapper(false); |
| 106 | } |
| 107 | KJ_CASE_ONEOF(freelisted, Freelisted) { |
| 108 | KJ_DASSERT(&KJ_ASSERT_NONNULL(*freelisted.prev) == this); |
| 109 | *freelisted.prev = freelisted.next; |
| 110 | KJ_IF_SOME(next, freelisted.next) { |
| 111 | KJ_DASSERT(next.state.get<Freelisted>().prev == &freelisted.next); |
| 112 | next.state.get<Freelisted>().prev = freelisted.prev; |
| 113 | } |
| 114 | } |
| 115 | KJ_CASE_ONEOF(d, Dead) {} |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | void Trace(cppgc::Visitor* visitor) const override { |
| 120 | KJ_SWITCH_ONEOF(state) { |
| 121 | KJ_CASE_ONEOF(active, Active) { |
| 122 | active.wrappable->traceFromV8(*visitor); |
| 123 | } |
| 124 | KJ_CASE_ONEOF(freelisted, Freelisted) { |
| 125 | // We're tracing a shim for an object that was collected in minor GC. This could happen |
| 126 | // due to conservative GC or due to incremental marking. Unfortunately the shim won't be |
| 127 | // collected on this pass but hopefully it can be on the next pass. |
| 128 | } |
| 129 | KJ_CASE_ONEOF(d, Dead) {} |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | const char* GetHumanReadableName() const override { |
| 134 | return "CppgcShim"; |
| 135 | } |
| 136 | |
| 137 | struct Active { |
| 138 | kj::Own<JSGWrappable> wrappable; |
| 139 | }; |
| 140 | |
| 141 | // The JavaScript wrapper using this shim was collected in a minor GC. cppgc objects can only |
| 142 | // be collected in full GC, so we freelist the shim object in the meantime. |
| 143 | struct Freelisted { |
| 144 | kj::Maybe<JSGWrappable::CppgcShim&> next; |
| 145 | kj::Maybe<JSGWrappable::CppgcShim&>* prev; |
| 146 | // kj::List doesn't quite work here because the list link is inside a OneOf. Also we want a |
| 147 | // LIFO list anyway so we don't need a tail pointer, which makes things easier. So we do it |
| 148 | // manually. |
| 149 | }; |
| 150 | struct Dead {}; |
| 151 | |
| 152 | kj::StringPtr jsgGetMemoryName() const { |
| 153 | return "CppgcShim"_kjc; |
| 154 | } |
| 155 | size_t jsgGetMemorySelfSize() const { |
| 156 | return sizeof(CppgcShim); |
| 157 | } |
| 158 | void jsgGetMemoryInfo(MemoryTracker& tracker) const { |
| 159 | KJ_IF_SOME(active, state.tryGet<Active>()) { |
| 160 | tracker.trackField("wrappable", active.wrappable); |
| 161 | } |
| 162 | } |
| 163 | bool jsgGetMemoryInfoIsRootNode() const { |
| 164 | return false; |
| 165 | } |
| 166 | |
| 167 | mutable kj::OneOf<Active, Freelisted, Dead> state; |
| 168 | // This is `mutable` because `Trace()` is const. We configure V8 to perform traces atomically in |
| 169 | // the main thread so concurrency is not a concern. |
| 170 | }; |
| 171 | |
| 172 | void HeapTracer::addToFreelist(JSGWrappable::CppgcShim& shim) { |
| 173 | auto& freelisted = shim.state.init<JSGWrappable::CppgcShim::Freelisted>(); |
| 174 | freelisted.next = freelistedShims; |
| 175 | KJ_IF_SOME(next, freelisted.next) { |
| 176 | next.state.get<JSGWrappable::CppgcShim::Freelisted>().prev = &freelisted.next; |
| 177 | } |
| 178 | freelisted.prev = &freelistedShims; |
| 179 | freelistedShims = shim; |
| 180 | } |
| 181 | |
| 182 | JSGWrappable::CppgcShim* HeapTracer::allocateShim(JSGWrappable& wrappable) { |
| 183 | // Under gc-stress, skip freelist reuse so each shim has a unique address in logs. |
| 184 | if (!isGcStressModeForTest()) { |
| 185 | KJ_IF_SOME(shim, freelistedShims) { |
| 186 | freelistedShims = shim.state.get<JSGWrappable::CppgcShim::Freelisted>().next; |
| 187 | KJ_IF_SOME(next, freelistedShims) { |
| 188 | next.state.get<JSGWrappable::CppgcShim::Freelisted>().prev = &freelistedShims; |
| 189 | } |
| 190 | shim.state = JSGWrappable::CppgcShim::Active{kj::addRef(wrappable)}; |
| 191 | KJ_DASSERT(wrappable.cppgcShim == kj::none); |
| 192 | wrappable.cppgcShim = shim; |
| 193 | return &shim; |
| 194 | } |
| 195 | } |
| 196 | auto& cppgcAllocHandle = isolate->GetCppHeap()->GetAllocationHandle(); |
| 197 | auto* shim = cppgc::MakeGarbageCollected<JSGWrappable::CppgcShim>(cppgcAllocHandle, wrappable); |
| 198 | return shim; |
| 199 | } |
| 200 | |
| 201 | void HeapTracer::clearFreelistedShims() { |
| 202 | for (;;) { |
| 203 | KJ_IF_SOME(shim, freelistedShims) { |
| 204 | freelistedShims = shim.state.get<JSGWrappable::CppgcShim::Freelisted>().next; |
| 205 | shim.state = JSGWrappable::CppgcShim::Dead{}; |
| 206 | } else { |
| 207 | break; |
| 208 | } |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | void HeapTracer::jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const { |
| 213 | for (const auto& wrapper: wrappers) { |
| 214 | tracker.trackField("wrapper", wrapper); |
| 215 | } |
| 216 | // TODO(soon): Track the other fields here? |
| 217 | } |
| 218 | |
| 219 | kj::Own<JSGWrappable> JSGWrappable::detachWrapper(bool shouldFreelistShim) { |
| 220 | KJ_IF_SOME(shim, cppgcShim) { |
| 221 | #if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__) |
| 222 | // There's a possibility that the CppgcShim has already been found to be unreachable by a GC |
| 223 | // pass, but has not actually been destroyed yet. For some reason, cppgc likes to delay the |
| 224 | // calling of actual destructors. However, in ASAN builds, cppgc will poison the memory in the |
| 225 | // meantime, because it figures that we "shouldn't" be accessing unreachable memory. This |
| 226 | // assumption makes sense in the abstract, but not for our specific use case, where we are |
| 227 | // essentially maintaining a weak pointer to the CppgcShim. If the destructor had been called, |
| 228 | // then `cppgcShim` here would have been nulled out at that time. We're expecting that until |
| 229 | // the destructor is called, we can still safely access the object to detach the wrapper. |
| 230 | // |
| 231 | // So to work around cppgc's incorrect assumption, we manually unpoison the memory. |
| 232 | // |
| 233 | // Note: An alternative strategy could have been for CppgcShim itself to allocate a separate |
| 234 | // C++ heap object to store its own state in, so that that state could be modified even while |
| 235 | // the CppgcShim object itself is poisoned. In this case `Wrappable::cppgcShim` would change to |
| 236 | // point at this state object, not to the `CppgcShim` itself. However, this approach would |
| 237 | // require extra heap allocation for everyone, just to satisfy ASAN, which seems undesirable. |
| 238 | ASAN_UNPOISON_MEMORY_REGION(&shim, sizeof(shim)); |
| 239 | #endif |
| 240 | |
| 241 | auto& tracer = HeapTracer::getTracer(isolate); |
| 242 | auto result = |
| 243 | kj::mv(KJ_ASSERT_NONNULL(shim.state.tryGet<JSGWrappable::CppgcShim::Active>()).wrappable); |
| 244 | if (shouldFreelistShim) { |
| 245 | tracer.addToFreelist(shim); |
| 246 | } else { |
| 247 | shim.state = JSGWrappable::CppgcShim::Dead{}; |
| 248 | } |
| 249 | wrapper = kj::none; |
| 250 | cppgcShim = kj::none; |
| 251 | strongWrapper.Reset(); |
| 252 | tracer.removeWrapper({}, *this); |
| 253 | if (strongRefcount > 0) { |
| 254 | // Need to visit child references in order to convert them to strong references, since we |
| 255 | // no longer have an intervening wrapper. |
| 256 | GcVisitor visitor(*this, kj::none); |
| 257 | jsgVisitForGc(visitor); |
| 258 | } |
| 259 | return result; |
| 260 | } else { |
| 261 | return {}; |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | v8::Local<v8::Object> Wrappable::getHandle(v8::Isolate* isolate) { |
| 266 | return KJ_REQUIRE_NONNULL(tryGetHandle(isolate)); |
| 267 | } |
| 268 | |
| 269 | void Wrappable::addStrongRef() { |
| 270 | // The `isolate == nullptr` check here ensures that `js.alloc<T>()` can be used with no |
| 271 | // isolate, simply allocating the object as a normal C++ heap object. |
| 272 | KJ_DREQUIRE(isolate == nullptr || v8::Isolate::TryGetCurrent() != nullptr, |
| 273 | "referencing wrapper without isolate lock"); |
| 274 | if (strongRefcount++ == 0) { |
| 275 | // This object previously had no strong references, but now it has one. |
| 276 | KJ_IF_SOME(w, wrapper) { |
| 277 | // Copy the traced reference into the strong reference. |
| 278 | v8::HandleScope scope(isolate); |
| 279 | strongWrapper.Reset(isolate, w.Get(isolate)); |
| 280 | } else { |
| 281 | // Since we have no JS wrapper, we're forced to recursively mark all references reachable |
| 282 | // through this wrapper as strong. |
| 283 | GcVisitor visitor(*this, kj::none); |
| 284 | jsgVisitForGc(visitor); |
| 285 | } |
| 286 | } |
| 287 | } |
| 288 | void Wrappable::removeStrongRef() { |
| 289 | KJ_DREQUIRE(isolate == nullptr || v8::Isolate::TryGetCurrent() == isolate, |
| 290 | "destroying wrapper without isolate lock"); |
| 291 | if (--strongRefcount == 0) { |
| 292 | // This was the last strong reference. |
| 293 | if (wrapper == kj::none) { |
| 294 | // We have no wrapper. We need to mark all references held by this object as weak. |
| 295 | if (isolate != nullptr) { |
| 296 | // But only if the current isolate isn't null. If strong ref count is zero, |
| 297 | // the wrapper is empty, and isolate is null, then the child handles it has will |
| 298 | // be released anyway (since we're about to be destroyed), thus this visitation |
| 299 | // isn't required (and may be buggy, since it may happen outside the isolate lock). |
| 300 | GcVisitor visitor(*this, kj::none); |
| 301 | jsgVisitForGc(visitor); |
| 302 | } |
| 303 | } else { |
| 304 | // Just clear the strong ref. |
| 305 | strongWrapper.Reset(); |
| 306 | } |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | void Wrappable::maybeDeferDestruction(bool strong, kj::Own<void> ownSelf, Wrappable* self) { |
| 311 | DISALLOW_KJ_IO_DESTRUCTORS_SCOPE; |
| 312 | |
| 313 | auto item = IsolateBase::RefToDelete(strong, kj::mv(ownSelf), self); |
| 314 | |
| 315 | if (isolate == nullptr || v8::Locker::IsLocked(isolate)) { |
| 316 | // If we never attached a wrapper and were never traced, or the isolate is already locked, then |
| 317 | // we can just destroy the Wrappable immediately. |
| 318 | auto drop = kj::mv(item); |
| 319 | } else { |
| 320 | // Otherwise, we have a wrapper and we don't have the isolate locked. |
| 321 | auto& jsgIsolate = *reinterpret_cast<IsolateBase*>(isolate->GetData(SET_DATA_ISOLATE_BASE)); |
| 322 | jsgIsolate.deferDestruction(kj::mv(item)); |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | void Wrappable::traceFromV8(cppgc::Visitor& cppgcVisitor) { |
| 327 | cppgcVisitor.Trace(KJ_ASSERT_NONNULL(wrapper)); |
| 328 | GcVisitor visitor(*this, cppgcVisitor); |
| 329 | jsgVisitForGc(visitor); |
| 330 | } |
| 331 | |
| 332 | void Wrappable::attachWrapper( |
| 333 | v8::Isolate* isolate, v8::Local<v8::Object> object, bool needsGcTracing) { |
| 334 | auto& tracer = HeapTracer::getTracer(isolate); |
| 335 | |
| 336 | KJ_REQUIRE(wrapper == kj::none); |
| 337 | KJ_REQUIRE(strongWrapper.IsEmpty()); |
| 338 | |
| 339 | // The C++ Wrappable object must hold a TracedReference to its own JavaScript wrapper, while |
| 340 | // such a wrapper exists. This way, if the object is reached through C++ again later, we can |
| 341 | // return the same object to JavaScript. |
| 342 | // |
| 343 | // This reference is special: it is marked as "droppable". This tells V8 that we know how to |
| 344 | // recreate this wrapper on-demand (from the C++ object). This is an optimization: If the |
| 345 | // application drops all of its direct references to the wrapper, such that object is only |
| 346 | // reachable implicitly through C++ objects, then V8 can drop the wrapper entirely and have us |
| 347 | // recreate it later, when JS needs it again. |
| 348 | // |
| 349 | // For example, consider a Request object that contains a Headers object. Say the application |
| 350 | // accesses the Headers briefly, like `request.headers.get("foo")` -- it doesn't keep around a |
| 351 | // direct reference to the Headers. But it DOES keep around a reference to the Request, and the |
| 352 | // C++ API object backing the Request keeps a `jsg::Ref<Headers>`. In this case, we do not really |
| 353 | // need the JavaScript wrapper for `Headers` to stick around. We know we can create a new one if |
| 354 | // and when it is needed. So we tell V8 that our internal reference is "droppable", so that it |
| 355 | // will go ahead and drop it in this scenario. (Specifically, v8 calls |
| 356 | // `EmbedderRootsHandler::ResetRoot()`, which is implemented by our `HeapTracer`, to tell us that |
| 357 | // it is dropping the wrapper.) |
| 358 | // |
| 359 | // Note that there are things that the application might do which actually make it unsafe for us |
| 360 | // to drop and recreate the wrapper. For example, the application could add a property to the |
| 361 | // wrapper object itself, like `request.headers.foo = 123`. Later on, when the app accesses |
| 362 | // `request.headers.foo` again, it expects the property will still be there. But if we dropped |
| 363 | // our wrapper and recreated it, the property would be gone. Luckily, V8 already handles this |
| 364 | // for us! V8 knows not to drop our wrapper if the application has done anything with it such |
| 365 | // that a recreated wrapper would no longer be equivalent. |
| 366 | wrapper.emplace(isolate, object, v8::TracedReference<v8::Object>::IsDroppable()); |
| 367 | this->isolate = isolate; |
| 368 | |
| 369 | // Add to list of objects to force-clean at isolate shutdown. |
| 370 | tracer.addWrapper({}, *this); |
| 371 | |
| 372 | // Set up internal fields for a newly-allocated object. |
| 373 | KJ_REQUIRE(object->InternalFieldCount() == Wrappable::INTERNAL_FIELD_COUNT); |
| 374 | // The third argument is the type tag, a small integer that should be |
| 375 | // different for every pointer type to avoid type confusion attacks. We just |
| 376 | // use the slot index for now, since we have a different pointer type for |
| 377 | // each slot. |
| 378 | auto tagAddress = const_cast<uint16_t*>(&WORKERD_WRAPPABLE_TAG); |
| 379 | object->SetAlignedPointerInInternalField(WRAPPABLE_TAG_FIELD_INDEX, tagAddress, |
| 380 | static_cast<v8::EmbedderDataTypeTag>(WRAPPABLE_TAG_FIELD_INDEX)); |
| 381 | object->SetAlignedPointerInInternalField(WRAPPED_OBJECT_FIELD_INDEX, this, |
| 382 | static_cast<v8::EmbedderDataTypeTag>(WRAPPED_OBJECT_FIELD_INDEX)); |
| 383 | |
| 384 | v8::Object::Wrap<WRAPPABLE_TAG>(isolate, object, tracer.allocateShim(*this)); |
| 385 | |
| 386 | if (strongRefcount > 0) { |
| 387 | strongWrapper.Reset(isolate, object); |
| 388 | |
| 389 | // This object has untraced references, but didn't have a wrapper. That means that any refs |
| 390 | // transitively reachable through the reference are strong. Now that a wrapper exists, the |
| 391 | // refs will be traced when the wrapper is traced, so they should be converted to traced |
| 392 | // references. Performing a visitation pass will update them. |
| 393 | GcVisitor visitor(*this, kj::none); |
| 394 | jsgVisitForGc(visitor); |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | void Wrappable::jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const { |
| 399 | tracker.trackField("cppgcshim", cppgcShim); |
| 400 | } |
| 401 | |
| 402 | v8::Local<v8::Object> Wrappable::attachOpaqueWrapper( |
| 403 | v8::Local<v8::Context> context, bool needsGcTracing) { |
| 404 | auto isolate = v8::Isolate::GetCurrent(); |
| 405 | auto object = |
| 406 | jsg::check(IsolateBase::getOpaqueTemplate(isolate)->InstanceTemplate()->NewInstance(context)); |
| 407 | attachWrapper(isolate, object, needsGcTracing); |
| 408 | return object; |
| 409 | } |
| 410 | |
| 411 | kj::Maybe<Wrappable&> Wrappable::tryUnwrapOpaque( |
| 412 | v8::Isolate* isolate, v8::Local<v8::Value> handle) { |
| 413 | if (handle->IsObject()) { |
| 414 | v8::Local<v8::Object> instance = |
| 415 | v8::Local<v8::Object>::Cast(handle)->FindInstanceInPrototypeChain( |
| 416 | IsolateBase::getOpaqueTemplate(isolate)); |
| 417 | if (!instance.IsEmpty()) { |
| 418 | return *reinterpret_cast<Wrappable*>( |
| 419 | instance->GetAlignedPointerFromInternalField(WRAPPED_OBJECT_FIELD_INDEX, |
| 420 | static_cast<v8::EmbedderDataTypeTag>(WRAPPED_OBJECT_FIELD_INDEX))); |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | return kj::none; |
| 425 | } |
| 426 | |
| 427 | void Wrappable::jsgVisitForGc(GcVisitor& visitor) { |
| 428 | // Nothing; subclasses that need tracing will override. |
| 429 | } |
| 430 | |
| 431 | void Wrappable::visitRef(GcVisitor& visitor, kj::Maybe<Wrappable&>& refParent, bool& refStrong) { |
| 432 | KJ_IF_SOME(p, refParent) { |
| 433 | KJ_ASSERT(&p == &visitor.parent); |
| 434 | } else { |
| 435 | refParent = visitor.parent; |
| 436 | } |
| 437 | |
| 438 | if (isolate == nullptr) { |
| 439 | isolate = visitor.parent.isolate; |
| 440 | } |
| 441 | |
| 442 | // Make ref strength match the parent. |
| 443 | if (visitor.parent.strongRefcount > 0 && visitor.parent.wrapper == kj::none) { |
| 444 | // This reference should be strong, because the parent has strong refs and does not have its |
| 445 | // own wrapper that will be traced. |
| 446 | |
| 447 | if (!refStrong) { |
| 448 | // Ref transitions from weak to strong. |
| 449 | // |
| 450 | // This should never happen during a GC pass, since we should only be visiting traced |
| 451 | // references then. |
| 452 | KJ_ASSERT(visitor.cppgcVisitor == kj::none); |
| 453 | addStrongRef(); |
| 454 | refStrong = true; |
| 455 | } |
| 456 | } else { |
| 457 | if (refStrong) { |
| 458 | // Ref transitions from strong to weak. |
| 459 | // |
| 460 | // Note that a Ref can become weak here as part of a GC pass. Specifically, the Ref might |
| 461 | // have previously been added to an object that already had a JS wrapper before the Ref was |
| 462 | // added. In this case, we won't detect that the Ref is traced until the next GC pass reaches |
| 463 | // it. |
| 464 | refStrong = false; |
| 465 | removeStrongRef(); |
| 466 | } |
| 467 | } |
| 468 | |
| 469 | KJ_IF_SOME(cgv, visitor.cppgcVisitor) { |
| 470 | // We're visiting for the purpose of a GC trace. |
| 471 | KJ_IF_SOME(w, wrapper) { |
| 472 | cgv.Trace(w); |
| 473 | } else { |
| 474 | // This object doesn't currently have a wrapper, so traces must transitively trace through |
| 475 | // it. However, as an optimization, we can skip the trace if we've already been traced in |
| 476 | // this trace pass. |
| 477 | GcVisitor subVisitor(*this, visitor.cppgcVisitor); |
| 478 | jsgVisitForGc(subVisitor); |
| 479 | } |
| 480 | } |
| 481 | } |
| 482 | |
| 483 | void GcVisitor::visit(Data& value) { |
| 484 | if (!value.handle.IsEmpty()) { |
| 485 | // Make ref strength match the parent. |
| 486 | if (parent.strongRefcount > 0 && parent.wrapper == kj::none) { |
| 487 | // This is directly reachable by a strong ref, so mark the handle strong. |
| 488 | if (value.tracedHandle != kj::none) { |
| 489 | // Convert the handle back to strong and discard the traced reference. |
| 490 | value.handle.ClearWeak<void>(); |
| 491 | value.tracedHandle = kj::none; |
| 492 | } |
| 493 | } else { |
| 494 | // This is only reachable via traced objects, so the handle should be weak, and we should |
| 495 | // hold a TracedReference alongside it. |
| 496 | if (value.tracedHandle == kj::none) { |
| 497 | // Create the TracedReference. |
| 498 | v8::HandleScope scope(parent.isolate); |
| 499 | value.tracedHandle = |
| 500 | v8::TracedReference<v8::Data>(parent.isolate, value.handle.Get(parent.isolate)); |
| 501 | |
| 502 | // Set the handle weak. |
| 503 | value.handle.SetWeak(); |
| 504 | } |
| 505 | } |
| 506 | |
| 507 | KJ_IF_SOME(c, cppgcVisitor) { |
| 508 | KJ_IF_SOME(t, value.tracedHandle) { |
| 509 | c.Trace(t); |
| 510 | } |
| 511 | } |
| 512 | } |
| 513 | } |
| 514 | |
| 515 | void GcVisitor::visit(v8::Global<v8::Value>& strong, v8::TracedReference<v8::Data>& traced) { |
| 516 | if (strong.IsEmpty()) { |
| 517 | return; |
| 518 | } |
| 519 | |
| 520 | // Mirror visit(Data&): make handle strength match the parent. |
| 521 | // |
| 522 | // The `parent.wrapper == kj::none` check mirrors the same condition in |
| 523 | // visit(Data&): even when strongRefcount > 0, if the JS wrapper already |
| 524 | // exists we must keep the handle in traced mode so cppgc can follow edges |
| 525 | // from it. Only when there is no wrapper yet (object not yet exported to JS) |
| 526 | // does a positive strongRefcount alone justify keeping the handle strong. |
| 527 | if (parent.strongRefcount > 0 && parent.wrapper == kj::none) { |
| 528 | // Parent has strong Rust refs and no JS wrapper — keep handle strong, |
| 529 | // discard any traced ref. |
| 530 | if (!traced.IsEmpty()) { |
| 531 | strong.ClearWeak<void>(); |
| 532 | traced.Reset(); |
| 533 | } |
| 534 | } else { |
| 535 | // Parent is only reachable via GC tracing — downgrade to a TracedReference. |
| 536 | if (traced.IsEmpty()) { |
| 537 | v8::HandleScope scope(parent.isolate); |
| 538 | traced.Reset(parent.isolate, strong.Get(parent.isolate)); |
| 539 | strong.SetWeak(); |
| 540 | } |
| 541 | } |
| 542 | |
| 543 | KJ_IF_SOME(c, cppgcVisitor) { |
| 544 | if (!traced.IsEmpty()) { |
| 545 | c.Trace(traced); |
| 546 | } |
| 547 | } |
| 548 | } |
| 549 | |
| 550 | } // namespace workerd::jsg |