File
Blob: src/workerd/jsg/wrappable.h
| 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 | #pragma once |
| 6 | // INTERNAL IMPLEMENTATION FILE |
| 7 | // |
| 8 | // This file defines basic helpers involved in wrapping C++ objects for JavaScript consumption, |
| 9 | // including garbage-collecting those objects. |
| 10 | |
| 11 | #include <v8-context.h> |
| 12 | #include <v8-object.h> |
| 13 | #include <v8-version.h> |
| 14 | |
| 15 | #include <kj/common.h> |
| 16 | #include <kj/debug.h> |
| 17 | #include <kj/list.h> |
| 18 | #include <kj/refcount.h> |
| 19 | #include <kj/vector.h> |
| 20 | |
| 21 | // Niche value optimization for v8::TracedReference<T>. This teaches kj::Maybe to use |
| 22 | // TracedReference's built-in empty state (IsEmpty()) as the "none" representation, eliminating |
| 23 | // the extra bool + alignment padding that kj::Maybe normally adds. This saves 8 bytes per |
| 24 | // Maybe<TracedReference<T>> instance while preserving full type safety. |
| 25 | namespace kj { |
| 26 | template <typename T> |
| 27 | struct MaybeTraits<v8::TracedReference<T>> { |
| 28 | static void initNone(v8::TracedReference<T>* ptr) noexcept { |
| 29 | ctor(*ptr); |
| 30 | } |
| 31 | static bool isNone(const v8::TracedReference<T>& ref) noexcept { |
| 32 | return ref.IsEmpty(); |
| 33 | } |
| 34 | static constexpr bool noneIsMoveSafe = false; |
| 35 | }; |
| 36 | } // namespace kj |
| 37 | |
| 38 | namespace cppgc { |
| 39 | class Visitor; |
| 40 | } |
| 41 | |
| 42 | namespace workerd::jsg { |
| 43 | |
| 44 | // The ContextPointerSlot enum defines the embedder slots we use in v8::Context for |
| 45 | // storing pointers to various important objects. |
| 46 | enum class ContextPointerSlot : int { |
| 47 | // Pointer slot 0 is special and should never be used by us. |
| 48 | RESERVED = 0, |
| 49 | GLOBAL_WRAPPER = 1, |
| 50 | MODULE_REGISTRY = 2, |
| 51 | EXTENDED_CONTEXT_WRAPPER = 3, |
| 52 | VIRTUAL_FILE_SYSTEM = 4, |
| 53 | // Keep the MAX_POINTER_SLOT as the last entry and always set to |
| 54 | // to the highest value of the other entries. We use this to |
| 55 | // ensure that the highest used index is always initialized in |
| 56 | // every context we create without having to update the specific |
| 57 | // callsites whenever we add a new slot. We can just make the |
| 58 | // change here. |
| 59 | MAX_POINTER_SLOT = VIRTUAL_FILE_SYSTEM, |
| 60 | }; |
| 61 | |
| 62 | inline void setAlignedPointerInEmbedderData( |
| 63 | v8::Local<v8::Context> context, ContextPointerSlot slot, void* ptr) { |
| 64 | // The type tag is a small integer that should be different for every pointer |
| 65 | // type to avoid type confusion attacks. We just use the slot index for now, |
| 66 | // since we have a different pointer type for each slot. |
| 67 | KJ_DASSERT(slot != ContextPointerSlot::RESERVED, "Attempt to use reserved embedder data slot."); |
| 68 | context->SetAlignedPointerInEmbedderData( |
| 69 | static_cast<int>(slot), ptr, static_cast<v8::EmbedderDataTypeTag>(slot)); |
| 70 | } |
| 71 | |
| 72 | template <typename T> |
| 73 | kj::Maybe<T&> getAlignedPointerFromEmbedderData( |
| 74 | v8::Local<v8::Context> context, ContextPointerSlot slot) { |
| 75 | KJ_DASSERT(slot != ContextPointerSlot::RESERVED, "Attempt to use reserved embedder data slot."); |
| 76 | void* ptr = context->GetAlignedPointerFromEmbedderData( |
| 77 | static_cast<int>(slot), static_cast<v8::EmbedderDataTypeTag>(slot)); |
| 78 | if (ptr == nullptr) return kj::none; |
| 79 | return *reinterpret_cast<T*>(ptr); |
| 80 | } |
| 81 | |
| 82 | class MemoryTracker; |
| 83 | |
| 84 | using kj::uint; |
| 85 | |
| 86 | class GcVisitor; |
| 87 | class HeapTracer; |
| 88 | |
| 89 | // Base class for C++ objects which can be "wrapped" for JavaScript consumption. A JavaScript |
| 90 | // "wrapper" object is created, and then the JS wrapper and C++ Wrappable are "attached" to each |
| 91 | // other via attachWrapper(). |
| 92 | // |
| 93 | // A Wrappable instance does not necessarily have a wrapper attached. E.g. for JSG_RESOURCE |
| 94 | // types, wrappers are allocated lazily when the object first gets passed into JavaScript. |
| 95 | // |
| 96 | // Wrappable is refcounted via kj::Refcounted. When a JavaScript wrapper exists, it counts as |
| 97 | // a reference, keeping the object alive. When the JS object is garbage-collected, this |
| 98 | // reference is dropped, freeing the C++ object (unless other references exist). |
| 99 | // |
| 100 | // Wrappable also maintains a *second* reference count on the wrapper itself. While the second |
| 101 | // refcount is non-zero, the wrapper (the JavaScript object) will not be allowed to be |
| 102 | // garbage-collected, even if there are no references to it from other JS objects. This is |
| 103 | // important if the C++ object may be re-exported to JavaScript in the future and needs to have |
| 104 | // the same identity at that point (including maintaining any monkey-patches that the script |
| 105 | // may have applied to it previously). |
| 106 | // |
| 107 | // For resource types, this wrapper refcount counts the number of Ref<T>s that point to the |
| 108 | // Wrappable and are not visible to GC tracing. |
| 109 | class Wrappable: public kj::Refcounted { |
| 110 | public: |
| 111 | enum InternalFields : int { |
| 112 | // Field must contain a pointer to `WORKERD_WRAPPABLE_TAG`. This is a workerd-specific |
| 113 | // tag that helps us to identify a v8 API object as one of our own. |
| 114 | WRAPPABLE_TAG_FIELD_INDEX, |
| 115 | |
| 116 | // Index of the internal field that points back to the `Wrappable`. |
| 117 | WRAPPED_OBJECT_FIELD_INDEX, |
| 118 | |
| 119 | // Number of internal fields in a wrapper object. |
| 120 | INTERNAL_FIELD_COUNT, |
| 121 | }; |
| 122 | |
| 123 | static constexpr v8::CppHeapPointerTag WRAPPABLE_TAG = v8::CppHeapPointerTag::kDefaultTag; |
| 124 | |
| 125 | // The value pointed to by the internal field field `WRAPPABLE_TAG_FIELD_INDEX`. |
| 126 | // |
| 127 | // This value was chosen randomly. |
| 128 | static constexpr uint16_t WORKERD_WRAPPABLE_TAG = 0xeb04; |
| 129 | static constexpr uint16_t WORKERD_RUST_WRAPPABLE_TAG = 0xeb05; |
| 130 | |
| 131 | static bool isWorkerdApiObject(v8::Local<v8::Object> object) { |
| 132 | return object->GetAlignedPointerFromInternalField(WRAPPABLE_TAG_FIELD_INDEX, |
| 133 | static_cast<v8::EmbedderDataTypeTag>(WRAPPABLE_TAG_FIELD_INDEX)) == |
| 134 | &WORKERD_WRAPPABLE_TAG; |
| 135 | } |
| 136 | |
| 137 | void addStrongRef(); |
| 138 | void removeStrongRef(); |
| 139 | uint getStrongRefcount() const { |
| 140 | return strongRefcount; |
| 141 | } |
| 142 | |
| 143 | // Called by jsg::Ref<T> to ensure that its Wrappable is destroyed under the isolate lock. |
| 144 | // `ownSelf` keeps the raw `self` pointer valid -- they are passed separately because Wrappable is |
| 145 | // a private base class of the object. |
| 146 | void maybeDeferDestruction(bool strong, kj::Own<void> ownSelf, Wrappable* self); |
| 147 | |
| 148 | v8::Local<v8::Object> getHandle(v8::Isolate* isolate); |
| 149 | |
| 150 | kj::Maybe<v8::Local<v8::Object>> tryGetHandle(v8::Isolate* isolate) { |
| 151 | return wrapper.map([&](v8::TracedReference<v8::Object>& ref) { return ref.Get(isolate); }); |
| 152 | } |
| 153 | |
| 154 | // Visits a Ref<T> pointing at this Wrappable. `refParent` and `refStrong` are the members of |
| 155 | // `Ref<T>`, and this method is invoked on the object the ref points at. (This avoids the need |
| 156 | // to templatize the implementation of this method.) |
| 157 | void visitRef(GcVisitor& visitor, kj::Maybe<Wrappable&>& refParent, bool& refStrong); |
| 158 | |
| 159 | // Attach to a JavaScript object. This increments the Wrappable's refcount until `object` |
| 160 | // is garbage-collected (or unlink() is called). |
| 161 | // |
| 162 | // The object MUST have exactly 2 internal field slots, which will be initialized by this |
| 163 | // call as follows: |
| 164 | // - Internal field 0 is special and is used by the GC tracing implementation. |
| 165 | // - Internal field 1 is set to a pointer to the Wrappable. It can be used to unwrap the |
| 166 | // object. |
| 167 | // |
| 168 | // If `needsGcTracing` is true, then the virtual method jsgVisitForGc() will be called to |
| 169 | // perform GC tracing. If false, the method is never called (may be more efficient, if the |
| 170 | // method does nothing anyway). |
| 171 | void attachWrapper(v8::Isolate* isolate, v8::Local<v8::Object> object, bool needsGcTracing); |
| 172 | |
| 173 | // Attach an empty object as the wrapper. |
| 174 | v8::Local<v8::Object> attachOpaqueWrapper(v8::Local<v8::Context> context, bool needsGcTracing); |
| 175 | |
| 176 | // If `handle` was originally returned by attachOpaqueWrapper(), return the Wrappable it wraps. |
| 177 | // Otherwise, return nullptr. |
| 178 | static kj::Maybe<Wrappable&> tryUnwrapOpaque(v8::Isolate* isolate, v8::Local<v8::Value> handle); |
| 179 | |
| 180 | // Perform GC visitation. This is named with the `jsg` prefix because it pollutes the |
| 181 | // namespace of JSG_RESOURCE types. |
| 182 | virtual void jsgVisitForGc(GcVisitor& visitor); |
| 183 | |
| 184 | virtual kj::StringPtr jsgGetMemoryName() const { |
| 185 | KJ_UNIMPLEMENTED("jsgGetTypeName is not implemented. " |
| 186 | "It must be overridden by subclasses"); |
| 187 | } |
| 188 | |
| 189 | virtual size_t jsgGetMemorySelfSize() const { |
| 190 | KJ_UNIMPLEMENTED("jsgGetMemorySelfSize is not implemented. " |
| 191 | "It must be overridden by subclasses"); |
| 192 | } |
| 193 | |
| 194 | virtual void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const; |
| 195 | |
| 196 | virtual bool jsgGetMemoryInfoIsRootNode() const { |
| 197 | return strongRefcount > 0; |
| 198 | } |
| 199 | |
| 200 | virtual v8::Local<v8::Object> jsgGetMemoryInfoWrapperObject(v8::Isolate* isolate) { |
| 201 | KJ_IF_SOME(handle, tryGetHandle(isolate)) { |
| 202 | return handle; |
| 203 | } |
| 204 | return v8::Local<v8::Object>(); |
| 205 | } |
| 206 | |
| 207 | // Detaches the wrapper from V8 and returns the reference that V8 had previously held. |
| 208 | // (Typically, the caller will ignore the return value, thus dropping the reference.) |
| 209 | kj::Own<Wrappable> detachWrapper(bool shouldFreelistShim); |
| 210 | |
| 211 | // Called by HeapTracer when V8 tells us that it found a reference to this object. |
| 212 | void traceFromV8(cppgc::Visitor& cppgcVisitor); |
| 213 | |
| 214 | private: |
| 215 | class CppgcShim; |
| 216 | |
| 217 | // If a JS wrapper is currently allocated, this point to the cppgc shim object. |
| 218 | kj::Maybe<CppgcShim&> cppgcShim; |
| 219 | |
| 220 | // Handle to the JS wrapper object. The wrapper is created lazily when the object is first |
| 221 | // exported to JavaScript; until then, the wrapper is empty. |
| 222 | // |
| 223 | // If the wrapper object is "unmodified" from its original creation state, then V8 may choose to |
| 224 | // collect it even when it could still technically be reached via C++ objects. The idea here is |
| 225 | // that if the object is returned to JavaScript again later, the wrapper can be reconstructed at |
| 226 | // that time. However, if the wrapper is modified by the application (e.g. monkey-patched with |
| 227 | // a new property), then collecting and recreating it won't work. The logic to decide if an |
| 228 | // object has been "modified" is internal to V8 and baked into its use of EmbedderRootsHandler. |
| 229 | kj::Maybe<v8::TracedReference<v8::Object>> wrapper; |
| 230 | |
| 231 | // Whenever there are non-GC-traced references to the object (i.e. from other C++ objects, i.e. |
| 232 | // strongRefcount > 0), and `wrapper` is non-null, then `strongWrapper` contains a copy of |
| 233 | // `wrapper`, to force it to stay alive. Otherwise, `strongWrapper` is empty. |
| 234 | v8::Global<v8::Object> strongWrapper; |
| 235 | |
| 236 | // Will be non-null if `wrapper` has ever been non-null. |
| 237 | v8::Isolate* isolate = nullptr; |
| 238 | |
| 239 | // How many strong Ref<T>s point at this object, forcing the wrapper to stay alive even if GC |
| 240 | // tracing doesn't find it? |
| 241 | // |
| 242 | // Whenever the value of the boolean expression (strongRefcount > 0 && wrapper.IsEmpty()) changes, |
| 243 | // a GC visitation is needed to update all outgoing refs. |
| 244 | uint strongRefcount = 0; |
| 245 | |
| 246 | // When `wrapperRef` is non-empty, the Wrappable is a member of the list `HeapTracer::wrappers`. |
| 247 | kj::ListLink<Wrappable> link; |
| 248 | |
| 249 | friend class GcVisitor; |
| 250 | friend class HeapTracer; |
| 251 | friend class MemoryTracker; |
| 252 | }; |
| 253 | |
| 254 | // For historical reasons, this is actually implemented in setup.c++. |
| 255 | class HeapTracer: public v8::EmbedderRootsHandler { |
| 256 | public: |
| 257 | explicit HeapTracer(v8::Isolate* isolate); |
| 258 | |
| 259 | ~HeapTracer() noexcept { |
| 260 | // Destructor has to be noexcept because it inherits from a V8 type that has a noexcept |
| 261 | // destructor. |
| 262 | KJ_IREQUIRE(isolate == nullptr, "you must call HeapTracer.destroy()"); |
| 263 | } |
| 264 | |
| 265 | // Call under isolate lock when shutting down isolate. |
| 266 | void destroy(); |
| 267 | |
| 268 | static HeapTracer& getTracer(v8::Isolate* isolate); |
| 269 | |
| 270 | // Returns true if the current thread is currently executing the destructor of a CppgcShim |
| 271 | // object, which implies that we are collecting unreachable objects. |
| 272 | static bool isInCppgcDestructor(); |
| 273 | |
| 274 | void addWrapper(kj::Badge<Wrappable>, Wrappable& wrappable) { |
| 275 | wrappers.add(wrappable); |
| 276 | } |
| 277 | void removeWrapper(kj::Badge<Wrappable>, Wrappable& wrappable) { |
| 278 | wrappers.remove(wrappable); |
| 279 | } |
| 280 | void clearWrappers(); |
| 281 | |
| 282 | void addToFreelist(Wrappable::CppgcShim& shim); |
| 283 | Wrappable::CppgcShim* allocateShim(Wrappable& wrappable); |
| 284 | void clearFreelistedShims(); |
| 285 | |
| 286 | // implements EmbedderRootsHandler ------------------------------------------- |
| 287 | void ResetRoot(const v8::TracedReference<v8::Value>& handle) override; |
| 288 | bool TryResetRoot(const v8::TracedReference<v8::Value>& handle) override; |
| 289 | |
| 290 | kj::StringPtr jsgGetMemoryName() const { |
| 291 | return "HeapTracer"_kjc; |
| 292 | } |
| 293 | size_t jsgGetMemorySelfSize() const { |
| 294 | return sizeof(*this); |
| 295 | } |
| 296 | void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const; |
| 297 | bool jsgGetMemoryInfoIsRootNode() const { |
| 298 | return false; |
| 299 | } |
| 300 | |
| 301 | private: |
| 302 | v8::Isolate* isolate; |
| 303 | kj::Vector<Wrappable*> wrappersToTrace; |
| 304 | |
| 305 | // Wrappables on which detachWrapper() should be called at the end of this GC pass. |
| 306 | kj::Vector<Wrappable*> detachLater; |
| 307 | |
| 308 | // List of all Wrappables for which a JavaScript wrapper exists. |
| 309 | kj::List<Wrappable, &Wrappable::link> wrappers; |
| 310 | |
| 311 | // List of shim objects for wrappers that were collected during a minor GC. The shim objects |
| 312 | // can be reused for future allocations. |
| 313 | kj::Maybe<Wrappable::CppgcShim&> freelistedShims; |
| 314 | }; |
| 315 | |
| 316 | // Try to use this in any scope where JavaScript wrapped objects are destroyed, to confirm that |
| 317 | // they don't hold disallowed references to KJ I/O objects. IoOwn's destructor will explicitly |
| 318 | // create AllowAsyncDestructorsScope to permit holding such objects via IoOwn. This is meant to |
| 319 | // help catch bugs. |
| 320 | #define DISALLOW_KJ_IO_DESTRUCTORS_SCOPE \ |
| 321 | kj::DisallowAsyncDestructorsScope disallow( \ |
| 322 | "JavaScript heap objects must not contain KJ I/O objects without a IoOwn") |
| 323 | // TODO(soon): |
| 324 | // - Track memory usage of native objects. |
| 325 | |
| 326 | // Given a handle to a resource type, extract the raw C++ object pointer. |
| 327 | template <typename T, bool isContext> |
| 328 | T& extractInternalPointer( |
| 329 | const v8::Local<v8::Context>& context, const v8::Local<v8::Object>& object) { |
| 330 | // Due to bugs in V8, we can't use internal fields on the global object: |
| 331 | // https://groups.google.com/d/msg/v8-users/RET5b3KOa5E/3EvpRBzwAQAJ |
| 332 | // |
| 333 | // So, when wrapping a global object, we store the pointer in the "embedder data" of the context |
| 334 | // instead of the internal fields of the object. |
| 335 | |
| 336 | if constexpr (isContext) { |
| 337 | // V8 docs say EmbedderData slot 0 is special, so we use slot 1. (See comments in newContext().) |
| 338 | return KJ_ASSERT_NONNULL( |
| 339 | getAlignedPointerFromEmbedderData<T>(context, ContextPointerSlot::GLOBAL_WRAPPER)); |
| 340 | } else { |
| 341 | KJ_ASSERT(object->InternalFieldCount() == Wrappable::INTERNAL_FIELD_COUNT); |
| 342 | return *reinterpret_cast<T*>( |
| 343 | object->GetAlignedPointerFromInternalField(Wrappable::WRAPPED_OBJECT_FIELD_INDEX, |
| 344 | static_cast<v8::EmbedderDataTypeTag>(Wrappable::WRAPPED_OBJECT_FIELD_INDEX))); |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | } // namespace workerd::jsg |