expo-modules-jsi 58.0.7 → 58.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/apple/Benchmarks/ContainerCodingBenchmarks.swift +74 -0
- package/apple/Benchmarks/HostFunctionBenchmarks.swift +41 -0
- package/apple/Benchmarks/RuntimeCacheBenchmarks.swift +71 -0
- package/apple/Benchmarks/UnownedDecodeBenchmarks.swift +121 -0
- package/apple/Benchmarks/ValueAccessBenchmarks.swift +23 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Containers.swift +52 -10
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Data.swift +5 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Date.swift +27 -5
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +210 -21
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Set.swift +79 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptDecodable.swift +34 -5
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptValueKinds.swift +73 -0
- package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +4 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +43 -31
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntimeCache.swift +74 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +26 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +97 -7
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +14 -14
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +17 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +54 -1
- package/apple/Sources/ExpoModulesJSI/Utilities/Errors.swift +19 -0
- package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +4 -3
- package/apple/Sources/ExpoModulesJSI/Utilities/UncheckedSendable.swift +13 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +68 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/RuntimeScheduler.h +18 -5
- package/apple/Tests/JavaScriptCodableSetTests.swift +115 -0
- package/apple/Tests/JavaScriptDecodableKindsTests.swift +132 -0
- package/apple/Tests/JavaScriptRuntimeCacheTests.swift +117 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +86 -3
- package/apple/Tests/JavaScriptUnownedDecodeTests.swift +99 -0
- package/apple/Tests/JavaScriptUnownedElementsTests.swift +76 -0
- package/apple/scripts/build-xcframework.sh +15 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 58.0.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [iOS] Add a `JavaScriptCodable` conformance for `Set`: it encodes to a JS `Set` and decodes from a JS `Set` or an array, collapsing duplicate elements. ([#50859](https://github.com/expo/expo/pull/50859) by [@tsapeta](https://github.com/tsapeta))
|
|
8
|
+
- `JavaScriptRuntime.evalAsync` now evaluates on the JavaScript thread when called from another thread on a runtime with a scheduler, instead of on the calling thread. ([#51015](https://github.com/expo/expo/pull/51015) by [@tsapeta](https://github.com/tsapeta))
|
|
9
|
+
- [iOS] Speed up `JavaScriptObject.defineProperty` by looking up `Object.defineProperty` once per runtime and building the descriptor in C++. ([#51004](https://github.com/expo/expo/pull/51004) by [@tsapeta](https://github.com/tsapeta))
|
|
10
|
+
- [iOS] Fix the xcframework failing to build with Xcode 26 (Swift 6.2): `RuntimeScheduler` constructors annotated with `SWIFT_RETURNS_RETAINED` were rejected, and host function and host object getter callbacks failed with `sending '...' risks causing data races`. ([#51040](https://github.com/expo/expo/pull/51040) by [@tsapeta](https://github.com/tsapeta))
|
|
11
|
+
- [iOS] Speed up integer arguments and results of native functions by running the integer conversions specialized inside `ExpoModulesJSI` instead of unspecialized in the calling module. A host function that adds two `Int`s runs about 7× faster. ([#51055](https://github.com/expo/expo/pull/51055) by [@tsapeta](https://github.com/tsapeta))
|
|
12
|
+
|
|
13
|
+
## 58.0.8
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [iOS] Add `decodableKinds` to `JavaScriptDecodable`: the kinds of JavaScript value (`JavaScriptValueKinds`) that `decode` can accept, so code that picks between several types can skip the ones that can't match. ([#50905](https://github.com/expo/expo/pull/50905) by [@tsapeta](https://github.com/tsapeta))
|
|
18
|
+
- [iOS] Fix crash reports symbolicated on the device showing no function names for `ExpoModulesJSI` frames. ([#50698](https://github.com/expo/expo/pull/50698) by [@tsapeta](https://github.com/tsapeta))
|
|
19
|
+
- Return strings, objects and arrays from host functions and host object getters without cloning the engine handle, and build short ASCII strings from JS inline. ([#50937](https://github.com/expo/expo/pull/50937) by [@tsapeta](https://github.com/tsapeta))
|
|
20
|
+
- [iOS] Add `JavaScriptRuntime.cached(_:_:)` with typed `JavaScriptRuntime.Cache.Key`s, to create a value once per runtime and reuse it, for example a JavaScript constructor or a property name. A lookup reads one array slot, about 5× faster than the string-keyed `JavaScriptPropNameID.cached(_:_:)`. ([#50888](https://github.com/expo/expo/pull/50888) by [@tsapeta](https://github.com/tsapeta))
|
|
21
|
+
- [iOS] Add `JavaScriptValue.withUnownedValue(in:_:)`, and give the owning `JavaScriptDecodable.decode` a default that borrows the value and decodes it through the `JavaScriptUnownedValue` overload, so a conformer can implement only that one. Arrays, dictionaries, dates, records and enums now decode unowned values without copying them first. ([#50960](https://github.com/expo/expo/pull/50960) by [@tsapeta](https://github.com/tsapeta))
|
|
22
|
+
- [iOS] Add `JavaScriptArray.mapUnowned(_:)`, `JavaScriptObject.withUnownedProperty(_:_:)`, and `isArray()` and `getArray(in:)` on `JavaScriptUnownedValue`. Arrays, dictionaries and dates now decode through their unowned overload without copying the value or wrapping each element in a `JavaScriptValue`, and their owning decodes forward to it. ([#50980](https://github.com/expo/expo/pull/50980) by [@tsapeta](https://github.com/tsapeta))
|
|
23
|
+
- [iOS] Keep checkout paths out of the Swift compilation cache key so modules importing ExpoModulesCore can reuse cached compilation results across checkouts and worktrees. ([#50354](https://github.com/expo/expo/pull/50354) by [@janicduplessis](https://github.com/janicduplessis))
|
|
24
|
+
|
|
3
25
|
## 58.0.7
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Testing
|
|
5
|
+
|
|
6
|
+
/// Benchmarks comparing the `Array` and `Set` `JavaScriptCodable` conformances. `Set` decoding from a
|
|
7
|
+
/// JS array shares the `Array` fast path and adds only the hashing, while decoding from a JS `Set` and
|
|
8
|
+
/// encoding to one go through the global `Array.from` and `Set` constructor, so each case is measured
|
|
9
|
+
/// against its `Array` counterpart at a small and a large element count.
|
|
10
|
+
extension JSIBenchmarks {
|
|
11
|
+
// MARK: - Decode
|
|
12
|
+
|
|
13
|
+
@Test(arguments: [10, 1000])
|
|
14
|
+
func `decode an array of ints as Array`(count: Int) async throws {
|
|
15
|
+
try await benchmarkCase { runtime in
|
|
16
|
+
let value = try runtime.eval("Array.from({ length: \(count) }, (_, i) => i)")
|
|
17
|
+
try benchmark("[Int].decode: JS array of \(count)", runtime: runtime) { iterations in
|
|
18
|
+
for _ in 0..<iterations {
|
|
19
|
+
_ = try [Int].decode(value, in: runtime)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test(arguments: [10, 1000])
|
|
26
|
+
func `decode an array of ints as Set`(count: Int) async throws {
|
|
27
|
+
try await benchmarkCase { runtime in
|
|
28
|
+
let value = try runtime.eval("Array.from({ length: \(count) }, (_, i) => i)")
|
|
29
|
+
try benchmark("Set<Int>.decode: JS array of \(count)", runtime: runtime) { iterations in
|
|
30
|
+
for _ in 0..<iterations {
|
|
31
|
+
_ = try Set<Int>.decode(value, in: runtime)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test(arguments: [10, 1000])
|
|
38
|
+
func `decode a set of ints as Set`(count: Int) async throws {
|
|
39
|
+
try await benchmarkCase { runtime in
|
|
40
|
+
let value = try runtime.eval("new Set(Array.from({ length: \(count) }, (_, i) => i))")
|
|
41
|
+
try benchmark("Set<Int>.decode: JS Set of \(count)", runtime: runtime) { iterations in
|
|
42
|
+
for _ in 0..<iterations {
|
|
43
|
+
_ = try Set<Int>.decode(value, in: runtime)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// MARK: - Encode
|
|
50
|
+
|
|
51
|
+
@Test(arguments: [10, 1000])
|
|
52
|
+
func `encode an Array of ints`(count: Int) async throws {
|
|
53
|
+
try await benchmarkCase { runtime in
|
|
54
|
+
let array = Array(0..<count)
|
|
55
|
+
try benchmark("[Int].encode: \(count) elements", runtime: runtime) { iterations in
|
|
56
|
+
for _ in 0..<iterations {
|
|
57
|
+
_ = try [Int].encode(array, in: runtime)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test(arguments: [10, 1000])
|
|
64
|
+
func `encode a Set of ints`(count: Int) async throws {
|
|
65
|
+
try await benchmarkCase { runtime in
|
|
66
|
+
let set = Set(0..<count)
|
|
67
|
+
try benchmark("Set<Int>.encode: \(count) elements", runtime: runtime) { iterations in
|
|
68
|
+
for _ in 0..<iterations {
|
|
69
|
+
_ = try Set<Int>.encode(set, in: runtime)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -87,6 +87,47 @@ extension JSIBenchmarks {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// The integer codables, as bindings call them: `Int.decode` for an `Int` argument and `Int?.decode`
|
|
91
|
+
// for an optional one.
|
|
92
|
+
|
|
93
|
+
@Test
|
|
94
|
+
func `host function adding two Ints`() async throws {
|
|
95
|
+
try await benchmarkCase { runtime in
|
|
96
|
+
let fn = runtime.createFunction("add") {
|
|
97
|
+
[runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
|
|
98
|
+
let a = try Int.decode(arguments.unownedValue(at: 0), in: runtime)
|
|
99
|
+
let b = try Int.decode(arguments.unownedValue(at: 1), in: runtime)
|
|
100
|
+
return try Int.encode(a + b, in: runtime)
|
|
101
|
+
}
|
|
102
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
103
|
+
let driver = try runtime.eval(
|
|
104
|
+
"(function(n) { for (var i = 0; i < n; i++) benchFn(3, 38); })"
|
|
105
|
+
).getFunction()
|
|
106
|
+
try benchmark("host function: add two Ints", runtime: runtime) { iterations in
|
|
107
|
+
_ = try driver.call(arguments: iterations)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Test
|
|
113
|
+
func `host function adding two optional Ints`() async throws {
|
|
114
|
+
try await benchmarkCase { runtime in
|
|
115
|
+
let fn = runtime.createFunction("add") {
|
|
116
|
+
[runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
|
|
117
|
+
let a = try Int?.decode(arguments.unownedValue(at: 0), in: runtime) ?? 0
|
|
118
|
+
let b = try Int?.decode(arguments.unownedValue(at: 1), in: runtime) ?? 0
|
|
119
|
+
return try Int.encode(a + b, in: runtime)
|
|
120
|
+
}
|
|
121
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
122
|
+
let driver = try runtime.eval(
|
|
123
|
+
"(function(n) { for (var i = 0; i < n; i++) benchFn(3, 38); })"
|
|
124
|
+
).getFunction()
|
|
125
|
+
try benchmark("host function: add two optional Ints", runtime: runtime) { iterations in
|
|
126
|
+
_ = try driver.call(arguments: iterations)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
90
131
|
// MARK: - Unowned-this form with string arguments
|
|
91
132
|
|
|
92
133
|
@Test
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Testing
|
|
5
|
+
|
|
6
|
+
private let answerPropNameKey = JavaScriptRuntime.Cache.Key<JavaScriptPropNameID>()
|
|
7
|
+
|
|
8
|
+
/// Benchmarks for values cached per runtime with ``JavaScriptRuntime/cached(_:_:)``, read against
|
|
9
|
+
/// the string-keyed ``JavaScriptPropNameID/cached(_:_:)`` registry.
|
|
10
|
+
extension JSIBenchmarks {
|
|
11
|
+
@Test
|
|
12
|
+
func `runtime cache hit`() async throws {
|
|
13
|
+
try await benchmarkCase { runtime in
|
|
14
|
+
_ = runtime.cached(answerPropNameKey) { JavaScriptPropNameID(runtime, string: "answer") }
|
|
15
|
+
try benchmark("JavaScriptRuntime.cached(_:_:): hit", runtime: runtime) { iterations in
|
|
16
|
+
for _ in 0..<iterations {
|
|
17
|
+
_ = runtime.cached(answerPropNameKey) { JavaScriptPropNameID(runtime, string: "answer") }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test
|
|
24
|
+
func `object property by runtime-cached PropNameID`() async throws {
|
|
25
|
+
try await benchmarkCase { runtime in
|
|
26
|
+
let object = try runtime.eval("({ answer: 42 })").getObject()
|
|
27
|
+
try benchmark("JavaScriptObject.getProperty(_:): runtime-cached PropNameID", runtime: runtime) { iterations in
|
|
28
|
+
for _ in 0..<iterations {
|
|
29
|
+
_ = object.getProperty(runtime.cached(answerPropNameKey) { JavaScriptPropNameID(runtime, string: "answer") })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
func `string-keyed PropNameID cache hit`() async throws {
|
|
37
|
+
try await benchmarkCase { runtime in
|
|
38
|
+
try benchmark("JavaScriptPropNameID.cached(_:_:): hit", runtime: runtime) { iterations in
|
|
39
|
+
for _ in 0..<iterations {
|
|
40
|
+
_ = JavaScriptPropNameID.cached(runtime, "answer")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
func `object property by string-keyed cached PropNameID`() async throws {
|
|
48
|
+
try await benchmarkCase { runtime in
|
|
49
|
+
let object = try runtime.eval("({ answer: 42 })").getObject()
|
|
50
|
+
try benchmark("JavaScriptObject.getProperty(_:): string-cached PropNameID", runtime: runtime) { iterations in
|
|
51
|
+
for _ in 0..<iterations {
|
|
52
|
+
_ = object.getProperty(.cached(runtime, "answer"))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Test
|
|
59
|
+
func `object property by prebuilt PropNameID`() async throws {
|
|
60
|
+
try await benchmarkCase { runtime in
|
|
61
|
+
let object = try runtime.eval("({ answer: 42 })").getObject()
|
|
62
|
+
let propName = JavaScriptPropNameID(runtime, string: "answer")
|
|
63
|
+
try benchmark("JavaScriptObject.getProperty(_:): prebuilt PropNameID", runtime: runtime) { iterations in
|
|
64
|
+
for _ in 0..<iterations {
|
|
65
|
+
_ = object.getProperty(propName)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
withExtendedLifetime(propName) {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Foundation
|
|
5
|
+
import Testing
|
|
6
|
+
|
|
7
|
+
/// Decoding from a borrowed `JavaScriptUnownedValue`, the path a `@JS` argument takes, and from an owning
|
|
8
|
+
/// `JavaScriptValue`, the path a nested value takes. A type without its own unowned decode copies the
|
|
9
|
+
/// value into an owning `JavaScriptValue` first.
|
|
10
|
+
extension JSIBenchmarks {
|
|
11
|
+
@Test
|
|
12
|
+
func `unowned decode of an array`() async throws {
|
|
13
|
+
try await benchmarkCase { runtime in
|
|
14
|
+
let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("[1, 2, 3, 4, 5, 6, 7, 8]"))
|
|
15
|
+
try benchmark("[Double].decode(unowned): 8 elements", runtime: runtime) { iterations in
|
|
16
|
+
for _ in 0..<iterations {
|
|
17
|
+
_ = try [Double].decode(buffer.unownedValue(at: 0), in: runtime)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test
|
|
24
|
+
func `unowned decode of a dictionary`() async throws {
|
|
25
|
+
try await benchmarkCase { runtime in
|
|
26
|
+
let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("({ a: 1, b: 2, c: 3, d: 4 })"))
|
|
27
|
+
try benchmark("[String: Double].decode(unowned): 4 entries", runtime: runtime) { iterations in
|
|
28
|
+
for _ in 0..<iterations {
|
|
29
|
+
_ = try [String: Double].decode(buffer.unownedValue(at: 0), in: runtime)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
func `unowned decode of a date`() async throws {
|
|
37
|
+
try await benchmarkCase { runtime in
|
|
38
|
+
let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("1700000000000"))
|
|
39
|
+
try benchmark("Date.decode(unowned): number", runtime: runtime) { iterations in
|
|
40
|
+
for _ in 0..<iterations {
|
|
41
|
+
_ = try Date.decode(buffer.unownedValue(at: 0), in: runtime)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
func `owning decode of an array`() async throws {
|
|
49
|
+
try await benchmarkCase { runtime in
|
|
50
|
+
let value = try runtime.eval("[1, 2, 3, 4, 5, 6, 7, 8]")
|
|
51
|
+
try benchmark("[Double].decode(owning): 8 elements", runtime: runtime) { iterations in
|
|
52
|
+
for _ in 0..<iterations {
|
|
53
|
+
_ = try [Double].decode(value, in: runtime)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
func `owning decode of a dictionary`() async throws {
|
|
61
|
+
try await benchmarkCase { runtime in
|
|
62
|
+
let value = try runtime.eval("({ a: 1, b: 2, c: 3, d: 4 })")
|
|
63
|
+
try benchmark("[String: Double].decode(owning): 4 entries", runtime: runtime) { iterations in
|
|
64
|
+
for _ in 0..<iterations {
|
|
65
|
+
_ = try [String: Double].decode(value, in: runtime)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test
|
|
72
|
+
func `owning decode of nested arrays`() async throws {
|
|
73
|
+
try await benchmarkCase { runtime in
|
|
74
|
+
let value = try runtime.eval("[[1, 2], [3, 4], [5, 6], [7, 8]]")
|
|
75
|
+
try benchmark("[[Double]].decode(owning): 4 x 2 elements", runtime: runtime) { iterations in
|
|
76
|
+
for _ in 0..<iterations {
|
|
77
|
+
_ = try [[Double]].decode(value, in: runtime)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Test
|
|
84
|
+
func `unowned decode of nested arrays`() async throws {
|
|
85
|
+
try await benchmarkCase { runtime in
|
|
86
|
+
let buffer = JavaScriptValuesBuffer.allocate(
|
|
87
|
+
in: runtime,
|
|
88
|
+
with: try runtime.eval("[[1, 2], [3, 4], [5, 6], [7, 8]]")
|
|
89
|
+
)
|
|
90
|
+
try benchmark("[[Double]].decode(unowned): 4 x 2 elements", runtime: runtime) { iterations in
|
|
91
|
+
for _ in 0..<iterations {
|
|
92
|
+
_ = try [[Double]].decode(buffer.unownedValue(at: 0), in: runtime)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Test
|
|
99
|
+
func `unowned decode of a date string`() async throws {
|
|
100
|
+
try await benchmarkCase { runtime in
|
|
101
|
+
let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("'2026-10-02T12:00:00Z'"))
|
|
102
|
+
try benchmark("Date.decode(unowned): ISO string", runtime: runtime) { iterations in
|
|
103
|
+
for _ in 0..<iterations {
|
|
104
|
+
_ = try Date.decode(buffer.unownedValue(at: 0), in: runtime)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@Test
|
|
111
|
+
func `unowned decode of a date object`() async throws {
|
|
112
|
+
try await benchmarkCase { runtime in
|
|
113
|
+
let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("new Date(1700000000000)"))
|
|
114
|
+
try benchmark("Date.decode(unowned): Date object", runtime: runtime) { iterations in
|
|
115
|
+
for _ in 0..<iterations {
|
|
116
|
+
_ = try Date.decode(buffer.unownedValue(at: 0), in: runtime)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -129,4 +129,27 @@ extension JSIBenchmarks {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
@Test
|
|
134
|
+
func `define a property`() async throws {
|
|
135
|
+
try await benchmarkCase { runtime in
|
|
136
|
+
try benchmark("JavaScriptObject.defineProperty(_:value:options:)", runtime: runtime) { iterations in
|
|
137
|
+
for index in 0..<iterations {
|
|
138
|
+
let object = runtime.createObject()
|
|
139
|
+
object.defineProperty("__expo_shared_object_id__", value: index, options: [.writable])
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Test
|
|
146
|
+
func `create an object`() async throws {
|
|
147
|
+
try await benchmarkCase { runtime in
|
|
148
|
+
try benchmark("JavaScriptRuntime.createObject()", runtime: runtime) { iterations in
|
|
149
|
+
for _ in 0..<iterations {
|
|
150
|
+
_ = runtime.createObject()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
132
155
|
}
|
|
@@ -10,19 +10,36 @@
|
|
|
10
10
|
// MARK: - Array
|
|
11
11
|
|
|
12
12
|
extension Array: JavaScriptDecodable where Element: JavaScriptDecodable {
|
|
13
|
+
// A non-array value is decoded as a single-element array, so it's accepted when the element is.
|
|
14
|
+
@inlinable
|
|
15
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
16
|
+
return Element.decodableKinds.union(.object)
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
@JavaScriptActor
|
|
14
20
|
@inlinable
|
|
15
21
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
16
22
|
-> [Element]
|
|
23
|
+
{
|
|
24
|
+
// Forwards to the unowned overload, which holds the implementation.
|
|
25
|
+
let runtime = copy runtime
|
|
26
|
+
return try value.withUnownedValue(in: runtime) { unownedValue in
|
|
27
|
+
return try decode(unownedValue, in: runtime)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@JavaScriptActor
|
|
32
|
+
@inlinable
|
|
33
|
+
public static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
34
|
+
-> [Element]
|
|
17
35
|
{
|
|
18
36
|
// A non-array value is "arrayized" into a single-element array, so a caller that passes a
|
|
19
37
|
// scalar where an array is expected still works.
|
|
20
38
|
guard value.isArray() else {
|
|
21
39
|
return [try Element.decode(value, in: runtime)]
|
|
22
40
|
}
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
return try value.getArray().map { element in
|
|
41
|
+
// Each element is lent out unowned, so it's decoded without a `JavaScriptValue` per element.
|
|
42
|
+
return try value.getArray(in: runtime).mapUnowned { element in
|
|
26
43
|
return try Element.decode(element, in: runtime)
|
|
27
44
|
}
|
|
28
45
|
}
|
|
@@ -45,6 +62,11 @@ extension Array: JavaScriptEncodable where Element: JavaScriptEncodable {
|
|
|
45
62
|
// MARK: - Optional
|
|
46
63
|
|
|
47
64
|
extension Optional: JavaScriptDecodable where Wrapped: JavaScriptDecodable {
|
|
65
|
+
@inlinable
|
|
66
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
67
|
+
return Wrapped.decodableKinds.union([.null, .undefined])
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
// Optional copies nothing itself, so it overrides the zero-copy overload too and forwards the
|
|
49
71
|
// borrowed value straight through — a wrapped primitive argument stays fully zero-copy.
|
|
50
72
|
@JavaScriptActor
|
|
@@ -87,22 +109,42 @@ extension Optional: JavaScriptEncodable where Wrapped: JavaScriptEncodable {
|
|
|
87
109
|
// MARK: - Dictionary
|
|
88
110
|
|
|
89
111
|
extension Dictionary: JavaScriptDecodable where Key == String, Value: JavaScriptDecodable {
|
|
112
|
+
@inlinable
|
|
113
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
114
|
+
return .object
|
|
115
|
+
}
|
|
116
|
+
|
|
90
117
|
@JavaScriptActor
|
|
91
118
|
@inlinable
|
|
92
119
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
93
120
|
-> [String: Value]
|
|
94
121
|
{
|
|
95
|
-
|
|
122
|
+
// Forwards to the unowned overload, which holds the implementation.
|
|
123
|
+
let runtime = copy runtime
|
|
124
|
+
return try value.withUnownedValue(in: runtime) { unownedValue in
|
|
125
|
+
return try decode(unownedValue, in: runtime)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@JavaScriptActor
|
|
130
|
+
@inlinable
|
|
131
|
+
public static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
132
|
+
-> [String: Value]
|
|
133
|
+
{
|
|
134
|
+
// Reads the object straight from the borrowed value and lends each property out unowned, so it's
|
|
135
|
+
// decoded without a `JavaScriptValue` per property.
|
|
136
|
+
let object = try value.asObject(in: runtime)
|
|
96
137
|
let keys = object.getPropertyNames()
|
|
97
138
|
var result = [String: Value](minimumCapacity: keys.count)
|
|
98
139
|
for key in keys {
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
140
|
+
let decoded: Value? = try object.withUnownedProperty(key) { property in
|
|
141
|
+
// Treat an `undefined`-valued property as an absent entry. Without this a non-optional
|
|
142
|
+
// `Value` would reject an object that simply omits the property as `undefined`.
|
|
143
|
+
return property.isUndefined() ? nil : try Value.decode(property, in: runtime)
|
|
144
|
+
}
|
|
145
|
+
if let decoded {
|
|
146
|
+
result[key] = decoded
|
|
104
147
|
}
|
|
105
|
-
result[key] = try Value.decode(property, in: runtime)
|
|
106
148
|
}
|
|
107
149
|
return result
|
|
108
150
|
}
|
|
@@ -9,6 +9,11 @@ import Foundation
|
|
|
9
9
|
// read/write rather than a single accessor.
|
|
10
10
|
|
|
11
11
|
extension Data: JavaScriptCodable {
|
|
12
|
+
@inlinable
|
|
13
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
14
|
+
return .object
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
@JavaScriptActor
|
|
13
18
|
@inlinable
|
|
14
19
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Data
|
|
@@ -9,22 +9,44 @@ import Foundation
|
|
|
9
9
|
// A `Date` is an absolute instant with no timezone/calendar; resolution is milliseconds.
|
|
10
10
|
|
|
11
11
|
extension Date: JavaScriptCodable {
|
|
12
|
+
@inlinable
|
|
13
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
14
|
+
return [.number, .string, .object]
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
@JavaScriptActor
|
|
13
18
|
@inlinable
|
|
14
19
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Date
|
|
15
20
|
{
|
|
16
|
-
//
|
|
17
|
-
|
|
21
|
+
// Forwards to the unowned overload, which holds the implementation.
|
|
22
|
+
let runtime = copy runtime
|
|
23
|
+
return try value.withUnownedValue(in: runtime) { unownedValue in
|
|
24
|
+
return try decode(unownedValue, in: runtime)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@JavaScriptActor
|
|
29
|
+
@inlinable
|
|
30
|
+
public static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
31
|
+
-> Date
|
|
32
|
+
{
|
|
33
|
+
// The cheap tag checks come before the `instanceof` check, which needs a global lookup; a number or
|
|
34
|
+
// a string can't be a `Date`, so the order is behavior-neutral. Only the string branch copies the
|
|
35
|
+
// value, since the `Date` constructor takes owning arguments; a string is the rare input.
|
|
18
36
|
if value.isNumber() {
|
|
19
37
|
return try dateFromMilliseconds(value.getDouble())
|
|
20
38
|
}
|
|
21
39
|
if value.isString() {
|
|
22
40
|
let dateConstructor = try runtime.global().getPropertyAsFunction("Date")
|
|
23
|
-
let constructed = try dateConstructor.callAsConstructor(value.
|
|
41
|
+
let constructed = try dateConstructor.callAsConstructor(value.copied(in: runtime)).asObject()
|
|
24
42
|
return try dateFromMilliseconds(constructed.callFunction("getTime").asDouble())
|
|
25
43
|
}
|
|
26
|
-
if value.
|
|
27
|
-
|
|
44
|
+
if value.isObject() {
|
|
45
|
+
let dateConstructor = try runtime.global().getPropertyAsFunction("Date")
|
|
46
|
+
let object = value.getObject(in: runtime)
|
|
47
|
+
if object.instanceOf(dateConstructor) {
|
|
48
|
+
return try dateFromMilliseconds(object.callFunction("getTime").asDouble())
|
|
49
|
+
}
|
|
28
50
|
}
|
|
29
51
|
throw InvalidDateException()
|
|
30
52
|
}
|