Skip to content

Add a helper method to copy an array of numbers to a JS TypedArray #31

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 26 commits into from
Sep 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fe68a31
Add a helper method to copy an array of numbers to a JS TypedArray
j-f1 Aug 3, 2020
0cb3f65
_copy_typed_array_content → _create_typed_array
j-f1 Aug 4, 2020
3eca18c
Merge remote-tracking branch 'upstream/master' into typed-array
j-f1 Aug 12, 2020
bff8568
Add globalVariable
j-f1 Aug 12, 2020
136315f
Remove broken test target
j-f1 Aug 13, 2020
5b875b2
Create JSTypedArray
j-f1 Aug 13, 2020
af57583
Reduce to just a single class
j-f1 Aug 13, 2020
e71fc56
Clean up types
j-f1 Aug 13, 2020
17d83f5
Fix tests
j-f1 Aug 13, 2020
64342d2
Formatting
j-f1 Aug 13, 2020
ab974af
Test all the array types
j-f1 Aug 13, 2020
0928da8
Fix test error
j-f1 Aug 13, 2020
a1f5b03
Add a test("name") { ... } helper that makes it easy to find out whic…
j-f1 Aug 13, 2020
7971185
Rename allocHeap and freeHeap to retain/release
j-f1 Aug 13, 2020
dde8cf2
Propagate names through to the Swift side
j-f1 Aug 13, 2020
74610c2
Add an explicit retain() function and fix a ref counting bug
j-f1 Aug 13, 2020
14ab088
Add error when reading invalid reference
j-f1 Aug 13, 2020
b6602c8
Actually fix the tests
j-f1 Aug 13, 2020
561b8a6
Explain why _retain is necessary
j-f1 Aug 14, 2020
b0ff949
Update _CJavaScriptKit.h
j-f1 Aug 14, 2020
7836ac2
Merge remote-tracking branch 'upstream/master' into typed-array
j-f1 Aug 22, 2020
e0ef55f
Merge branch 'master' into typed-array-change-proposal
kateinoigakukun Sep 9, 2020
d64def7
Remove manual reference counting
kateinoigakukun Sep 9, 2020
fab45e1
Fix test cases
kateinoigakukun Sep 9, 2020
f83a84c
Expose failable initializer
kateinoigakukun Sep 10, 2020
2370a1f
Merge pull request #1 from kateinoigakukun/typed-array-change-proposal
j-f1 Sep 10, 2020
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
53 changes: 53 additions & 0 deletions IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,56 @@ try test("Closure Identifiers") {
let oid2 = closureScope()
try expectEqual(oid1, oid2)
}

func checkArray<T>(_ array: [T]) throws where T: TypedArrayElement {
try expectEqual(toString(JSTypedArray(array).jsValue().object!), jsStringify(array))
}

func toString<T: JSObject>(_ 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<Int>(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)))
}
86 changes: 67 additions & 19 deletions Runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ type pointer = number;
interface GlobalVariable { }
declare const window: GlobalVariable;
declare const global: GlobalVariable;
let globalVariable: any;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this variable can be capsulized in SwiftRuntimeHeap.

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;
Expand All @@ -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,
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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)
}
}
}
Expand Down
136 changes: 136 additions & 0 deletions Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift
Original file line number Diff line number Diff line change
@@ -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<Element>: 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<Element>) 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<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: 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 }
}
5 changes: 3 additions & 2 deletions Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -47,3 +47,4 @@ public class JSObject: Equatable {
.object(self)
}
}

4 changes: 4 additions & 0 deletions Sources/JavaScriptKit/JSValueConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)) }
}
Expand Down
Loading