Skip to content
File

Blob: src/workerd/jsg/wrappable.h

cpp349 lines
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.
25namespace kj {
26template <typename T>
27struct 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 
38namespace cppgc {
39class Visitor;
40}
41 
42namespace workerd::jsg {
43 
44// The ContextPointerSlot enum defines the embedder slots we use in v8::Context for
45// storing pointers to various important objects.
46enum 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 
62inline 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 
72template <typename T>
73kj::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 
82class MemoryTracker;
83 
84using kj::uint;
85 
86class GcVisitor;
87class 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.
109class 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++.
255class 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.
327template <typename T, bool isContext>
328T& 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