From 01d7a5284f88236f991d1f74cb3480ca223a0811 Mon Sep 17 00:00:00 2001 From: Ben Cohen Date: Sun, 24 May 2026 09:27:36 -0700 Subject: [PATCH] JSTypedArray: annotate swjs_create_typed_array with bounds-safety attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On recent clang/Swift toolchains, bounds-safety annotations on `void *` parameters cause the Swift importer to synthesize an `UnsafeRawBufferPointer`-taking overload alongside the original. Adding these annotations to `swjs_create_typed_array` gives us that overload for free, and it lays the foundation for broader `Span` adoption in JavaScriptKit in follow-up PRs. Because `elements_ptr` is `void *`, the byte count is `length * sizeof(element)`. This PR adds a trailing `element_size` parameter and annotates `elements_ptr` as `__sized_by_or_null(length * element_size) __noescape`. Both annotations are correct given the JS implementation: it constructs a typed array view over WASM linear memory at `elementsPtr` with `length` elements and immediately calls `.slice()` to copy before returning. The view constructor reads exactly `length * constructor.BYTES_PER_ELEMENT` bytes, which equals `length * element_size` since Swift passes `MemoryLayout.stride` — matching `BYTES_PER_ELEMENT` for every `TypedArrayElement` type. And because only the `.slice()` copy is retained and returned, `elementsPtr` never escapes the call. `__sized_by_or_null` rather than `__sized_by` preserves the existing nullable contract: Swift's `Array.baseAddress` is `nil` for empty arrays, and the JS side already handles `length == 0` without dereferencing the pointer. The macros are defined by clang's `` and `` resource headers, both included explicitly: Apple's `` pulls in `` transitively, but Linux's does not, so direct inclusion is required for cross-platform consistency. The only coordinated change is in `JSTypedArray.swift`'s `createTypedArray`, which passes `Int32(MemoryLayout.stride)` as the new argument; the JS dispatch entry is unchanged — extra WASM arguments are silently dropped. --- .../BasicObjects/JSTypedArray.swift | 7 ++++++- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 19 +++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index 4717b6705..91eca7cdf 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -68,7 +68,12 @@ public final class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiter private static func createTypedArray(from buffer: UnsafeBufferPointer) -> JSObject { // Retain the constructor function to avoid it being released before calling `swjs_create_typed_array` let jsArrayRef = withExtendedLifetime(Self.constructor!) { ctor in - swjs_create_typed_array(ctor.id, buffer.baseAddress, Int32(buffer.count)) + swjs_create_typed_array( + ctor.id, + buffer.baseAddress, + Int32(buffer.count), + Int32(MemoryLayout.stride) + ) } return JSObject(id: jsArrayRef) } diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 3800a6d9e..63cfff92c 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -8,6 +8,8 @@ #endif #include #include +#include +#include /// `JavaScriptObjectRef` represents JavaScript object reference that is referenced by Swift side. /// This value is an address of `SwiftRuntimeHeap`. @@ -291,12 +293,21 @@ IMPORT_JS_FUNCTION(swjs_create_oneshot_function, JavaScriptObjectRef, (const Jav /// This is used to provide an efficient way to create `TypedArray`. /// /// @param constructor The `TypedArray` constructor. -/// @param elements_ptr The elements pointer to initialize. They are assumed to be the same size of `constructor` elements size. -/// @param length The length of `elements_ptr` +/// @param elements_ptr The elements to copy into the new typed array. May be NULL +/// when `length` is 0 (Swift's `Array.baseAddress` is NULL for +/// empty arrays). The pointee bytes are copied; the pointer +/// does not escape the call. +/// @param length The number of elements at `elements_ptr`. +/// @param element_size The size in bytes of each element. Should match the byte +/// width of `constructor`'s element type +/// (`constructor.BYTES_PER_ELEMENT`). The JS side ignores this +/// value; it exists so clang's bounds-safety analyzer can +/// express the buffer size as `length * element_size`. /// @returns A reference to the constructed typed array IMPORT_JS_FUNCTION(swjs_create_typed_array, JavaScriptObjectRef, (const JavaScriptObjectRef constructor, - const void * _Nullable elements_ptr, - const int length)) + const void * _Nullable __sized_by_or_null(length * element_size) __noescape elements_ptr, + const int length, + const int element_size)) /// Copies the byte contents of a typed array into a Swift side memory buffer. ///