Uh oh!
There was an error while loading. Please reload this page.
JSTypedArray: annotate swjs_create_typed_array for bounds-safety - #749
JSTypedArray: annotate swjs_create_typed_array for bounds-safety#749airspeedswift wants to merge 1 commit into
Conversation
30bb265 to
34ab30aCompareairspeedswift
commented
May 24, 2026
removed the guard checking for clang headers, that's probably overkill if you assume the minimum tools version, but can put it back if this repo's practice prefers it. |
…ributes 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<Element>.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 `<ptrcheck.h>` and `<lifetimebound.h>` resource headers, both included explicitly: Apple's `<sys/cdefs.h>` pulls in `<ptrcheck.h>` 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<Element>.stride)` as the new argument; the JS dispatch entry is unchanged — extra WASM arguments are silently dropped.
34ab30a to
01d7a52Compareairspeedswift
commented
May 24, 2026
hmm guess I was wrong about ptrcheck being quite so available... |
airspeedswift
commented
May 24, 2026
Moving this to draft pending decision about how best to handle 6.1 toolchain support. Also needs some tests for calling the safe version. |
krodak
commented
Jun 24, 2026
@airspeedswift this should be safe to rebase now. The red checks here are from the old CI matrix. The failures were just the bounds-safety headers not shipping in the 6.1/6.2 clang - One thing to pair with the rebase: |
On recent clang/Swift toolchains, bounds-safety annotations on
void *parameters cause the Swift importer to synthesize anUnsafeRawBufferPointer-taking overload alongside the original. Adding these annotations toswjs_create_typed_arraygives us that overload for free, and it lays the foundation for broaderSpanadoption in JavaScriptKit in follow-up PRs.Because
elements_ptrisvoid *, the byte count islength * sizeof(element). This PR adds a trailingelement_sizeparameter and annotateselements_ptras__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 atelementsPtrwithlengthelements and immediately calls.slice()to copy before returning. The view constructor reads exactlylength * constructor.BYTES_PER_ELEMENTbytes, which equalslength * element_sizesince Swift passesMemoryLayout<Element>.stride— matchingBYTES_PER_ELEMENTfor everyTypedArrayElementtype. And because only the.slice()copy is retained and returned,elementsPtrnever escapes the call.__sized_by_or_nullrather than__sized_bypreserves the existing nullable contract: Swift'sArray.baseAddressisnilfor empty arrays, and the JS side already handleslength == 0without dereferencing the pointer.The only coordinated change is in
JSTypedArray.swift'screateTypedArray, which passesInt32(MemoryLayout<Element>.stride)as the new argument; the JS dispatch entry is unchanged — extra WASM arguments are silently dropped.The alternative is to redefine length as a byte count and drop element_size, collapsing the bounds expression to
__sized_by_or_null(byte_length);– this is a narrower change that doesn't update the typescript, but you could argue the alternative is cleaner.