Skip to content

JSTypedArray: annotate swjs_create_typed_array for bounds-safety#749

Draft
airspeedswift wants to merge 1 commit into
swiftwasm:mainfrom
airspeedswift:typed-array-bounds-safety
Draft

JSTypedArray: annotate swjs_create_typed_array for bounds-safety#749
airspeedswift wants to merge 1 commit into
swiftwasm:mainfrom
airspeedswift:typed-array-bounds-safety

Conversation

@airspeedswift
Copy link
Copy Markdown

@airspeedswift airspeedswift commented May 24, 2026

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 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.

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.

@airspeedswift airspeedswift force-pushed the typed-array-bounds-safety branch 2 times, most recently from 30bb265 to 34ab30a Compare May 24, 2026 17:07
@airspeedswift
Copy link
Copy Markdown
Author

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.
@airspeedswift airspeedswift force-pushed the typed-array-bounds-safety branch from 34ab30a to 01d7a52 Compare May 24, 2026 17:12
@airspeedswift
Copy link
Copy Markdown
Author

hmm guess I was wrong about ptrcheck being quite so available...

@airspeedswift airspeedswift marked this pull request as draft May 24, 2026 22:50
@airspeedswift
Copy link
Copy Markdown
Author

Moving this to draft pending decision about how best to handle 6.1 toolchain support. Also needs some tests for calling the safe version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant