Skip to content

Unlock JSTypedArray for Embedded Swift #317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions Examples/Embedded/Sources/EmbeddedApp/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,49 @@ var divElement = document.createElement("div")
divElement.innerText = .string("Count \(count)")
_ = document.body.appendChild(divElement)

var buttonElement = document.createElement("button")
buttonElement.innerText = "Click me"
buttonElement.onclick = JSValue.object(
var clickMeElement = document.createElement("button")
clickMeElement.innerText = "Click me"
clickMeElement.onclick = JSValue.object(
JSClosure { _ in
count += 1
divElement.innerText = .string("Count \(count)")
return .undefined
}
)
_ = document.body.appendChild(clickMeElement)

_ = document.body.appendChild(buttonElement)
var encodeResultElement = document.createElement("pre")
var textInputElement = document.createElement("input")
textInputElement.type = "text"
textInputElement.placeholder = "Enter text to encode to UTF-8"
textInputElement.oninput = JSValue.object(
JSClosure { _ in
let textEncoder = JSObject.global.TextEncoder.function!.new()
let encode = textEncoder.encode.function!
let encodedData = JSTypedArray<UInt8>(
unsafelyWrapping: encode(this: textEncoder, textInputElement.value).object!
)
encodeResultElement.innerText = .string(
encodedData.withUnsafeBytes { bytes in
bytes.map { hex($0) }.joined(separator: " ")
}
)
return .undefined
}
)
let encoderContainer = document.createElement("div")
_ = encoderContainer.appendChild(textInputElement)
_ = encoderContainer.appendChild(encodeResultElement)
_ = document.body.appendChild(encoderContainer)

func print(_ message: String) {
_ = JSObject.global.console.log(message)
}

func hex(_ value: UInt8) -> String {
var result = "0x"
let hexChars: [Character] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
result.append(hexChars[Int(value / 16)])
result.append(hexChars[Int(value % 16)])
return result
}
65 changes: 30 additions & 35 deletions Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
//
// Created by Manuel Burghard. Licensed unter MIT.
//
#if !hasFeature(Embedded)
import _CJavaScriptKit

/// A protocol that allows a Swift numeric type to be mapped to the JavaScript TypedArray that holds integers of its type
public protocol TypedArrayElement: ConvertibleToJSValue, ConstructibleFromJSValue {
public protocol TypedArrayElement {
associatedtype Element: ConvertibleToJSValue, ConstructibleFromJSValue = Self
/// The constructor function for the TypedArray class for this particular kind of number
static var typedArrayClass: JSFunction { get }
}

/// A wrapper around all [JavaScript `TypedArray`
/// classes](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)
/// that exposes their properties in a type-safe way.
public class JSTypedArray<Element>: JSBridgedClass, ExpressibleByArrayLiteral where Element: TypedArrayElement {
public class var constructor: JSFunction? { Element.typedArrayClass }
public final class JSTypedArray<Traits>: JSBridgedClass, ExpressibleByArrayLiteral where Traits: TypedArrayElement {
public typealias Element = Traits.Element
public class var constructor: JSFunction? { Traits.typedArrayClass }
public var jsObject: JSObject

public subscript(_ index: Int) -> Element {
Expand Down Expand Up @@ -139,33 +140,28 @@ public class JSTypedArray<Element>: JSBridgedClass, ExpressibleByArrayLiteral wh
}
}

// MARK: - Int and UInt support

// FIXME: Should be updated to support wasm64 when that becomes available.
func valueForBitWidth<T>(typeName: String, bitWidth: Int, when32: T) -> T {
if bitWidth == 32 {
return when32
} else if bitWidth == 64 {
fatalError("64-bit \(typeName)s are not yet supported in JSTypedArray")
} else {
fatalError(
"Unsupported bit width for type \(typeName): \(bitWidth) (hint: stick to fixed-size \(typeName)s to avoid this issue)"
)
}
}

extension Int: TypedArrayElement {
public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue }
private static let _typedArrayClass = LazyThreadLocal(initialize: {
valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObject.global.Int32Array).function!
})
public static var typedArrayClass: JSFunction {
#if _pointerBitWidth(_32)
return JSObject.global.Int32Array.function!
#elseif _pointerBitWidth(_64)
return JSObject.global.Int64Array.function!
#else
#error("Unsupported pointer width")
#endif
}
}

extension UInt: TypedArrayElement {
public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue }
private static let _typedArrayClass = LazyThreadLocal(initialize: {
valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function!
})
public static var typedArrayClass: JSFunction {
#if _pointerBitWidth(_32)
return JSObject.global.Uint32Array.function!
#elseif _pointerBitWidth(_64)
return JSObject.global.Uint64Array.function!
#else
#error("Unsupported pointer width")
#endif
}
}

