Skip to content

Commit 171c391

Browse files
j-f1kateinoigakukun
andauthoredSep 10, 2020
Add a helper method to copy an array of numbers to a JS TypedArray (#31)
* Add a helper method to copy an array of numbers to a JS TypedArray * _copy_typed_array_content → _create_typed_array * Add globalVariable * Remove broken test target * Create JSTypedArray * Reduce to just a single class * Clean up types * Fix tests * Formatting * Test all the array types * Fix test error * Add a test("name") { ... } helper that makes it easy to find out which test errors on the JS side happen in * Rename allocHeap and freeHeap to retain/release This matches the Objective-C names and better corresponds with their tasks (freeHeap just decreases ref count so it doesn’t always free). I’m not as sure about allocHeap → retain because it adds to the heap in addition to increasing the refcount. * Propagate names through to the Swift side * Add an explicit retain() function and fix a ref counting bug * Add error when reading invalid reference Previously it would just return undefined which would throw an error when attempting to use the object later on. Now you’re able to demangle the stack trace to find exactly where you’re misusing a ref. * Actually fix the tests * Explain why _retain is necessary * Update _CJavaScriptKit.h * Remove manual reference counting * Fix test cases * Expose failable initializer Co-authored-by: Yuta Saito <kateinoigakukun@gmail.com>
1 parent 445ea5b commit 171c391

File tree

7 files changed

+292
-24
lines changed

7 files changed

+292
-24
lines changed
 

‎IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift

+53
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,56 @@ try test("Closure Identifiers") {
341341
let oid2 = closureScope()
342342
try expectEqual(oid1, oid2)
343343
}
344+
345+
func checkArray<T>(_ array: [T]) throws where T: TypedArrayElement {
346+
try expectEqual(toString(JSTypedArray(array).jsValue().object!), jsStringify(array))
347+
}
348+
349+
func toString<T: JSObject>(_ object: T) -> String {
350+
return object.toString!().string!
351+
}
352+
353+
func jsStringify(_ array: [Any]) -> String {
354+
array.map({ String(describing: $0) }).joined(separator: ",")
355+
}
356+
357+
try test("TypedArray") {
358+
let numbers = [UInt8](0 ... 255)
359+
let typedArray = JSTypedArray(numbers)
360+
try expectEqual(typedArray[12], 12)
361+
362+
try checkArray([0, .max, 127, 1] as [UInt8])
363+
try checkArray([0, 1, .max, .min, -1] as [Int8])
364+
365+
try checkArray([0, .max, 255, 1] as [UInt16])
366+
try checkArray([0, 1, .max, .min, -1] as [Int16])
367+
368+
try checkArray([0, .max, 255, 1] as [UInt32])
369+
try checkArray([0, 1, .max, .min, -1] as [Int32])
370+
371+
try checkArray([0, .max, 255, 1] as [UInt])
372+
try checkArray([0, 1, .max, .min, -1] as [Int])
373+
374+
let float32Array: [Float32] = [0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, .leastNormalMagnitude, 42]
375+
let jsFloat32Array = JSTypedArray(float32Array)
376+
for (i, num) in float32Array.enumerated() {
377+
try expectEqual(num, jsFloat32Array[i])
378+
}
379+
380+
let float64Array: [Float64] = [0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, .leastNormalMagnitude, 42]
381+
let jsFloat64Array = JSTypedArray(float64Array)
382+
for (i, num) in float64Array.enumerated() {
383+
try expectEqual(num, jsFloat64Array[i])
384+
}
385+
}
386+
387+
try test("TypedArray_Mutation") {
388+
let array = JSTypedArray<Int>(length: 100)
389+
for i in 0..<100 {
390+
array[i] = i
391+
}
392+
for i in 0..<100 {
393+
try expectEqual(i, array[i])
394+
}
395+
try expectEqual(toString(array.jsValue().object!), jsStringify(Array(0..<100)))
396+
}

‎Runtime/src/index.ts

+67-19
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ type pointer = number;
88
interface GlobalVariable { }
99
declare const window: GlobalVariable;
1010
declare const global: GlobalVariable;
11+
let globalVariable: any;
12+
if (typeof globalThis !== "undefined") {
13+
globalVariable = globalThis
14+
} else if (typeof window !== "undefined") {
15+
globalVariable = window
16+
} else if (typeof global !== "undefined") {
17+
globalVariable = global
18+
} else if (typeof self !== "undefined") {
19+
globalVariable = self
20+
}
21+
1122

1223
interface SwiftRuntimeExportedFunctions {
1324
swjs_library_version(): number;
@@ -31,6 +42,32 @@ enum JavaScriptValueKind {
3142
Function = 6,
3243
}
3344

45+
enum JavaScriptTypedArrayKind {
46+
Int8 = 0,
47+
Uint8 = 1,
48+
Int16 = 2,
49+
Uint16 = 3,
50+
Int32 = 4,
51+
Uint32 = 5,
52+
BigInt64 = 6,
53+
BigUint64 = 7,
54+
Float32 = 8,
55+
Float64 = 9,
56+
}
57+
58+
type TypedArray =
59+
| Int8ArrayConstructor
60+
| Uint8ArrayConstructor
61+
| Int16ArrayConstructor
62+
| Uint16ArrayConstructor
63+
| Int32ArrayConstructor
64+
| Uint32ArrayConstructor
65+
// | BigInt64ArrayConstructor
66+
// | BigUint64ArrayConstructor
67+
| Float32ArrayConstructor
68+
| Float64ArrayConstructor
69+
70+
3471
type SwiftRuntimeHeapEntry = {
3572
id: number,
3673
rc: number,
@@ -41,23 +78,17 @@ class SwiftRuntimeHeap {
4178
private _heapNextKey: number;
4279

4380
constructor() {
44-
let _global: any;
45-
if (typeof window !== "undefined") {
46-
_global = window
47-
} else if (typeof global !== "undefined") {
48-
_global = global
49-
}
5081
this._heapValueById = new Map();
51-
this._heapValueById.set(0, _global);
82+
this._heapValueById.set(0, globalVariable);
5283

5384
this._heapEntryByValue = new Map();
54-
this._heapEntryByValue.set(_global, { id: 0, rc: 1 });
85+
this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 });
5586

5687
// Note: 0 is preserved for global
5788
this._heapNextKey = 1;
5889
}
5990

60-
allocHeap(value: any) {
91+
retain(value: any) {
6192
const isObject = typeof value == "object";
6293
const entry = this._heapEntryByValue.get(value);
6394
if (isObject && entry) {
@@ -72,7 +103,7 @@ class SwiftRuntimeHeap {
72103
return id
73104
}
74105

75-
freeHeap(ref: ref) {
106+
release(ref: ref) {
76107
const value = this._heapValueById.get(ref);
77108
const isObject = typeof value == "object"
78109
if (isObject) {
@@ -88,7 +119,11 @@ class SwiftRuntimeHeap {
88119
}
89120

90121
referenceHeap(ref: ref) {
91-
return this._heapValueById.get(ref)
122+
const value = this._heapValueById.get(ref)
123+
if (value === undefined) {
124+
throw new ReferenceError("Attempted to read invalid reference " + ref)
125+
}
126+
return value
92127
}
93128
}
94129

@@ -130,7 +165,7 @@ export class SwiftRuntime {
130165
writeValue(argument, base, base + 4, base + 8, base + 16)
131166
}
132167
let output: any;
133-
const callback_func_ref = this.heap.allocHeap(function (result: any) {
168+
const callback_func_ref = this.heap.retain(function (result: any) {
134169
output = result
135170
})
136171
exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref)
@@ -241,7 +276,7 @@ export class SwiftRuntime {
241276
case "string": {
242277
const bytes = textEncoder.encode(value);
243278
writeUint32(kind_ptr, JavaScriptValueKind.String);
244-
writeUint32(payload1_ptr, this.heap.allocHeap(bytes));
279+
writeUint32(payload1_ptr, this.heap.retain(bytes));
245280
writeUint32(payload2_ptr, bytes.length);
246281
break;
247282
}
@@ -253,13 +288,13 @@ export class SwiftRuntime {
253288
}
254289
case "object": {
255290
writeUint32(kind_ptr, JavaScriptValueKind.Object);
256-
writeUint32(payload1_ptr, this.heap.allocHeap(value));
291+
writeUint32(payload1_ptr, this.heap.retain(value));
257292
writeUint32(payload2_ptr, 0);
258293
break;
259294
}
260295
case "function": {
261296
writeUint32(kind_ptr, JavaScriptValueKind.Function);
262-
writeUint32(payload1_ptr, this.heap.allocHeap(value));
297+
writeUint32(payload1_ptr, this.heap.retain(value));
263298
writeUint32(payload2_ptr, 0);
264299
break;
265300
}
@@ -347,7 +382,7 @@ export class SwiftRuntime {
347382
host_func_id: number,
348383
func_ref_ptr: pointer,
349384
) => {
350-
const func_ref = this.heap.allocHeap(function () {
385+
const func_ref = this.heap.retain(function () {
351386
return callHostFunction(host_func_id, Array.prototype.slice.call(arguments))
352387
})
353388
writeUint32(func_ref_ptr, func_ref)
@@ -360,7 +395,7 @@ export class SwiftRuntime {
360395
const result = Reflect.construct(obj, decodeValues(argv, argc))
361396
if (typeof result != "object")
362397
throw Error(`Invalid result type of object constructor of "${obj}": "${result}"`)
363-
writeUint32(result_obj, this.heap.allocHeap(result));
398+
writeUint32(result_obj, this.heap.retain(result));
364399
},
365400
swjs_instanceof: (
366401
obj_ref: ref, constructor_ref: ref,
@@ -370,8 +405,21 @@ export class SwiftRuntime {
370405
const constructor = this.heap.referenceHeap(constructor_ref)
371406
return obj instanceof constructor
372407
},
373-
swjs_destroy_ref: (ref: ref) => {
374-
this.heap.freeHeap(ref)
408+
swjs_create_typed_array: (
409+
kind: JavaScriptTypedArrayKind,
410+
elementsPtr: pointer, length: number,
411+
result_obj: pointer
412+
) => {
413+
const ArrayType: TypedArray = globalVariable[JavaScriptTypedArrayKind[kind] + 'Array']
414+
const array = new ArrayType(memory().buffer, elementsPtr, length);
415+
// Call `.slice()` to copy the memory
416+
writeUint32(result_obj, this.heap.retain(array.slice()));
417+
},
418+
swjs_retain: (ref: ref) => {
419+
this.heap.retain(this.heap.referenceHeap(ref))
420+
},
421+
swjs_release: (ref: ref) => {
422+
this.heap.release(ref)
375423
}
376424
}
377425
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//
2+
// Created by Manuel Burghard. Licensed unter MIT.
3+
//
4+
5+
import _CJavaScriptKit
6+
7+
public protocol TypedArrayElement: JSValueConvertible, JSValueConstructible {
8+
static var typedArrayKind: JavaScriptTypedArrayKind { get }
9+
static var typedArrayClass: JSFunctionRef { get }
10+
}
11+
12+
public class JSTypedArray<Element>: JSValueConvertible, ExpressibleByArrayLiteral where Element: TypedArrayElement {
13+
let ref: JSObject
14+
public func jsValue() -> JSValue {
15+
.object(ref)
16+
}
17+
18+
public subscript(_ index: Int) -> Element {
19+
get {
20+
return Element.construct(from: getJSValue(this: ref, index: Int32(index)))!
21+
}
22+
set {
23+
setJSValue(this: ref, index: Int32(index), value: newValue.jsValue())
24+
}
25+
}
26+
27+
// This private initializer assumes that the passed object is TypedArray
28+
private init(unsafe object: JSObject) {
29+
self.ref = object
30+
}
31+
32+
public init?(_ object: JSObject) {
33+
guard object.isInstanceOf(Element.typedArrayClass) else { return nil }
34+
self.ref = object
35+
}
36+
37+
public convenience init(length: Int) {
38+
let jsObject = Element.typedArrayClass.new(length)
39+
self.init(unsafe: jsObject)
40+
}
41+
42+
required public convenience init(arrayLiteral elements: Element...) {
43+
self.init(elements)
44+
}
45+
46+
public convenience init(_ array: [Element]) {
47+
var resultObj = JavaScriptObjectRef()
48+
array.withUnsafeBufferPointer { ptr in
49+
_create_typed_array(Element.typedArrayKind, ptr.baseAddress!, Int32(array.count), &resultObj)
50+
}
51+
self.init(unsafe: JSObject(id: resultObj))
52+
}
53+
54+
public convenience init(_ stride: StrideTo<Element>) where Element: Strideable {
55+
self.init(stride.map({ $0 }))
56+
}
57+
}
58+
59+
// MARK: - Int and UInt support
60+
61+
// FIXME: Should be updated to support wasm64 when that becomes available.
62+
func valueForBitWidth<T>(typeName: String, bitWidth: Int, when32: T) -> T {
63+
if bitWidth == 32 {
64+
return when32
65+
} else if bitWidth == 64 {
66+
fatalError("64-bit \(typeName)s are not yet supported in JSTypedArray")
67+
} else {
68+
fatalError("Unsupported bit width for type \(typeName): \(bitWidth) (hint: stick to fixed-size \(typeName)s to avoid this issue)")
69+
}
70+
}
71+
72+
extension Int: TypedArrayElement {
73+
public static var typedArrayClass: JSFunctionRef {
74+
valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObjectRef.global.Int32Array).function!
75+
}
76+
public static var typedArrayKind: JavaScriptTypedArrayKind {
77+
valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: .int32)
78+
}
79+
}
80+
extension UInt: TypedArrayElement {
81+
public static var typedArrayClass: JSFunctionRef {
82+
valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObjectRef.global.Uint32Array).function!
83+
}
84+
public static var typedArrayKind: JavaScriptTypedArrayKind {
85+
valueForBitWidth(typeName: "UInt", bitWidth: UInt.bitWidth, when32: .uint32)
86+
}
87+
}
88+
89+
// MARK: - Concrete TypedArray classes
90+
91+
extension Int8: TypedArrayElement {
92+
public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Int8Array.function! }
93+
public static var typedArrayKind: JavaScriptTypedArrayKind { .int8 }
94+
}
95+
extension UInt8: TypedArrayElement {
96+
public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Uint8Array.function! }
97+
public static var typedArrayKind: JavaScriptTypedArrayKind { .uint8 }
98+
}
99+
// TODO: Support Uint8ClampedArray?
100+
101+
extension Int16: TypedArrayElement {
102+
public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Int16Array.function! }
103+
public static var typedArrayKind: JavaScriptTypedArrayKind { .int16 }
104+
}
105+
extension UInt16: TypedArrayElement {
106+
public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Uint16Array.function! }
107+
public static var typedArrayKind: JavaScriptTypedArrayKind { .uint16 }
108+
}
109+
110+
extension Int32: TypedArrayElement {
111+
public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Int32Array.function! }
112+
public static var typedArrayKind: JavaScriptTypedArrayKind { .int32 }
113+
}
114+
extension UInt32: TypedArrayElement {
115+
public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Uint32Array.function! }
116+
public static var typedArrayKind: JavaScriptTypedArrayKind { .uint32 }
117+
}
118+
119+
// FIXME: Support passing BigInts across the bridge
120+
//extension Int64: TypedArrayElement {
121+
// public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.BigInt64Array.function! }
122+
// public static var type: JavaScriptTypedArrayKind { .bigInt64 }
123+
//}
124+
//extension UInt64: TypedArrayElement {
125+
// public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.BigUint64Array.function! }
126+
// public static var type: JavaScriptTypedArrayKind { .bigUint64 }
127+
//}
128+
129+
extension Float32: TypedArrayElement {
130+
public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Float32Array.function! }
131+
public static var typedArrayKind: JavaScriptTypedArrayKind { .float32 }
132+
}
133+
extension Float64: TypedArrayElement {
134+
public static var typedArrayClass: JSFunctionRef { JSObjectRef.global.Float64Array.function! }
135+
public static var typedArrayKind: JavaScriptTypedArrayKind { .float64 }
136+
}

‎Sources/JavaScriptKit/FundamentalObjects/JSObject.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ public class JSObject: Equatable {
3434
_instanceof(id, constructor.id)
3535
}
3636

37-
static let _JS_Predef_Value_Global: UInt32 = 0
37+
static let _JS_Predef_Value_Global: JavaScriptObjectRef = 0
3838
public static let global = JSObject(id: _JS_Predef_Value_Global)
3939

40-
deinit { _destroy_ref(id) }
40+
deinit { _release(id) }
4141

4242
public static func == (lhs: JSObject, rhs: JSObject) -> Bool {
4343
return lhs.id == rhs.id
@@ -47,3 +47,4 @@ public class JSObject: Equatable {
4747
.object(self)
4848
}
4949
}
50+

‎Sources/JavaScriptKit/JSValueConvertible.swift

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ extension UInt16: JSValueConvertible {
4040
public func jsValue() -> JSValue { .number(Double(self)) }
4141
}
4242

43+
extension UInt32: JSValueConvertible {
44+
public func jsValue() -> JSValue { .number(Double(self)) }
45+
}
46+
4347
extension Float: JSValueConvertible {
4448
public func jsValue() -> JSValue { .number(Double(self)) }
4549
}

‎Sources/JavaScriptKit/XcodeSupport.swift

+8-1
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,12 @@ import _CJavaScriptKit
7272
_: JavaScriptHostFuncRef,
7373
_: UnsafePointer<JavaScriptObjectRef>!
7474
) { fatalError() }
75-
func _destroy_ref(_: JavaScriptObjectRef) { fatalError() }
75+
func _release(_: JavaScriptObjectRef) { fatalError() }
76+
func _create_typed_array<T: TypedArrayElement>(
77+
_: JavaScriptTypedArrayKind,
78+
_: UnsafePointer<T>,
79+
_: Int32,
80+
_: UnsafeMutablePointer<JavaScriptObjectRef>!
81+
) { fatalError() }
82+
7683
#endif

‎Sources/_CJavaScriptKit/include/_CJavaScriptKit.h

+21-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ typedef enum __attribute__((enum_extensibility(closed))) {
1818
JavaScriptValueKindFunction = 6,
1919
} JavaScriptValueKind;
2020

21+
typedef enum __attribute__((enum_extensibility(closed))) {
22+
JavaScriptTypedArrayKindInt8 = 0,
23+
JavaScriptTypedArrayKindUint8 = 1,
24+
JavaScriptTypedArrayKindInt16 = 2,
25+
JavaScriptTypedArrayKindUint16 = 3,
26+
JavaScriptTypedArrayKindInt32 = 4,
27+
JavaScriptTypedArrayKindUint32 = 5,
28+
JavaScriptTypedArrayKindBigInt64 = 6,
29+
JavaScriptTypedArrayKindBigUint64 = 7,
30+
JavaScriptTypedArrayKindFloat32 = 8,
31+
JavaScriptTypedArrayKindFloat64 = 9,
32+
} JavaScriptTypedArrayKind;
33+
2134
typedef unsigned JavaScriptPayload1;
2235
typedef unsigned JavaScriptPayload2;
2336
typedef double JavaScriptPayload3;
@@ -94,8 +107,14 @@ _create_function(const JavaScriptHostFuncRef host_func_id,
94107
const JavaScriptObjectRef *func_ref_ptr);
95108

96109
__attribute__((__import_module__("javascript_kit"),
97-
__import_name__("swjs_destroy_ref"))) extern void
98-
_destroy_ref(const JavaScriptObjectRef ref);
110+
__import_name__("swjs_release"))) extern void
111+
_release(const JavaScriptObjectRef ref);
112+
113+
__attribute__((__import_module__("javascript_kit"),
114+
__import_name__("swjs_create_typed_array"))) extern void
115+
_create_typed_array(const JavaScriptTypedArrayKind kind,
116+
const void *elementsPtr, const int length,
117+
JavaScriptObjectRef *result_obj);
99118

100119
#endif
101120

0 commit comments

Comments
 (0)
Please sign in to comment.