diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 7d041a631..292e6f3a5 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -341,3 +341,56 @@ try test("Closure Identifiers") { let oid2 = closureScope() try expectEqual(oid1, oid2) } + +func checkArray(_ array: [T]) throws where T: TypedArrayElement { + try expectEqual(toString(JSTypedArray(array).jsValue().object!), jsStringify(array)) +} + +func toString(_ object: T) -> String { + return object.toString!().string! +} + +func jsStringify(_ array: [Any]) -> String { + array.map({ String(describing: $0) }).joined(separator: ",") +} + +try test("TypedArray") { + let numbers = [UInt8](0 ... 255) + let typedArray = JSTypedArray(numbers) + try expectEqual(typedArray[12], 12) + + try checkArray([0, .max, 127, 1] as [UInt8]) + try checkArray([0, 1, .max, .min, -1] as [Int8]) + + try checkArray([0, .max, 255, 1] as [UInt16]) + try checkArray([0, 1, .max, .min, -1] as [Int16]) + + try checkArray([0, .max, 255, 1] as [UInt32]) + try checkArray([0, 1, .max, .min, -1] as [Int32]) + + try checkArray([0, .max, 255, 1] as [UInt]) + try checkArray([0, 1, .max, .min, -1] as [Int]) + + let float32Array: [Float32] = [0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, .leastNormalMagnitude, 42] + let jsFloat32Array = JSTypedArray(float32Array) + for (i, num) in float32Array.enumerated() { + try expectEqual(num, jsFloat32Array[i]) + } + + let float64Array: [Float64] = [0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, .leastNormalMagnitude, 42] + let jsFloat64Array = JSTypedArray(float64Array) + for (i, num) in float64Array.enumerated() { + try expectEqual(num, jsFloat64Array[i]) + } +} + +try test("TypedArray_Mutation") { + let array = JSTypedArray(length: 100) + for i in 0..<100 { + array[i] = i + } + for i in 0..<100 { + try expectEqual(i, array[i]) + } + try expectEqual(toString(array.jsValue().object!), jsStringify(Array(0..<100))) +} diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 94e436aed..d75035cbf 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -8,6 +8,17 @@ type pointer = number; interface GlobalVariable { } declare const window: GlobalVariable; declare const global: GlobalVariable; +let globalVariable: any; +if (typeof globalThis !== "undefined") { + globalVariable = globalThis +} else if (typeof window !== "undefined") { + globalVariable = window +} else if (typeof global !== "undefined") { + globalVariable = global +} else if (typeof self !== "undefined") { + globalVariable = self +} + interface SwiftRuntimeExportedFunctions { swjs_library_version(): number; @@ -31,6 +42,32 @@ enum JavaScriptValueKind { Function = 6, } +enum JavaScriptTypedArrayKind { + Int8 = 0, + Uint8 = 1, + Int16 = 2, + Uint16 = 3, + Int32 = 4, + Uint32 = 5, + BigInt64 = 6, + BigUint64 = 7, + Float32 = 8, + Float64 = 9, +} + +type TypedArray = + | Int8ArrayConstructor + | Uint8ArrayConstructor + | Int16ArrayConstructor + | Uint16ArrayConstructor + | Int32ArrayConstructor + | Uint32ArrayConstructor + // | BigInt64ArrayConstructor + // | BigUint64ArrayConstructor + | Float32ArrayConstructor + | Float64ArrayConstructor + + type SwiftRuntimeHeapEntry = { id: number, rc: number, @@ -41,23 +78,17 @@ class SwiftRuntimeHeap { private _heapNextKey: number; constructor() { - let _global: any; - if (typeof window !== "undefined") { - _global = window - } else if (typeof global !== "undefined") { - _global = global - } this._heapValueById = new Map(); - this._heapValueById.set(0, _global); + this._heapValueById.set(0, globalVariable); this._heapEntryByValue = new Map(); - this._heapEntryByValue.set(_global, { id: 0, rc: 1 }); + this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 }); // Note: 0 is preserved for global this._heapNextKey = 1; } - allocHeap(value: any) { + retain(value: any) { const isObject = typeof value == "object"; const entry = this._heapEntryByValue.get(value); if (isObject && entry) { @@ -72,7 +103,7 @@ class SwiftRuntimeHeap { return id } - freeHeap(ref: ref) { + release(ref: ref) { const value = this._heapValueById.get(ref); const isObject = typeof value == "object" if (isObject) { @@ -88,7 +119,11 @@ class SwiftRuntimeHeap { } referenceHeap(ref: ref) { - return this._heapValueById.get(ref) + const value = this._heapValueById.get(ref) + if (value === undefined) { + throw new ReferenceError("Attempted to read invalid reference " + ref) + } + return value } } @@ -130,7 +165,7 @@ export class SwiftRuntime { writeValue(argument, base, base + 4, base + 8, base + 16) } let output: any; - const callback_func_ref = this.heap.allocHeap(function (result: any) { + const callback_func_ref = this.heap.retain(function (result: any) { output = result }) exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref) @@ -241,7 +276,7 @@ export class SwiftRuntime { case "string": { const bytes = textEncoder.encode(value); writeUint32(kind_ptr, JavaScriptValueKind.String); - writeUint32(payload1_ptr, this.heap.allocHeap(bytes)); + writeUint32(payload1_ptr, this.heap.retain(bytes)); writeUint32(payload2_ptr, bytes.length); break; } @@ -253,13 +288,13 @@ export class SwiftRuntime { } case "object": { writeUint32(kind_ptr, JavaScriptValueKind.Object); - writeUint32(payload1_ptr, this.heap.allocHeap(value)); + writeUint32(payload1_ptr, this.heap.retain(value)); writeUint32(payload2_ptr, 0); break; } case "function": { writeUint32(kind_ptr, JavaScriptValueKind.Function); - writeUint32(payload1_ptr, this.heap.allocHeap(value)); + writeUint32(payload1_ptr, this.heap.retain(value)); writeUint32(payload2_ptr, 0); break; } @@ -347,7 +382,7 @@ export class SwiftRuntime { host_func_id: number, func_ref_ptr: pointer, ) => { - const func_ref = this.heap.allocHeap(function () { + const func_ref = this.heap.retain(function () { return callHostFunction(host_func_id, Array.prototype.slice.call(arguments)) }) writeUint32(func_ref_ptr, func_ref) @@ -360,7 +395,7 @@ export class SwiftRuntime { const result = Reflect.construct(obj, decodeValues(argv, argc)) if (typeof result != "object") throw Error(`Invalid result type of object constructor of "${obj}": "${result}"`) - writeUint32(result_obj, this.heap.allocHeap(result)); + writeUint32(result_obj, this.heap.retain(result)); }, swjs_instanceof: ( obj_ref: ref, constructor_ref: ref, @@ -370,8 +405,21 @@ export class SwiftRuntime { const constructor = this.heap.referenceHeap(constructor_ref) return obj instanceof constructor }, - swjs_destroy_ref: (ref: ref) => { - this.heap.freeHeap(ref) + swjs_create_typed_array: ( + kind: JavaScriptTypedArrayKind, + elementsPtr: pointer, length: number, + result_obj: pointer + ) => { + const ArrayType: TypedArray = globalVariable[JavaScriptTypedArrayKind[kind] + 'Array'] + const array = new ArrayType(memory().buffer, elementsPtr, length); + // Call `.slice()` to copy the memory + writeUint32(result_obj, this.heap.retain(array.slice())); + }, + swjs_retain: (ref: ref) => { + this.heap.retain(this.heap.referenceHeap(ref)) + }, + swjs_release: (ref: ref) => { + this.heap.release(ref) } } } diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift new file mode 100644 index 000000000..beb430a5f --- /dev/null +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -0,0 +1,136 @@ +// +// Created by Manuel Burghard. Licensed unter MIT. +// + +import _CJavaScriptKit + +public protocol TypedArrayElement: JSValueConvertible, JSValueConstructible { + static var typedArrayKind: JavaScriptTypedArrayKind { get } + static var typedArrayClass: JSFunctionRef { get } +} + +public class JSTypedArray: JSValueConvertible, ExpressibleByArrayLiteral where Element: TypedArrayElement { + let ref: JSObject + public func jsValue() -> JSValue { + .object(ref) + } + + public subscript(_ index: Int) -> Element { + get { + return Element.construct(from: getJSValue(this: ref, index: Int32(index)))! + } + set { + setJSValue(this: ref, index: Int32(index), value: newValue.jsValue()) + } + } + + // This private initializer assumes that the passed object is TypedArray + private init(unsafe object: JSObject) { + self.ref = object + } + + public init?(_ object: JSObject) { + guard object.isInstanceOf(Element.typedArrayClass) else { return nil } + self.ref = object + } + + public convenience init(length: Int) { + let jsObject = Element.typedArrayClass.new(length) + self.init(unsafe: jsObject) + } + + required public convenience init(arrayLiteral elements: Element...) { + self.init(elements) + } + + public convenience init(_ array: [Element]) { + var resultObj = JavaScriptObjectRef() + array.withUnsafeBufferPointer { ptr in + _create_typed_array(Element.typedArrayKind, ptr.baseAddress!, Int32(array.count), &resultObj) + } + self.init(unsafe: JSObject(id: resultObj)) + } + + public convenience init(_ stride: StrideTo) where Element: Strideable { + self.init(stride.map({ $0 })) + } +} + +// MARK: - Int and UInt support + +// FIXME: Should be updated to support wasm64 when that becomes available. +func valueForBitWidth(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: JSFunctionRef { + valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObjectRef.global.Int32Array).function! + } + public static var typedArrayKind: JavaScriptTypedArrayKind { + valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: .int32) + } +} +extension UInt: TypedArrayElement { + public static var typedArrayClass: JSFunctionRef { + valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObjectRef.global.Uint32Array).function! + } + public static var typedArrayKind: JavaScriptTypedArrayKind { + valueForBitWidth(typeName: "UInt", bitWidth: UInt.bitWidth, when32: .uint32) + } +} + +// MARK: - Concrete TypedArray classes + +extension Int8: TypedArrayElement { + public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Int8Array.function! } + public static var typedArrayKind: JavaScriptTypedArrayKind { .int8 } +} +extension UInt8: TypedArrayElement { + public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Uint8Array.function! } + public static var typedArrayKind: JavaScriptTypedArrayKind { .uint8 } +} +// TODO: Support Uint8ClampedArray? + +extension Int16: TypedArrayElement { + public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Int16Array.function! } + public static var typedArrayKind: JavaScriptTypedArrayKind { .int16 } +} +extension UInt16: TypedArrayElement { + public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Uint16Array.function! } + public static var typedArrayKind: JavaScriptTypedArrayKind { .uint16 } +} + +extension Int32: TypedArrayElement { + public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Int32Array.function! } + public static var typedArrayKind: JavaScriptTypedArrayKind { .int32 } +} +extension UInt32: TypedArrayElement { + public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Uint32Array.function! } + public static var typedArrayKind: JavaScriptTypedArrayKind { .uint32 } +} + +// FIXME: Support passing BigInts across the bridge +//extension Int64: TypedArrayElement { +// public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.BigInt64Array.function! } +// public static var type: JavaScriptTypedArrayKind { .bigInt64 } +//} +//extension UInt64: TypedArrayElement { +// public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.BigUint64Array.function! } +// public static var type: JavaScriptTypedArrayKind { .bigUint64 } +//} + +extension Float32: TypedArrayElement { + public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Float32Array.function! } + public static var typedArrayKind: JavaScriptTypedArrayKind { .float32 } +} +extension Float64: TypedArrayElement { + public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Float64Array.function! } + public static var typedArrayKind: JavaScriptTypedArrayKind { .float64 } +} diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 5955aa536..076367a5d 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -34,10 +34,10 @@ public class JSObject: Equatable { _instanceof(id, constructor.id) } - static let _JS_Predef_Value_Global: UInt32 = 0 + static let _JS_Predef_Value_Global: JavaScriptObjectRef = 0 public static let global = JSObject(id: _JS_Predef_Value_Global) - deinit { _destroy_ref(id) } + deinit { _release(id) } public static func == (lhs: JSObject, rhs: JSObject) -> Bool { return lhs.id == rhs.id @@ -47,3 +47,4 @@ public class JSObject: Equatable { .object(self) } } + diff --git a/Sources/JavaScriptKit/JSValueConvertible.swift b/Sources/JavaScriptKit/JSValueConvertible.swift index eb4e27452..bc3b3491e 100644 --- a/Sources/JavaScriptKit/JSValueConvertible.swift +++ b/Sources/JavaScriptKit/JSValueConvertible.swift @@ -40,6 +40,10 @@ extension UInt16: JSValueConvertible { public func jsValue() -> JSValue { .number(Double(self)) } } +extension UInt32: JSValueConvertible { + public func jsValue() -> JSValue { .number(Double(self)) } +} + extension Float: JSValueConvertible { public func jsValue() -> JSValue { .number(Double(self)) } } diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift index 4f45d907f..ed42417c8 100644 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ b/Sources/JavaScriptKit/XcodeSupport.swift @@ -72,5 +72,12 @@ import _CJavaScriptKit _: JavaScriptHostFuncRef, _: UnsafePointer! ) { fatalError() } - func _destroy_ref(_: JavaScriptObjectRef) { fatalError() } + func _release(_: JavaScriptObjectRef) { fatalError() } + func _create_typed_array( + _: JavaScriptTypedArrayKind, + _: UnsafePointer, + _: Int32, + _: UnsafeMutablePointer! + ) { fatalError() } + #endif diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 61789e16c..9bdfc9b63 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -18,6 +18,19 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindFunction = 6, } JavaScriptValueKind; +typedef enum __attribute__((enum_extensibility(closed))) { + JavaScriptTypedArrayKindInt8 = 0, + JavaScriptTypedArrayKindUint8 = 1, + JavaScriptTypedArrayKindInt16 = 2, + JavaScriptTypedArrayKindUint16 = 3, + JavaScriptTypedArrayKindInt32 = 4, + JavaScriptTypedArrayKindUint32 = 5, + JavaScriptTypedArrayKindBigInt64 = 6, + JavaScriptTypedArrayKindBigUint64 = 7, + JavaScriptTypedArrayKindFloat32 = 8, + JavaScriptTypedArrayKindFloat64 = 9, +} JavaScriptTypedArrayKind; + typedef unsigned JavaScriptPayload1; typedef unsigned JavaScriptPayload2; typedef double JavaScriptPayload3; @@ -94,8 +107,14 @@ _create_function(const JavaScriptHostFuncRef host_func_id, const JavaScriptObjectRef *func_ref_ptr); __attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_destroy_ref"))) extern void -_destroy_ref(const JavaScriptObjectRef ref); + __import_name__("swjs_release"))) extern void +_release(const JavaScriptObjectRef ref); + +__attribute__((__import_module__("javascript_kit"), + __import_name__("swjs_create_typed_array"))) extern void +_create_typed_array(const JavaScriptTypedArrayKind kind, + const void *elementsPtr, const int length, + JavaScriptObjectRef *result_obj); #endif