extension Int8: TypedArrayElement {
Expand All @@ -176,13 +172,6 @@ extension UInt8: TypedArrayElement {
public static var typedArrayClass: JSFunction { JSObject.global.Uint8Array.function! }
}

/// A wrapper around [the JavaScript `Uint8ClampedArray`
/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)
/// that exposes its properties in a type-safe and Swifty way.
public class JSUInt8ClampedArray: JSTypedArray<UInt8> {
override public class var constructor: JSFunction? { JSObject.global.Uint8ClampedArray.function! }
}

extension Int16: TypedArrayElement {
public static var typedArrayClass: JSFunction { JSObject.global.Int16Array.function! }
}
Expand All @@ -206,4 +195,10 @@ extension Float32: TypedArrayElement {
extension Float64: TypedArrayElement {
public static var typedArrayClass: JSFunction { JSObject.global.Float64Array.function! }
}
#endif

public enum JSUInt8Clamped: TypedArrayElement {
public typealias Element = UInt8
public static var typedArrayClass: JSFunction { JSObject.global.Uint8ClampedArray.function! }
}

public typealias JSUInt8ClampedArray = JSTypedArray<JSUInt8Clamped>
16 changes: 8 additions & 8 deletions Tests/JavaScriptKitTests/JSTypedArrayTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ final class JSTypedArrayTests: XCTestCase {
}

func testTypedArray() {
func checkArray<T>(_ array: [T]) where T: TypedArrayElement & Equatable {
XCTAssertEqual(toString(JSTypedArray(array).jsValue.object!), jsStringify(array))
func checkArray<T>(_ array: [T]) where T: TypedArrayElement & Equatable, T.Element == T {
XCTAssertEqual(toString(JSTypedArray<T>(array).jsValue.object!), jsStringify(array))
checkArrayUnsafeBytes(array)
}

Expand All @@ -30,20 +30,20 @@ final class JSTypedArrayTests: XCTestCase {
array.map({ String(describing: $0) }).joined(separator: ",")
}

func checkArrayUnsafeBytes<T>(_ array: [T]) where T: TypedArrayElement & Equatable {
let copyOfArray: [T] = JSTypedArray(array).withUnsafeBytes { buffer in
func checkArrayUnsafeBytes<T>(_ array: [T]) where T: TypedArrayElement & Equatable, T.Element == T {
let copyOfArray: [T] = JSTypedArray<T>(array).withUnsafeBytes { buffer in
Array(buffer)
}
XCTAssertEqual(copyOfArray, array)
}

let numbers = [UInt8](0...255)
let typedArray = JSTypedArray(numbers)
let typedArray = JSTypedArray<UInt8>(numbers)
XCTAssertEqual(typedArray[12], 12)
XCTAssertEqual(numbers.count, typedArray.lengthInBytes)

let numbersSet = Set(0...255)
let typedArrayFromSet = JSTypedArray(numbersSet)
let typedArrayFromSet = JSTypedArray<Int>(numbersSet)
XCTAssertEqual(typedArrayFromSet.jsObject.length, 256)
XCTAssertEqual(typedArrayFromSet.lengthInBytes, 256 * MemoryLayout<Int>.size)

Expand All @@ -63,7 +63,7 @@ final class JSTypedArrayTests: XCTestCase {
0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude,
.leastNormalMagnitude, 42,
]
let jsFloat32Array = JSTypedArray(float32Array)
let jsFloat32Array = JSTypedArray<Float32>(float32Array)
for (i, num) in float32Array.enumerated() {
XCTAssertEqual(num, jsFloat32Array[i])
}
Expand All @@ -72,7 +72,7 @@ final class JSTypedArrayTests: XCTestCase {
0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude,
.leastNormalMagnitude, 42,
]
let jsFloat64Array = JSTypedArray(float64Array)
let jsFloat64Array = JSTypedArray<Float64>(float64Array)
for (i, num) in float64Array.enumerated() {
XCTAssertEqual(num, jsFloat64Array[i])
}
Expand Down