Skip to content
File

Blob: src/workerd/jsg/buffersource.h

cpp500 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 
7#include "jsg.h"
8 
9#include <v8-typed-array.h>
10 
11namespace workerd::jsg {
12 
13#define JSG_ARRAY_BUFFER_VIEW_TYPES(V) \
14 V(Uint8Array, 1, true) \
15 V(Uint8ClampedArray, 1, true) \
16 V(Uint16Array, 2, true) \
17 V(Uint32Array, 4, true) \
18 V(Int8Array, 1, true) \
19 V(Int16Array, 2, true) \
20 V(Int32Array, 4, true) \
21 V(Float16Array, 2, false) \
22 V(Float32Array, 4, false) \
23 V(Float64Array, 8, false) \
24 V(BigInt64Array, 8, true) \
25 V(BigUint64Array, 8, true)
26 
27template <typename T>
28concept BufferSourceType = requires(
29 T a) { kj::isSameType<v8::ArrayBuffer, T>() || std::is_base_of_v<v8::ArrayBufferView, T>; };
30 
31template <BufferSourceType T>
32static constexpr size_t getBufferSourceElementSize() {
33 if constexpr (kj::isSameType<v8::Uint8Array, T>() || kj::isSameType<v8::Uint8ClampedArray, T>() ||
34 kj::isSameType<v8::Int8Array, T>() || kj::isSameType<v8::DataView, T>() ||
35 kj::isSameType<v8::ArrayBuffer, T>() || kj::isSameType<v8::ArrayBufferView, T>() ||
36 kj::isSameType<v8::TypedArray, T>()) {
37 return 1;
38 }
39#define V(Type, size, _) \
40 else if constexpr (kj::isSameType<v8::Type, T>()) { \
41 return size; \
42 }
43 JSG_ARRAY_BUFFER_VIEW_TYPES(V)
44#undef V
45 asm("no_matching_buffer_view_type\n");
46}
47 
48template <BufferSourceType T>
49static constexpr size_t checkIsIntegerType() {
50 if constexpr (kj::isSameType<v8::ArrayBuffer, T>() || kj::isSameType<v8::DataView, T>() ||
51 kj::isSameType<v8::ArrayBufferView, T>()) {
52 return false;
53 } else if constexpr (kj::isSameType<v8::TypedArray, T>()) {
54 return true;
55 }
56#define V(Type, _, res) \
57 else if constexpr (kj::isSameType<v8::Type, T>()) { \
58 return res; \
59 }
60 JSG_ARRAY_BUFFER_VIEW_TYPES(V)
61#undef V
62 asm("no_matching_buffer_view_type\n");
63}
64 
65class BufferSource;
66class BackingStore;
67using BufferSourceViewConstructor = v8::Local<v8::Value> (*)(Lock&, BackingStore&);
68 
69// The jsg::BackingStore wraps a v8::BackingStore and retains information about the
70// type of ArrayBuffer or ArrayBufferView to which it is associated. Namely, it records
71// the byte length, offset, element size, and constructor type allowing the view to be
72// recreated.
73//
74// Once allocated, the BackingStore can be safely used outside of the isolate lock. If
75// using V8 sandboxing, it cannot be passed to another isolate unless that isolate is
76// part of the same IsolateGroup.
77class BackingStore {
78 public:
79 template <BufferSourceType T = v8::Uint8Array>
80 // This requires the js lock to ensure the backing is allocated in the correct
81 // V8 sandbox for the isolate.
82 static BackingStore from(Lock& js, kj::Array<kj::byte> data) {
83 // Creates a new BackingStore that takes over ownership of the given kj::Array.
84 // The bytes may be moved if they are not inside the sandbox already.
85 size_t size = data.size();
86 if (js.v8Isolate->GetGroup().SandboxContains(data.begin())) {
87 auto ptr = new kj::Array<byte>(kj::mv(data));
88 return BackingStore(v8::ArrayBuffer::NewBackingStore(ptr->begin(), size,
89 [](void*, size_t, void* ptr) {
90 delete reinterpret_cast<kj::Array<byte>*>(ptr);
91 }, ptr),
92 size, 0, getBufferSourceElementSize<T>(), construct<T>, checkIsIntegerType<T>());
93 } else {
94 auto backingStore = js.allocBackingStore(size, Lock::AllocOption::UNINITIALIZED);
95 
96 auto result = BackingStore(kj::mv(backingStore), size, 0, getBufferSourceElementSize<T>(),
97 construct<T>, checkIsIntegerType<T>());
98 memcpy(result.asArrayPtr().begin(), data.begin(), size);
99 return result;
100 }
101 }
102 
103 // Creates a new BackingStore of the given size.
104 template <BufferSourceType T = v8::Uint8Array>
105 static BackingStore alloc(
106 Lock& js, size_t size, Lock::AllocOption init_mode = Lock::AllocOption::ZERO_INITIALIZED) {
107 return BackingStore(js.allocBackingStore(size, init_mode), size, 0,
108 getBufferSourceElementSize<T>(), construct<T>, checkIsIntegerType<T>());
109 }
110 
111 using Disposer = void(void*, size_t, void*);
112 
113 // Creates and returns a BackingStore that wraps an external data pointer
114 // with a custom disposer.
115 template <BufferSourceType T = v8::Uint8Array>
116 static BackingStore wrap(void* data, size_t size, Disposer disposer, void* ctx) {
117 return BackingStore(v8::ArrayBuffer::NewBackingStore(data, size, disposer, ctx), size, 0,
118 getBufferSourceElementSize<T>(), construct<T>, checkIsIntegerType<T>());
119 }
120 
121 explicit BackingStore(std::shared_ptr<v8::BackingStore> backingStore,
122 size_t byteLength,
123 size_t byteOffset,
124 size_t elementSize,
125 BufferSourceViewConstructor ctor,
126 bool integerType);
127 
128 BackingStore(BackingStore&& other) = default;
129 BackingStore& operator=(BackingStore&& other) = default;
130 KJ_DISALLOW_COPY(BackingStore);
131 
132 template <typename T = kj::byte>
133 inline kj::ArrayPtr<T> asArrayPtr() KJ_LIFETIMEBOUND {
134 KJ_ASSERT(backingStore != nullptr, "Invalid access after move.");
135 KJ_ASSERT(byteLength % sizeof(T) == 0);
136 kj::byte* data = static_cast<kj::byte*>(backingStore->Data());
137 return kj::ArrayPtr<T>(reinterpret_cast<T*>(data + byteOffset), byteLength / sizeof(T));
138 }
139 
140 template <typename T = kj::byte>
141 inline operator kj::ArrayPtr<T>() KJ_LIFETIMEBOUND {
142 return asArrayPtr<T>();
143 }
144 
145 bool operator==(const BackingStore& other);
146 
147 template <typename T = kj::byte>
148 inline const kj::ArrayPtr<const T> asArrayPtr() const KJ_LIFETIMEBOUND {
149 KJ_ASSERT(backingStore != nullptr, "Invalid access after move.");
150 KJ_ASSERT(byteLength % sizeof(T) == 0);
151 return kj::ArrayPtr<T>(
152 static_cast<T*>(backingStore->Data()) + byteOffset, byteLength / sizeof(T));
153 }
154 
155 template <typename T = kj::byte>
156 inline operator const kj::ArrayPtr<const T>() const KJ_LIFETIMEBOUND {
157 return asArrayPtr<T>();
158 }
159 
160 inline size_t size() const {
161 return byteLength;
162 };
163 inline size_t getOffset() const {
164 return byteOffset;
165 }
166 inline size_t getElementSize() const {
167 return elementSize;
168 }
169 inline bool isIntegerType() const {
170 return integerType;
171 }
172 
173 // Creates a new BackingStore as a view over the same underlying v8::BackingStore
174 // but with different handle type information. This is required, for instance, in
175 // use cases like the Streams API where we have to be able to surface a Uint8Array
176 // view over the BackingStore to fulfill a BYOB read while maintaining the original
177 // type information to recreate the original type of view once the read is complete.
178 template <BufferSourceType T = v8::Uint8Array>
179 BackingStore getTypedView() {
180 return BackingStore(backingStore, byteLength, byteOffset, getBufferSourceElementSize<T>(),
181 construct<T>, checkIsIntegerType<T>());
182 }
183 
184 template <BufferSourceType T = v8::Uint8Array>
185 BackingStore getTypedViewSlice(size_t start, size_t end) {
186 KJ_ASSERT(start <= end);
187 auto length = end - start;
188 auto startOffset = byteOffset + start;
189 KJ_ASSERT(length <= byteLength);
190 KJ_ASSERT(startOffset <= backingStore->ByteLength());
191 KJ_ASSERT(startOffset + length <= backingStore->ByteLength());
192 return BackingStore(backingStore, length, startOffset, getBufferSourceElementSize<T>(),
193 construct<T>, checkIsIntegerType<T>());
194 }
195 
196 inline v8::Local<v8::Value> createHandle(Lock& js) {
197 return ctor(js, *this);
198 }
199 
200 // Shrinks the effective size of the backing store by a number of bytes off
201 // the front of the data. Useful when incrementally consuming the data as
202 // we do in the streams implementation.
203 inline void consume(size_t bytes) {
204 KJ_ASSERT(bytes <= byteLength);
205 byteOffset += bytes;
206 byteLength -= bytes;
207 }
208 
209 // Shrinks the effective size of the backing store by a number of bytes off
210 // the end of the data. Useful when a more limited view of the buffer is
211 // required (such as when fulfilling partial stream reads).
212 inline void trim(size_t bytes) {
213 KJ_ASSERT(bytes <= byteLength);
214 byteLength -= bytes;
215 }
216 
217 // Similar to trim except that it explicitly sets the byte length to a value
218 // equal to or less than the current byte length.
219 inline void limit(size_t bytes) {
220 KJ_ASSERT(bytes <= byteLength);
221 byteLength = bytes;
222 }
223 
224 inline BackingStore clone() {
225 return BackingStore(backingStore, byteLength, byteOffset, elementSize, ctor, integerType);
226 }
227 
228 template <class T = v8::Uint8Array>
229 inline BackingStore copy(jsg::Lock& js) {
230 if (byteLength == 0) return BackingStore::alloc<T>(js, 0);
231 auto dest = BackingStore::alloc<T>(js, byteLength);
232 memcpy(dest.asArrayPtr().begin(), asArrayPtr().begin(), byteLength);
233 return dest;
234 }
235 
236 JSG_MEMORY_INFO(BackingStore) {
237 tracker.trackFieldWithSize("buffer", size());
238 }
239 
240 private:
241 std::shared_ptr<v8::BackingStore> backingStore;
242 size_t byteLength;
243 size_t byteOffset;
244 size_t elementSize;
245 
246 // The ctor here is a pointer to a static template function that can create a
247 // new type-specific instance of the JavaScript ArrayBuffer or ArrayBufferView wrapper
248 // for the backing store. The specific type of constructor to store is determined
249 // when the BufferSource instance is created and it is used only if getHandle() is
250 // called on a BufferSource that has been detached.
251 BufferSourceViewConstructor ctor;
252 
253 bool integerType;
254 
255 template <BufferSourceType T>
256 static v8::Local<v8::Value> construct(Lock& js, BackingStore& store) {
257 if constexpr (kj::isSameType<v8::ArrayBuffer, T>()) {
258 return v8::ArrayBuffer::New(js.v8Isolate, store.backingStore);
259 } else if constexpr (kj::isSameType<v8::ArrayBufferView, T>()) {
260 return v8::DataView::New(v8::ArrayBuffer::New(js.v8Isolate, store.backingStore),
261 store.byteOffset, store.byteLength);
262 } else if constexpr (kj::isSameType<v8::TypedArray, T>()) {
263 return v8::Uint8Array::New(v8::ArrayBuffer::New(js.v8Isolate, store.backingStore),
264 store.byteOffset, store.byteLength);
265 } else {
266 return T::New(v8::ArrayBuffer::New(js.v8Isolate, store.backingStore), store.byteOffset,
267 store.byteLength / store.elementSize);
268 }
269 }
270 
271 friend class BufferSource;
272};
273 
274// A BufferSource is an abstraction for v8::ArrayBuffer and v8::ArrayBufferView types.
275// It has a couple of significant features relative to the alternative mapping between
276// kj::Array<kj::byte> and ArrayBuffer/ArrayBufferView:
277//
278// * A BufferSource created from an ArrayBuffer/ArrayBufferView maintains a reference
279// to JavaScript object, ensuring that when the BufferSource is passed back
280// out to JavaScript, the same object will be returned.
281// * A BufferSource can detach the BackingStore from the ArrayBuffer/ArrayBufferView.
282// When doing so, the BackingStore is removed from the BufferSource and the association
283// with the ArrayBuffer/ArrayBufferView is severed.
284//
285// When an object holds a reference to a BufferSource (e.g. as a member variable), it
286// must implement visitForGc and ensure the BufferSource is properly visited,
287//
288// As a side note, the name "BufferSource" comes from the Web IDL spec.
289//
290// How to use it:
291//
292// In methods that are exposed to JavaScript, specify jsg::BufferSource as the type:
293// e.g.
294//
295// class MyAPiObject: public jsg::Object {
296// public:
297// jsg::BufferSource foo(jsg::Lock& js, jsg::BufferSource source) {
298// // While the BufferSource is attached, you can access the data as an
299// // kj::ArrayPtr...
300// {
301// auto ptr = kj::ArrayPtr<kj::byte>(source);
302// }
303//
304// // Or, you can detach the jsg::BackingStore from the BufferSource.
305// auto backingStore = source.detach();
306// auto ptr = kj::ArrayPtr<kj::byte>(backingStore);
307// // Do something with ptr...
308// return BufferSource(js, kj::mv(backingStore));
309// }
310// };
311class BufferSource {
312 public:
313 static kj::Maybe<BufferSource> tryAlloc(Lock& js, size_t size);
314 
315 // The unsafe variant does not initialize the allocated memory. Use with caution!
316 static kj::Maybe<BufferSource> tryAllocUnsafe(Lock& js, size_t size);
317 static BufferSource wrap(
318 Lock& js, void* data, size_t size, BackingStore::Disposer disposer, void* ctx);
319 
320 // Create a new BufferSource that takes over ownership of the given BackingStore.
321 explicit BufferSource(Lock& js, BackingStore&& backingStore);
322 
323 // Create a BufferSource from the given JavaScript handle.
324 explicit BufferSource(Lock& js, v8::Local<v8::Value> handle);
325 
326 BufferSource(BufferSource&&) = default;
327 BufferSource& operator=(BufferSource&&) = default;
328 
329 KJ_DISALLOW_COPY(BufferSource);
330 
331 // True if the BackingStore has been removed from this BufferSource.
332 inline bool isDetached() const {
333 return maybeBackingStore == kj::none;
334 }
335 
336 bool canDetach(Lock& js);
337 
338 // Removes the BackingStore from the BufferSource and severs its connection to
339 // the ArrayBuffer/ArrayBufferView handle.
340 // It's worth mentioning that detach can throw application-visible exceptions
341 // in the case the ArrayBuffer cannot be detached. Any detaching should be
342 // performed as early as possible in an API method implementation.
343 BackingStore detach(Lock& js, kj::Maybe<v8::Local<v8::Value>> maybeKey = kj::none);
344 
345 v8::Local<v8::Value> getHandle(Lock& js);
346 JsBufferSource getJsHandle(Lock& js);
347 
348 template <typename T = kj::byte>
349 inline kj::ArrayPtr<T> asArrayPtr() KJ_LIFETIMEBOUND {
350 return KJ_ASSERT_NONNULL(maybeBackingStore).asArrayPtr<T>();
351 }
352 
353 template <typename T = kj::byte>
354 inline operator kj::ArrayPtr<T>() KJ_LIFETIMEBOUND {
355 return asArrayPtr<T>();
356 }
357 
358 template <typename T = kj::byte>
359 inline const kj::ArrayPtr<const T> asArrayPtr() const KJ_LIFETIMEBOUND {
360 return KJ_ASSERT_NONNULL(maybeBackingStore).asArrayPtr<T>();
361 }
362 
363 template <typename T = kj::byte>
364 inline operator const kj::ArrayPtr<const T>() const KJ_LIFETIMEBOUND {
365 return asArrayPtr<T>();
366 }
367 
368 inline size_t size() const {
369 return KJ_ASSERT_NONNULL(maybeBackingStore).size();
370 };
371 
372 inline kj::Maybe<size_t> underlyingArrayBufferSize(Lock& js) {
373 if (isDetached()) {
374 return kj::none;
375 }
376 auto h = getHandle(js);
377 if (h->IsArrayBuffer()) {
378 return h.As<v8::ArrayBuffer>()->ByteLength();
379 } else if (h->IsArrayBufferView()) {
380 return h.As<v8::ArrayBufferView>()->Buffer()->ByteLength();
381 }
382 KJ_UNREACHABLE;
383 }
384 
385 inline size_t getOffset() const {
386 return KJ_ASSERT_NONNULL(maybeBackingStore).getOffset();
387 }
388 
389 inline size_t getElementSize() const {
390 return KJ_ASSERT_NONNULL(maybeBackingStore).getElementSize();
391 }
392 
393 // Some standard APIs that use BufferSource / ArrayBufferView are limited to just
394 // supported "Integer-type ArrayBufferViews". As a convenience, when the BufferSource
395 // is created, we record whether or not the type qualifies as an integer type.
396 inline bool isIntegerType() const {
397 return KJ_ASSERT_NONNULL(maybeBackingStore).isIntegerType();
398 }
399 
400 // Sets the detach key that must be provided with the detach(...) method
401 // to successfully detach the backing store.
402 void setDetachKey(Lock& js, v8::Local<v8::Value> key);
403 
404 // Shrinks the effective size of the backing store by a number of bytes off
405 // the end of the data. Useful when a more limited view of the buffer is
406 // required (such as when fulfilling partial stream reads).
407 inline void trim(jsg::Lock& js, size_t bytes) {
408 auto& backing = KJ_ASSERT_NONNULL(maybeBackingStore);
409 backing.trim(bytes);
410 // When trimming, we need to also update the handle to reflect the new size.
411 handle = js.v8Ref(backing.createHandle(js));
412 }
413 
414 inline BufferSource clone(jsg::Lock& js) {
415 return BufferSource(js, KJ_ASSERT_NONNULL(maybeBackingStore).clone());
416 }
417 
418 template <class T = v8::Uint8Array>
419 inline BufferSource copy(jsg::Lock& js) {
420 KJ_IF_SOME(backing, maybeBackingStore) {
421 return BufferSource(js, backing.copy<T>(js));
422 }
423 return BufferSource(js, BackingStore::alloc<T>(js, 0));
424 }
425 
426 inline void setToZero() {
427 KJ_IF_SOME(backing, maybeBackingStore) {
428 backing.asArrayPtr().fill(0);
429 }
430 }
431 
432 template <BufferSourceType T = v8::Uint8Array>
433 BufferSource getTypedViewSlice(jsg::Lock& js, size_t start, size_t end) {
434 return BufferSource(js, KJ_ASSERT_NONNULL(maybeBackingStore).getTypedViewSlice<T>(start, end));
435 }
436 
437 template <BufferSourceType T = v8::Uint8Array>
438 BufferSource getTypedView(jsg::Lock& js) {
439 return BufferSource(js, KJ_ASSERT_NONNULL(maybeBackingStore).getTypedView<T>());
440 }
441 
442 JSG_MEMORY_INFO(BufferSource) {
443 tracker.trackField("handle", handle);
444 KJ_IF_SOME(backing, maybeBackingStore) {
445 tracker.trackField("backing", backing);
446 }
447 }
448 
449 private:
450 Value handle;
451 kj::Maybe<BackingStore> maybeBackingStore;
452 
453 static auto determineConstructor(auto& value) {
454 if (value->IsArrayBuffer()) {
455 return BackingStore::construct<v8::ArrayBuffer>;
456 } else if (value->IsDataView()) {
457 return BackingStore::construct<v8::DataView>;
458 }
459#define V(Type, _, __) else if (value->Is##Type()) return BackingStore::construct<v8::Type>;
460 JSG_ARRAY_BUFFER_VIEW_TYPES(V)
461#undef V
462 KJ_UNREACHABLE;
463 }
464 
465 friend class BackingStore;
466 friend class GcVisitor;
467};
468 
469// TypeWrapper implementation for the BufferSource type.
470class BufferSourceWrapper {
471 public:
472 static constexpr const char* getName(BufferSource*) {
473 return "BufferSource";
474 }
475 
476 v8::Local<v8::Value> wrap(Lock& js,
477 v8::Local<v8::Context> context,
478 kj::Maybe<v8::Local<v8::Object>> creator,
479 BufferSource bufferSource) {
480 return bufferSource.getHandle(js);
481 }
482 
483 kj::Maybe<BufferSource> tryUnwrap(Lock& js,
484 v8::Local<v8::Context> context,
485 v8::Local<v8::Value> handle,
486 BufferSource*,
487 kj::Maybe<v8::Local<v8::Object>> parentObject) {
488 if (!handle->IsArrayBuffer() && !handle->IsArrayBufferView()) {
489 return kj::none;
490 }
491 return BufferSource(js, handle);
492 }
493};
494 
495inline BufferSource Lock::arrayBuffer(kj::Array<kj::byte> data) {
496 return BufferSource(*this, BackingStore::from<v8::ArrayBuffer>(*this, kj::mv(data)));
497}
498 
499} // namespace workerd::jsg