docs
Loading...
Searching...
No Matches
Buffer.h
Go to the documentation of this file.
1///
2/// Copyright (C) 2026 Ultralight, Inc. All rights reserved.
3/// A license is required for commercial use. https://ultralig.ht
4///
5#pragma once
6#include <Ultralight/Buffer.h>
7#include <Ultralight/CAPI/CAPI_JSBuffer.h>
8#include <Ultralight/RefPtr.h>
10#include <Ultralight/js/Value.h>
11#include <Ultralight/js/detail/BufferBridge.h>
12
13#include <cstdint>
14#include <span>
15#include <string>
16#include <type_traits>
17
18namespace ultralight {
19namespace js {
20
21///
22/// Handle to a JavaScript ArrayBuffer for sharing raw binary memory with the page.
23///
24/// ArrayBuffer wraps a JavaScript ArrayBuffer, letting native code and page script share raw binary
25/// memory without copying. You can pass native memory directly to the page as an ArrayBuffer, or
26/// wrap a buffer received from the page.
27///
28/// Share native memory with the page by wrapping a Buffer in an ArrayBuffer:
29///
30/// ```
31/// RefPtr<Buffer> pixels = Buffer::Create(
32/// pixel_data, pixel_size, nullptr,
33/// [](void* user_data, void* data) { FreePixels(data); });
34/// ctx["pixels"] = ctx.MakeArrayBuffer(pixels);
35/// ```
36///
37/// Page script accesses the shared memory through a typed array:
38///
39/// ```js
40/// const bytes = new Uint8Array(pixels); // the same memory, no copy
41/// ```
42///
43/// ## Reading Bytes from the Page
44///
45/// Native code accesses the page's bytes through a Buffer that views the memory without copying:
46///
47/// - **Accept a RefPtr<Buffer> in a bound function.** Page script passes an ArrayBuffer as the
48/// argument.
49/// - **Call ShareBytes() on an ArrayBuffer handle.** The method returns a Buffer for an ArrayBuffer
50/// you already hold.
51///
52/// Access the bytes from native code:
53///
54/// ```
55/// app["upload"] = [](RefPtr<Buffer> bytes) { Enqueue(bytes); };
56///
57/// js::ArrayBuffer save(ctx["saveData"]); // empty if it isn't an ArrayBuffer
58/// RefPtr<Buffer> save_bytes = save.ShareBytes();
59/// ```
60///
61/// ## Detaching an ArrayBuffer
62///
63/// Call Detach() to take shared bytes back from the page early so JavaScript can no longer reach
64/// the memory.
65///
66/// Once native code reads the bytes (by calling ShareBytes(), receiving a RefPtr<Buffer> or
67/// std::span parameter in a bound function, or calling TypedArray::view()), the ArrayBuffer can no
68/// longer be detached by native code or by the page, including an ArrayBuffer created from your own
69/// Buffer. Calling Detach() then returns `false`.
70///
71/// ## Byte Lifetimes
72///
73/// A native Buffer shared with the page follows these lifetime rules:
74///
75/// - **Shared buffers stay referenced until collection or detachment.** The library releases its
76/// reference during a later Renderer::Update() after the ArrayBuffer is garbage collected or
77/// detached.
78/// - **The destruction callback runs where the last reference is released.** That happens during
79/// Renderer::Update() if the ArrayBuffer held the last reference, or wherever native code drops
80/// its last one.
81///
82/// @warning Bytes owned by the page are freed when the page goes away, even while native code
83/// holds a Buffer over them. Use Buffer::CreateFromCopy() if the data must outlive the
84/// page.
85///
86/// @see js::Context::MakeArrayBuffer(), js::TypedArray, Buffer::Create(), Buffer::CreateFromCopy()
87///
88class ArrayBuffer : public Value {
89 public:
90 ///
91 /// Create an empty ArrayBuffer wrapper.
92 ///
93 ArrayBuffer() = default;
94
95 ///
96 /// Wrap a value if it's an ArrayBuffer (detached ones included), otherwise create an empty
97 /// wrapper.
98 ///
99 /// @param value The value to wrap.
100 ///
101 explicit ArrayBuffer(const Value& value)
102 : Value(value.raw() && ulJSValueIsArrayBuffer(value.raw()) ? value : Value()) {}
103
104 ///
105 /// Share this ArrayBuffer's bytes with native code without copying.
106 ///
107 /// The Buffer is always a new one, even for an ArrayBuffer created from your own Buffer.
108 /// Holding it keeps the ArrayBuffer alive while the page lives.
109 ///
110 /// @return Returns a Buffer that views the bytes (a null RefPtr if the ArrayBuffer is
111 /// detached or the wrapper is empty).
112 ///
113 /// \parblock
114 /// @note The ArrayBuffer can no longer be detached afterwards.
115 /// \endparblock
116 ///
117 /// \parblock
118 /// @note You can release the returned Buffer from any thread.
119 /// \endparblock
120 ///
121 /// @warning The bytes can be freed when the page goes away, even while you hold the Buffer (see
122 /// "Byte Lifetimes" in the class overview).
123 ///
125 return detail::AdoptULBuffer(ulJSArrayBufferGetBuffer(raw()));
126 }
127
128 ///
129 /// Get the length in bytes. The ArrayBuffer can still be detached.
130 ///
131 /// @return Returns the byte length (0 if the ArrayBuffer is detached or the wrapper is
132 /// empty).
133 ///
134 size_t byte_length() const { return ulJSArrayBufferGetByteLength(raw()); }
135
136 ///
137 /// Detach this ArrayBuffer so JavaScript can no longer reach its bytes.
138 ///
139 /// Afterwards its `byteLength` is 0, typed arrays over it have length 0 (element reads
140 /// return `undefined`), and creating a new typed array over it throws a TypeError. If it was
141 /// created from a Buffer, its reference to that Buffer is released during a later
142 /// Renderer::Update().
143 ///
144 /// @return Returns true if the ArrayBuffer is detached on return (including one that was
145 /// already detached). Returns false if it can no longer be detached or the wrapper
146 /// is empty.
147 ///
148 [[nodiscard]] bool Detach() const { return ulJSArrayBufferDetach(raw()); }
149};
150
151///
152/// Handle to a JavaScript typed array on a page.
153///
154/// TypedArray wraps a JavaScript typed array (such as a Float32Array or Uint8Array) on a page,
155/// letting native code read and write its elements directly. It doesn't copy the underlying bytes,
156/// so changes appear on the page right away.
157///
158/// Create a typed array for the page, or read elements from one that script created:
159///
160/// ```
161/// js::TypedArray<float> samples = ctx.MakeTypedArray<float>(4);
162/// samples.view()[0] = 0.5f;
163/// ctx["samples"] = samples;
164///
165/// js::TypedArray<float> weights(ctx["weights"]); // empty unless a Float32Array
166/// float total = 0;
167/// for (float weight : weights.view())
168/// total += weight;
169/// ```
170///
171/// ## Element Types
172///
173/// Each supported C++ type pairs with a matching JavaScript typed array:
174///
175/// | C++ Type | JavaScript Typed Array |
176/// |------------|------------------------|
177/// | `int8_t` | Int8Array |
178/// | `uint8_t` | Uint8Array |
179/// | `int16_t` | Int16Array |
180/// | `uint16_t` | Uint16Array |
181/// | `int32_t` | Int32Array |
182/// | `uint32_t` | Uint32Array |
183/// | `int64_t` | BigInt64Array |
184/// | `uint64_t` | BigUint64Array |
185/// | `float` | Float32Array |
186/// | `double` | Float64Array |
187///
188/// A Uint8ClampedArray also counts as `uint8_t` when read from the page.
189///
190/// \parblock
191/// @note A typed array's bytes belong to an underlying ArrayBuffer, and view() and data() follow
192/// the byte rules there (see js::ArrayBuffer).
193/// \endparblock
194///
195/// \parblock
196/// @note A bound function takes a typed array as a std::span<T> parameter instead of a TypedArray
197/// wrapper.
198/// \endparblock
199///
200/// @see js::ArrayBuffer, js::Context::MakeTypedArray()
201///
202template <typename T>
203class TypedArray : public Value {
204 static_assert(detail::kTypedArrayTypeOf<T> != kULJSTypedArrayType_None,
205 "TypedArray<T>: T must be one of the fixed-width typed-array element types "
206 "(intN_t / uintN_t, float, double)");
207
208 public:
209 ///
210 /// Create an empty TypedArray wrapper.
211 ///
212 TypedArray() = default;
213
214 ///
215 /// Wrap a value if it's a typed array with elements of type `T`, otherwise create an empty
216 /// wrapper. A Uint8ClampedArray counts as `uint8_t`.
217 ///
218 /// @param value The value to wrap.
219 ///
220 explicit TypedArray(const Value& value)
221 : Value(value.raw()
222 && detail::ElementAccepts(ulJSValueGetTypedArrayType(value.raw()),
223 detail::kTypedArrayTypeOf<T>)
224 ? value
225 : Value()) {}
226
227 ///
228 /// Create a typed array over part of an existing ArrayBuffer without copying. `buffer` can
229 /// still be detached.
230 ///
231 /// To create a new typed array, use Context::MakeTypedArray().
232 ///
233 /// @param buffer The ArrayBuffer to view.
234 ///
235 /// @param byte_offset The byte offset of the first element (must be a multiple of
236 /// `sizeof(T)`).
237 ///
238 /// @param length The number of elements (not bytes).
239 ///
240 /// @return Returns a new TypedArray (an empty wrapper if `byte_offset` isn't a multiple of
241 /// `sizeof(T)` or the range doesn't fit in `buffer`).
242 ///
243 static TypedArray Create(const ArrayBuffer& buffer, size_t byte_offset, size_t length) {
244 ULJSContext ctx = ulJSValueGetContext(buffer.raw());
245 if (!ctx)
246 return TypedArray();
247 TypedArray result(Value::Adopt(ulCreateJSTypedArrayFromArrayBuffer(
248 ctx, detail::kTypedArrayTypeOf<T>, buffer.raw(), byte_offset, length)));
249 ulDestroyJSContext(ctx);
250 return result;
251 }
252
253 ///
254 /// Get a span over the elements without copying.
255 ///
256 /// The span stays valid while this wrapper lives and the page lives (see "Byte Lifetimes" on
257 /// js::ArrayBuffer for what ends it early).
258 ///
259 /// @return Returns the elements (an empty span if the array is detached or the wrapper is
260 /// empty).
261 ///
262 /// @note The ArrayBuffer can no longer be detached afterwards.
263 ///
264 std::span<T> view() const {
265 ULJSTypedArrayInfo info;
266 if (!ulJSTypedArrayGetInfo(raw(), &info) || !info.data
267 || !detail::ElementAccepts(info.type, detail::kTypedArrayTypeOf<T>))
268 return {}; // The element re-check guards wrappers reassigned through a Value&.
269 return std::span<T>(static_cast<T*>(info.data), info.length);
270 }
271
272 ///
273 /// Get a pointer to the first element. The ArrayBuffer can no longer be detached afterwards,
274 /// like view().
275 ///
276 /// @return Returns the pointer (NULL if the array is detached or the wrapper is empty).
277 ///
278 T* data() const { return view().data(); }
279
280 ///
281 /// Get the number of elements. The ArrayBuffer can still be detached.
282 ///
283 /// @return Returns the element count (0 if the array is detached or the wrapper is empty).
284 ///
285 size_t size() const {
286 // The element check guards wrappers reassigned through a Value&, as in view().
287 if (!detail::ElementAccepts(ulJSValueGetTypedArrayType(raw()), detail::kTypedArrayTypeOf<T>))
288 return 0;
289 return ulJSTypedArrayGetLength(raw());
290 }
291
292 ///
293 /// Get the ArrayBuffer this typed array views. The ArrayBuffer can still be detached.
294 ///
295 /// @return Returns the ArrayBuffer (an empty wrapper if this wrapper is empty).
296 ///
298 return ArrayBuffer(Value::Adopt(ulJSTypedArrayGetBuffer(raw())));
299 }
300};
301
302///
303/// Marshals RefPtr<Buffer> as a JavaScript ArrayBuffer without copying in either direction.
304///
305/// - **Returned to JavaScript**: a RefPtr<Buffer> becomes an ArrayBuffer, like
306/// Context::MakeArrayBuffer(). A null RefPtr becomes `null`.
307/// - **Received as a parameter**: an ArrayBuffer argument becomes a new Buffer that views its
308/// bytes, like ArrayBuffer::ShareBytes(). `null` becomes a null RefPtr. Any other argument (a
309/// detached ArrayBuffer or a typed array included) is rejected with a TypeError.
310///
311/// @note A received ArrayBuffer can no longer be detached afterwards, and its bytes can be
312/// freed when the page goes away even while you hold the Buffer (see js::ArrayBuffer).
313///
314// Buffer is binary data, not a bound class: this full specialization takes over from the
315// canonical-holder marshaling that a RefCounted RefPtr would otherwise select.
316template <>
318 static bool FromJS(ULJSContext, ULJSValue value, RefPtr<ultralight::Buffer>* out,
319 ULJSValue*) {
320 if (ulJSValueGetType(value) == kULJSType_Null) {
321 *out = nullptr;
322 return true;
323 }
324 ULBuffer handle = ulJSArrayBufferGetBuffer(value);
325 if (!handle)
326 return false;
327 *out = detail::AdoptULBuffer(handle);
328 return *out != nullptr;
329 }
330
331 static ULJSValue ToJS(ULJSContext ctx, const RefPtr<ultralight::Buffer>& value) {
332 return detail::WrapBufferThrough(ctx, value, [&](ULBuffer handle) {
333 return ulCreateJSArrayBufferFromBuffer(ctx, handle);
334 });
335 }
336
337 static constexpr const char* SchemaType() { return "ArrayBuffer"; }
338
339 // SchemaType() is TypeError-message casing; the schema expression is the lowercase token.
340 static void WriteSchemaExpr(std::string& out) { out += "\"arraybuffer\""; }
341};
342
343///
344/// Marshals a typed-array argument as a std::span<T> without copying.
345///
346/// A bound-function parameter of type std::span<T> (or std::span<const T>) accepts a typed
347/// array with elements of type `T` (a Uint8ClampedArray counts as `uint8_t`).
348/// std::span<uint8_t> and std::span<const uint8_t> also accept an ArrayBuffer. A detached
349/// argument gives an empty span, and any other argument is rejected with a TypeError.
350///
351/// The span is valid only until the bound function returns (for a js::Task coroutine, until
352/// the Task finishes), so never keep the span or its pointers. A span works only as a
353/// parameter: returning one, or using one where the value outlives the call (eg, an aggregate
354/// field, a container element, Value::To(), or js::API::Emit()), fails to compile. Copy the
355/// elements into a std::vector, or use RefPtr<Buffer> to share the bytes.
356///
357/// @note An accepted argument's ArrayBuffer can no longer be detached afterwards. A rejected
358/// argument's ArrayBuffer can still be detached.
359///
360template <typename T>
361struct TypeTraits<
362 std::span<T>,
363 std::enable_if_t<detail::kTypedArrayTypeOf<std::remove_const_t<T>>
364 != kULJSTypedArrayType_None>> {
365 private:
366 using Element = std::remove_const_t<T>;
367
368 public:
369 static bool FromJS(ULJSContext, ULJSValue value, std::span<T>* out, ULJSValue*) {
370 // Classify first, without pinning: reading element pointers pins the underlying
371 // ArrayBuffer permanently, and a rejected argument must leave the caller's buffer
372 // detachable/transferable.
373 ULJSTypedArrayType have = ulJSValueGetTypedArrayType(value);
374 if (have != kULJSTypedArrayType_None) {
375 if (!detail::ElementAccepts(have, detail::kTypedArrayTypeOf<Element>))
376 return false;
377 ULJSTypedArrayInfo info;
378 if (!ulJSTypedArrayGetInfo(value, &info))
379 return false;
380 // A detached view converts as an empty span (JavaScript length-0 semantics).
381 *out = info.data ? std::span<T>(static_cast<T*>(info.data), info.length)
382 : std::span<T>();
383 return true;
384 }
385 if constexpr (std::is_same_v<Element, uint8_t>) {
386 if (ulJSValueIsArrayBuffer(value)) {
387 size_t length = 0;
388 void* bytes = ulJSArrayBufferGetBytes(value, &length);
389 // A detached ArrayBuffer converts as an empty span, matching detached views.
390 *out = bytes ? std::span<T>(static_cast<T*>(bytes), length) : std::span<T>();
391 return true;
392 }
393 }
394 return false;
395 }
396
397 static constexpr const char* SchemaType() { return detail::TypedArrayNameOf<Element>(); }
398
399 // SchemaType() is TypeError-message casing ("Float32Array"); the schema expression is
400 // the typed-array composite with the lowercase element token.
401 static void WriteSchemaExpr(std::string& out) {
402 out += "{\"typedarray\":\"";
403 out += detail::TypedArrayElementToken<Element>();
404 out += "\"}";
405 }
406};
407
408} // namespace js
409} // namespace ultralight
struct C_JSContext * ULJSContext
Opaque handle to a JavaScript execution context (one page's script world in one View).
Definition View.h:25
A fixed-size container for raw byte data.
Definition Buffer.h:29
A nullable smart pointer.
Definition RefPtr.h:126
Handle to a JavaScript ArrayBuffer for sharing raw binary memory with the page.
Definition Buffer.h:88
ArrayBuffer(const Value &value)
Wrap a value if it's an ArrayBuffer (detached ones included), otherwise create an empty wrapper.
Definition Buffer.h:101
bool Detach() const
Detach this ArrayBuffer so JavaScript can no longer reach its bytes.
Definition Buffer.h:148
ArrayBuffer()=default
Create an empty ArrayBuffer wrapper.
RefPtr< ultralight::Buffer > ShareBytes() const
Share this ArrayBuffer's bytes with native code without copying.
Definition Buffer.h:124
size_t byte_length() const
Get the length in bytes.
Definition Buffer.h:134
TypedArray(const Value &value)
Wrap a value if it's a typed array with elements of type T, otherwise create an empty wrapper.
Definition Buffer.h:220
size_t size() const
Get the number of elements.
Definition Buffer.h:285
T * data() const
Get a pointer to the first element.
Definition Buffer.h:278
std::span< T > view() const
Get a span over the elements without copying.
Definition Buffer.h:264
static TypedArray Create(const ArrayBuffer &buffer, size_t byte_offset, size_t length)
Create a typed array over part of an existing ArrayBuffer without copying.
Definition Buffer.h:243
ArrayBuffer buffer() const
Get the ArrayBuffer this typed array views.
Definition Buffer.h:297
TypedArray()=default
Create an empty TypedArray wrapper.
static Value Adopt(ULJSValue handle)
Take ownership of a handle from the C API without adding a reference.
Definition Value.h:799
Value()
Create an empty Value.
Definition Value.h:215
ULJSValue raw() const
Get the C API handle without transferring ownership.
Definition Value.h:820
Definition StringSTL.h:166
Type-checked JavaScript bridge between C++ and web pages.
Definition JSInterop.h:141
Root namespace for every public Ultralight type, function, and enumeration.
static void WriteSchemaExpr(std::string &out)
Definition Buffer.h:340
static ULJSValue ToJS(ULJSContext ctx, const RefPtr< ultralight::Buffer > &value)
Definition Buffer.h:331
static bool FromJS(ULJSContext, ULJSValue value, RefPtr< ultralight::Buffer > *out, ULJSValue *)
Definition Buffer.h:318
static constexpr const char * SchemaType()
Definition Buffer.h:337
Type conversions between C++ and JavaScript across the bridge.
Definition TypeTraits.h:240