expo-modules-jsi 57.0.7 → 57.0.8
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 +20 -0
- package/CLAUDE.md +35 -0
- package/apple/Benchmarks/BenchmarkRunner.swift +115 -0
- package/apple/Benchmarks/FunctionCallBenchmarks.swift +45 -0
- package/apple/Benchmarks/HostFunctionBenchmarks.swift +217 -0
- package/apple/Benchmarks/StringConversionBenchmarks.swift +245 -0
- package/apple/Benchmarks/ValueAccessBenchmarks.swift +132 -0
- package/apple/Package.swift +10 -0
- package/apple/Sources/ExpoModulesJSI/Contexts/HostCallbackContext.swift +28 -0
- package/apple/Sources/ExpoModulesJSI/Contexts/HostFunctionContext.swift +18 -6
- package/apple/Sources/ExpoModulesJSI/Contexts/HostObjectContext.swift +7 -3
- package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +20 -13
- package/apple/Sources/ExpoModulesJSI/Protocols/JavaScriptRepresentable.swift +15 -3
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptPropNameID.swift +11 -7
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +80 -58
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptValuesBuffer.swift +7 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +2 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptBigInt.swift +1 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +18 -8
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +2 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +18 -7
- package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +130 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostFunctionClosure.h +6 -3
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObject.h +2 -1
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObjectCallbacks.h +5 -3
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +14 -3
- package/apple/Tests/JavaScriptArrayBufferTests.swift +2 -2
- package/apple/Tests/JavaScriptNativeStateTests.swift +10 -10
- package/apple/Tests/JavaScriptObjectTests.swift +79 -0
- package/apple/Tests/JavaScriptPropNameIDTests.swift +123 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +29 -0
- package/apple/Tests/JavaScriptTypedArrayTests.swift +1 -1
- package/apple/Tests/JavaScriptUnownedValueTests.swift +9 -0
- package/apple/Tests/JavaScriptValueTests.swift +102 -0
- package/apple/Tests/JavaScriptValuesBufferTests.swift +2 -2
- package/apple/Tests/JavaScriptWeakObjectTests.swift +2 -2
- package/apple/scripts/build-xcframework.sh +10 -0
- package/apple/scripts/generate-modulemap.sh +10 -1
- package/package.json +3 -2
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Testing
|
|
5
|
+
|
|
6
|
+
/// Benchmarks for the places where strings cross between the engine and Swift outside of plain
|
|
7
|
+
/// `getString()`: property name enumeration, the `JavaScriptRepresentable` conformance of `String`,
|
|
8
|
+
/// and `JavaScriptPropNameID`. Each one isolates a single conversion so regressions can be
|
|
9
|
+
/// attributed precisely.
|
|
10
|
+
extension JSIBenchmarks {
|
|
11
|
+
// MARK: - Property names
|
|
12
|
+
|
|
13
|
+
@Test
|
|
14
|
+
func `property names of an object with ASCII keys`() async throws {
|
|
15
|
+
try await benchmarkCase { runtime in
|
|
16
|
+
let object = try runtime.eval(
|
|
17
|
+
"({ alpha: 1, beta: 2, gamma: 3, delta: 4, epsilon: 5, zeta: 6, eta: 7, theta: 8, iota: 9, kappa: 10 })"
|
|
18
|
+
).getObject()
|
|
19
|
+
try benchmark("JavaScriptObject.getPropertyNames(): 10 ASCII keys", runtime: runtime) { iterations in
|
|
20
|
+
for _ in 0..<iterations {
|
|
21
|
+
_ = object.getPropertyNames()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test
|
|
28
|
+
func `property names of an object with non-ASCII keys`() async throws {
|
|
29
|
+
try await benchmarkCase { runtime in
|
|
30
|
+
let object = try runtime.eval(
|
|
31
|
+
"({ 'ąlpha': 1, 'bęta': 2, 'gąmma': 3, 'dęlta': 4, 'ępsilon': 5, 'zęta': 6, 'ęta': 7, 'thęta': 8, 'iotą': 9, 'kąppa': 10 })"
|
|
32
|
+
).getObject()
|
|
33
|
+
try benchmark("JavaScriptObject.getPropertyNames(): 10 non-ASCII keys", runtime: runtime) { iterations in
|
|
34
|
+
for _ in 0..<iterations {
|
|
35
|
+
_ = object.getPropertyNames()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// MARK: - Property access by name
|
|
42
|
+
|
|
43
|
+
@Test
|
|
44
|
+
func `object property by non-ASCII name`() async throws {
|
|
45
|
+
try await benchmarkCase { runtime in
|
|
46
|
+
let object = try runtime.eval("({ 'odpowiedź': 42 })").getObject()
|
|
47
|
+
try benchmark("JavaScriptObject.getProperty(_:): non-ASCII name", runtime: runtime) { iterations in
|
|
48
|
+
for _ in 0..<iterations {
|
|
49
|
+
_ = object.getProperty("odpowiedź")
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
func `has object property by name`() async throws {
|
|
57
|
+
try await benchmarkCase { runtime in
|
|
58
|
+
let object = try runtime.eval("({ answer: 42 })").getObject()
|
|
59
|
+
try benchmark("JavaScriptObject.hasProperty(_:)", runtime: runtime) { iterations in
|
|
60
|
+
for _ in 0..<iterations {
|
|
61
|
+
_ = object.hasProperty("answer")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Test
|
|
68
|
+
func `set object property by name`() async throws {
|
|
69
|
+
try await benchmarkCase { runtime in
|
|
70
|
+
let object = JavaScriptObject(runtime)
|
|
71
|
+
try benchmark("JavaScriptObject.setProperty(_:value:): double", runtime: runtime) { iterations in
|
|
72
|
+
for _ in 0..<iterations {
|
|
73
|
+
object.setProperty("answer", value: 42.0)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: - String as JavaScriptRepresentable
|
|
80
|
+
|
|
81
|
+
@Test
|
|
82
|
+
func `string from JavaScriptValue via JavaScriptRepresentable`() async throws {
|
|
83
|
+
try await benchmarkCase { runtime in
|
|
84
|
+
let value = try runtime.eval("'benchmark string value'")
|
|
85
|
+
try benchmark("String.fromJavaScriptValue(): 22B ASCII", runtime: runtime) { iterations in
|
|
86
|
+
for _ in 0..<iterations {
|
|
87
|
+
_ = String.fromJavaScriptValue(value)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Test
|
|
94
|
+
func `256B string from JavaScriptValue via JavaScriptRepresentable`() async throws {
|
|
95
|
+
try await benchmarkCase { runtime in
|
|
96
|
+
let value = try runtime.eval("'a'.repeat(256)")
|
|
97
|
+
try benchmark("String.fromJavaScriptValue(): 256B ASCII", runtime: runtime) { iterations in
|
|
98
|
+
for _ in 0..<iterations {
|
|
99
|
+
_ = String.fromJavaScriptValue(value)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
func `non-ASCII string from JavaScriptValue via JavaScriptRepresentable`() async throws {
|
|
107
|
+
try await benchmarkCase { runtime in
|
|
108
|
+
let value = try runtime.eval("'ąęó'.repeat(4)")
|
|
109
|
+
try benchmark("String.fromJavaScriptValue(): 12 non-ASCII chars", runtime: runtime) { iterations in
|
|
110
|
+
for _ in 0..<iterations {
|
|
111
|
+
_ = String.fromJavaScriptValue(value)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Sizes below the 512-code-unit threshold, where UTF-16 is transcoded by hand instead of by
|
|
118
|
+
/// `String(decoding:as:)`.
|
|
119
|
+
@Test
|
|
120
|
+
func `64 non-ASCII chars from JavaScriptValue via JavaScriptRepresentable`() async throws {
|
|
121
|
+
try await benchmarkCase { runtime in
|
|
122
|
+
let value = try runtime.eval("'zażółć gęślą jaźń '.repeat(4).slice(0, 64)")
|
|
123
|
+
try benchmark("String.fromJavaScriptValue(): 64 non-ASCII chars", runtime: runtime) { iterations in
|
|
124
|
+
for _ in 0..<iterations {
|
|
125
|
+
_ = String.fromJavaScriptValue(value)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Test
|
|
132
|
+
func `256 non-ASCII chars from JavaScriptValue via JavaScriptRepresentable`() async throws {
|
|
133
|
+
try await benchmarkCase { runtime in
|
|
134
|
+
let value = try runtime.eval("'zażółć gęślą jaźń '.repeat(16).slice(0, 256)")
|
|
135
|
+
try benchmark("String.fromJavaScriptValue(): 256 non-ASCII chars", runtime: runtime) { iterations in
|
|
136
|
+
for _ in 0..<iterations {
|
|
137
|
+
_ = String.fromJavaScriptValue(value)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@Test
|
|
144
|
+
func `128 emoji from JavaScriptValue via JavaScriptRepresentable`() async throws {
|
|
145
|
+
try await benchmarkCase { runtime in
|
|
146
|
+
let value = try runtime.eval("'🎉'.repeat(128)")
|
|
147
|
+
try benchmark("String.fromJavaScriptValue(): 128 emoji (256 code units)", runtime: runtime) { iterations in
|
|
148
|
+
for _ in 0..<iterations {
|
|
149
|
+
_ = String.fromJavaScriptValue(value)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
func `string to JavaScriptValue via JavaScriptRepresentable`() async throws {
|
|
157
|
+
try await benchmarkCase { runtime in
|
|
158
|
+
let string = "benchmark string value"
|
|
159
|
+
try benchmark("String.toJavaScriptValue(in:): 22B ASCII", runtime: runtime) { iterations in
|
|
160
|
+
for _ in 0..<iterations {
|
|
161
|
+
_ = string.toJavaScriptValue(in: runtime)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@Test
|
|
168
|
+
func `256B string to JavaScriptValue via JavaScriptRepresentable`() async throws {
|
|
169
|
+
try await benchmarkCase { runtime in
|
|
170
|
+
let string = String(repeating: "a", count: 256)
|
|
171
|
+
try benchmark("String.toJavaScriptValue(in:): 256B ASCII", runtime: runtime) { iterations in
|
|
172
|
+
for _ in 0..<iterations {
|
|
173
|
+
_ = string.toJavaScriptValue(in: runtime)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@Test
|
|
180
|
+
func `string dictionary from JavaScriptValue`() async throws {
|
|
181
|
+
try await benchmarkCase { runtime in
|
|
182
|
+
let value = try runtime.eval(
|
|
183
|
+
"""
|
|
184
|
+
({ alpha: 'one', beta: 'two', gamma: 'three', delta: 'four', epsilon: 'five',
|
|
185
|
+
zeta: 'six', eta: 'seven', theta: 'eight', iota: 'nine', kappa: 'ten' })
|
|
186
|
+
"""
|
|
187
|
+
)
|
|
188
|
+
try benchmark("[String: String].fromJavaScriptValue(): 10 entries", runtime: runtime) { iterations in
|
|
189
|
+
for _ in 0..<iterations {
|
|
190
|
+
_ = [String: String].fromJavaScriptValue(value)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// MARK: - PropNameID
|
|
197
|
+
|
|
198
|
+
@Test
|
|
199
|
+
func `PropNameID to string`() async throws {
|
|
200
|
+
try await benchmarkCase { runtime in
|
|
201
|
+
let propName = JavaScriptPropNameID(runtime, string: "propertyName")
|
|
202
|
+
try benchmark("JavaScriptPropNameID.utf8(): ASCII", runtime: runtime) { iterations in
|
|
203
|
+
for _ in 0..<iterations {
|
|
204
|
+
_ = propName.utf8()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@Test
|
|
211
|
+
func `non-ASCII PropNameID to string`() async throws {
|
|
212
|
+
try await benchmarkCase { runtime in
|
|
213
|
+
let propName = JavaScriptPropNameID(runtime, string: "właściwość")
|
|
214
|
+
try benchmark("JavaScriptPropNameID.utf8(): non-ASCII", runtime: runtime) { iterations in
|
|
215
|
+
for _ in 0..<iterations {
|
|
216
|
+
_ = propName.utf8()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@Test
|
|
223
|
+
func `PropNameID to string via utf16`() async throws {
|
|
224
|
+
try await benchmarkCase { runtime in
|
|
225
|
+
let propName = JavaScriptPropNameID(runtime, string: "propertyName")
|
|
226
|
+
try benchmark("JavaScriptPropNameID.utf16(): ASCII", runtime: runtime) { iterations in
|
|
227
|
+
for _ in 0..<iterations {
|
|
228
|
+
_ = propName.utf16()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@Test
|
|
235
|
+
func `PropNameID from string`() async throws {
|
|
236
|
+
try await benchmarkCase { runtime in
|
|
237
|
+
let string = "propertyName"
|
|
238
|
+
try benchmark("JavaScriptPropNameID(runtime, string:): ASCII", runtime: runtime) { iterations in
|
|
239
|
+
for _ in 0..<iterations {
|
|
240
|
+
_ = JavaScriptPropNameID(runtime, string: string)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Testing
|
|
5
|
+
|
|
6
|
+
/// Micro benchmarks for converting and accessing JavaScript values from Swift.
|
|
7
|
+
/// Each one isolates a single accessor so regressions can be attributed precisely.
|
|
8
|
+
extension JSIBenchmarks {
|
|
9
|
+
@Test
|
|
10
|
+
func `value to object`() async throws {
|
|
11
|
+
try await benchmarkCase { runtime in
|
|
12
|
+
let value = try runtime.eval("({ answer: 42 })")
|
|
13
|
+
try benchmark("JavaScriptValue.getObject()", runtime: runtime) { iterations in
|
|
14
|
+
for _ in 0..<iterations {
|
|
15
|
+
_ = value.getObject()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Test
|
|
22
|
+
func `value to array`() async throws {
|
|
23
|
+
try await benchmarkCase { runtime in
|
|
24
|
+
let value = try runtime.eval("[1, 2, 3, 4, 5]")
|
|
25
|
+
try benchmark("JavaScriptValue.getArray()", runtime: runtime) { iterations in
|
|
26
|
+
for _ in 0..<iterations {
|
|
27
|
+
_ = value.getArray()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test
|
|
34
|
+
func `value to string`() async throws {
|
|
35
|
+
try await benchmarkCase { runtime in
|
|
36
|
+
let value = try runtime.eval("'benchmark string value'")
|
|
37
|
+
try benchmark("JavaScriptValue.getString()", runtime: runtime) { iterations in
|
|
38
|
+
for _ in 0..<iterations {
|
|
39
|
+
_ = value.getString()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Test
|
|
46
|
+
func `value to double`() async throws {
|
|
47
|
+
try await benchmarkCase { runtime in
|
|
48
|
+
let value = try runtime.eval("123.45")
|
|
49
|
+
try benchmark("JavaScriptValue.getDouble()", runtime: runtime) { iterations in
|
|
50
|
+
for _ in 0..<iterations {
|
|
51
|
+
_ = value.getDouble()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
func `object property by name`() async throws {
|
|
59
|
+
try await benchmarkCase { runtime in
|
|
60
|
+
let object = try runtime.eval("({ answer: 42 })").getObject()
|
|
61
|
+
try benchmark("JavaScriptObject.getProperty(_:)", runtime: runtime) { iterations in
|
|
62
|
+
for _ in 0..<iterations {
|
|
63
|
+
_ = object.getProperty("answer")
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Unlike a number, a string or object property is a pointer value: wrapping it in a
|
|
70
|
+
/// ``JavaScriptValue`` must not clone the engine handle it already owns.
|
|
71
|
+
@Test
|
|
72
|
+
func `object string property by name`() async throws {
|
|
73
|
+
try await benchmarkCase { runtime in
|
|
74
|
+
let object = try runtime.eval("({ answer: 'forty-two' })").getObject()
|
|
75
|
+
try benchmark("JavaScriptObject.getProperty(_:): string value", runtime: runtime) { iterations in
|
|
76
|
+
for _ in 0..<iterations {
|
|
77
|
+
_ = object.getProperty("answer")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Test
|
|
84
|
+
func `object object property by name`() async throws {
|
|
85
|
+
try await benchmarkCase { runtime in
|
|
86
|
+
let object = try runtime.eval("({ answer: { value: 42 } })").getObject()
|
|
87
|
+
try benchmark("JavaScriptObject.getProperty(_:): object value", runtime: runtime) { iterations in
|
|
88
|
+
for _ in 0..<iterations {
|
|
89
|
+
_ = object.getProperty("answer")
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test
|
|
96
|
+
func `nested property traversal`() async throws {
|
|
97
|
+
try await benchmarkCase { runtime in
|
|
98
|
+
let object = try runtime.eval("({ a: { b: { c: { d: 42 } } } })").getObject()
|
|
99
|
+
try benchmark("JavaScriptObject nested subscript (4 keys)", runtime: runtime) { iterations in
|
|
100
|
+
for _ in 0..<iterations {
|
|
101
|
+
_ = object["a", "b", "c", "d"]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Test
|
|
108
|
+
func `property names enumeration`() async throws {
|
|
109
|
+
try await benchmarkCase { runtime in
|
|
110
|
+
let object = try runtime.eval(
|
|
111
|
+
"({ one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8 })"
|
|
112
|
+
).getObject()
|
|
113
|
+
try benchmark("JavaScriptObject.getPropertyNames() (8 properties)", runtime: runtime) { iterations in
|
|
114
|
+
for _ in 0..<iterations {
|
|
115
|
+
_ = object.getPropertyNames()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Test
|
|
122
|
+
func `array element access`() async throws {
|
|
123
|
+
try await benchmarkCase { runtime in
|
|
124
|
+
let array = try runtime.eval("[1, 2, 3, 4, 5]").getArray()
|
|
125
|
+
try benchmark("JavaScriptArray.getValue(at:)", runtime: runtime) { iterations in
|
|
126
|
+
for _ in 0..<iterations {
|
|
127
|
+
_ = try array.getValue(at: 0)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
package/apple/Package.swift
CHANGED
|
@@ -138,6 +138,16 @@ let package = Package(
|
|
|
138
138
|
.testTarget(
|
|
139
139
|
name: "Tests",
|
|
140
140
|
dependencies: testFrameworks.dependencies,
|
|
141
|
+
path: "Tests",
|
|
142
|
+
),
|
|
143
|
+
|
|
144
|
+
// Benchmarks are opt-in: their suites are gated on the `EXPO_BENCHMARK` environment
|
|
145
|
+
// variable, so regular test runs build them but skip every suite. Run `pnpm benchmark`
|
|
146
|
+
// to execute them in the Release configuration.
|
|
147
|
+
.testTarget(
|
|
148
|
+
name: "Benchmarks",
|
|
149
|
+
dependencies: testFrameworks.dependencies,
|
|
150
|
+
path: "Benchmarks",
|
|
141
151
|
),
|
|
142
152
|
] + testFrameworks.binaryTargets,
|
|
143
153
|
swiftLanguageModes: [.v6],
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/// A context object handed to JSI as an opaque pointer, owned by the JSI host function or host
|
|
2
|
+
/// object it was created for and freed from that owner's deallocate callback.
|
|
3
|
+
internal protocol HostCallbackContext: AnyObject {
|
|
4
|
+
/// The runtime the owner was installed in. Stored as `Unmanaged` rather than `weak`: the owner
|
|
5
|
+
/// lives in the runtime's heap, so a callback can only run while the runtime is alive and
|
|
6
|
+
/// executing JS, and a `weak` load plus a strong release per call was the largest single cost
|
|
7
|
+
/// of a no-op host call.
|
|
8
|
+
var runtime: Unmanaged<JavaScriptRuntime> { get }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// Runs `body` with guaranteed references to the context behind `pointer` and to its runtime.
|
|
12
|
+
/// `_withUnsafeGuaranteedRef` promises the compiler that both objects outlive the closure, so no
|
|
13
|
+
/// retain or release is emitted for either. Both promises hold for a JSI callback: the JSI owner
|
|
14
|
+
/// of the context is the caller, and a synchronous callback runs while the runtime executes JS,
|
|
15
|
+
/// with the wrapper owned for the runtime's whole lifetime. The body returns nothing because
|
|
16
|
+
/// `_withUnsafeGuaranteedRef` needs a `Copyable` result; callbacks write their `jsi::Value`
|
|
17
|
+
/// result into the slot the C++ caller provides instead.
|
|
18
|
+
@inline(__always)
|
|
19
|
+
internal func withGuaranteedContext<Context: HostCallbackContext>(
|
|
20
|
+
_ pointer: UnsafeMutableRawPointer,
|
|
21
|
+
_ body: (_ context: Context, _ runtime: JavaScriptRuntime) -> Void
|
|
22
|
+
) {
|
|
23
|
+
Unmanaged<Context>.fromOpaque(pointer)._withUnsafeGuaranteedRef { context in
|
|
24
|
+
context.runtime._withUnsafeGuaranteedRef { runtime in
|
|
25
|
+
body(context, runtime)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/// Context that captures Swift values to pass them to JSI host function as an unmanaged pointer for interoperability with C++.
|
|
2
|
-
internal final class HostFunctionContext: Sendable {
|
|
3
|
-
weak
|
|
2
|
+
internal final class HostFunctionContext: HostCallbackContext, Sendable {
|
|
3
|
+
// Stored as `Unmanaged` rather than `weak`, for the same reason as in
|
|
4
|
+
// ``UnownedThisHostFunctionContext`` below: the JSI host function owns the context and cannot
|
|
5
|
+
// outlive its runtime, and the per-call weak load plus strong release measured at about 10 ns
|
|
6
|
+
// of a no-op call through this form.
|
|
7
|
+
let runtime: Unmanaged<JavaScriptRuntime>
|
|
4
8
|
let name: String?
|
|
5
9
|
let call: JavaScriptRuntime.SyncFunctionClosure
|
|
6
10
|
|
|
7
11
|
init(runtime: JavaScriptRuntime, name: String? = nil, _ function: @escaping JavaScriptRuntime.SyncFunctionClosure) {
|
|
8
|
-
self.runtime = runtime
|
|
12
|
+
self.runtime = Unmanaged.passUnretained(runtime)
|
|
9
13
|
self.name = name
|
|
10
14
|
self.call = function
|
|
11
15
|
}
|
|
@@ -13,8 +17,16 @@ internal final class HostFunctionContext: Sendable {
|
|
|
13
17
|
|
|
14
18
|
/// Counterpart to ``HostFunctionContext`` for host functions whose closure receives `this` as a
|
|
15
19
|
/// borrowed ``JavaScriptUnownedValue`` (see ``JavaScriptRuntime/UnownedThisSyncFunctionClosure``).
|
|
16
|
-
internal final class UnownedThisHostFunctionContext: Sendable {
|
|
17
|
-
weak
|
|
20
|
+
internal final class UnownedThisHostFunctionContext: HostCallbackContext, Sendable {
|
|
21
|
+
// Stored as `Unmanaged` rather than `weak`: the context lives exactly as long as the JSI host
|
|
22
|
+
// function that owns it (freed from the `deallocate` callback), and the host function cannot
|
|
23
|
+
// outlive the runtime it was installed in. A `weak` reference here cost a `swift_weakLoadStrong`
|
|
24
|
+
// plus a strong release on every host call, and profiling showed that pair as the largest single
|
|
25
|
+
// item on the no-op `@JS` call floor. Callers read it through `_withUnsafeGuaranteedRef`, which
|
|
26
|
+
// also skips the strong retain/release a plain `unowned(unsafe)` load would still emit. Those go
|
|
27
|
+
// through the refcount side table (other wrappers hold the runtime `weak`), so they were the next
|
|
28
|
+
// largest item once the weak load was gone.
|
|
29
|
+
let runtime: Unmanaged<JavaScriptRuntime>
|
|
18
30
|
let name: String?
|
|
19
31
|
let call: JavaScriptRuntime.UnownedThisSyncFunctionClosure
|
|
20
32
|
|
|
@@ -22,7 +34,7 @@ internal final class UnownedThisHostFunctionContext: Sendable {
|
|
|
22
34
|
runtime: JavaScriptRuntime, name: String? = nil,
|
|
23
35
|
_ function: @escaping JavaScriptRuntime.UnownedThisSyncFunctionClosure
|
|
24
36
|
) {
|
|
25
|
-
self.runtime = runtime
|
|
37
|
+
self.runtime = Unmanaged.passUnretained(runtime)
|
|
26
38
|
self.name = name
|
|
27
39
|
self.call = function
|
|
28
40
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/// Context that captures Swift types to pass them to JSI host object as an unmanaged pointer for interoperability with C++.
|
|
2
|
-
internal final class HostObjectContext: Sendable {
|
|
2
|
+
internal final class HostObjectContext: HostCallbackContext, Sendable {
|
|
3
3
|
typealias Getter = @JavaScriptActor (_ propertyName: String) throws -> JavaScriptValue
|
|
4
4
|
typealias Setter = @JavaScriptActor (_ propertyName: String, _ value: JavaScriptValue) throws -> Void
|
|
5
5
|
typealias PropertyNamesGetter = @JavaScriptActor () -> [String]
|
|
6
6
|
typealias Deallocator = @JavaScriptActor () -> Void
|
|
7
7
|
|
|
8
|
-
weak
|
|
8
|
+
// Stored as `Unmanaged` rather than `weak`: the JSI host object owns the context (freed from the
|
|
9
|
+
// `deallocate` callback when the JS object is collected) and lives in the runtime's heap, so a
|
|
10
|
+
// getter or setter can only run while the runtime is alive and executing JS. Skips a weak load
|
|
11
|
+
// plus a strong release on every property access; see ``UnownedThisHostFunctionContext``.
|
|
12
|
+
let runtime: Unmanaged<JavaScriptRuntime>
|
|
9
13
|
let get: Getter
|
|
10
14
|
let set: Setter?
|
|
11
15
|
let getPropertyNames: PropertyNamesGetter
|
|
@@ -18,7 +22,7 @@ internal final class HostObjectContext: Sendable {
|
|
|
18
22
|
_ getPropertyNames: @escaping PropertyNamesGetter,
|
|
19
23
|
_ dealloc: @escaping Deallocator
|
|
20
24
|
) {
|
|
21
|
-
self.runtime = runtime
|
|
25
|
+
self.runtime = Unmanaged.passUnretained(runtime)
|
|
22
26
|
self.get = get
|
|
23
27
|
self.set = set
|
|
24
28
|
self.getPropertyNames = getPropertyNames
|
|
@@ -84,11 +84,25 @@ extension CGFloat: JSIRepresentableNumber {}
|
|
|
84
84
|
|
|
85
85
|
extension String: JSIRepresentable {
|
|
86
86
|
static func fromJSIValue(_ value: borrowing facebook.jsi.Value, in runtime: facebook.jsi.IRuntime) -> String {
|
|
87
|
-
return String(value.getString(runtime)
|
|
87
|
+
return String(jsiString: value.getString(runtime), in: runtime)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
func toJSIValue(in runtime: facebook.jsi.IRuntime) -> facebook.jsi.Value {
|
|
91
|
-
|
|
91
|
+
// Hand JSI the Swift string's own UTF-8 storage instead of going through `std::string`: the engine
|
|
92
|
+
// copies the bytes into its heap right away, so the intermediate `std::string` was one extra
|
|
93
|
+
// allocation, copy and free per string. `withUTF8` is mutating (it makes a bridged string
|
|
94
|
+
// contiguous first), hence the local copy; native strings are already contiguous and pay nothing.
|
|
95
|
+
// The value is moved out through a local because `withUTF8` needs a `Copyable` closure result.
|
|
96
|
+
var string = self
|
|
97
|
+
var value = facebook.jsi.Value.undefined()
|
|
98
|
+
string.withUTF8 { utf8 in
|
|
99
|
+
guard let base = utf8.baseAddress else {
|
|
100
|
+
value = facebook.jsi.Value(runtime, facebook.jsi.String.createFromAscii(runtime, "", 0))
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
value = facebook.jsi.Value(runtime, facebook.jsi.String.createFromUtf8(runtime, base, utf8.count))
|
|
104
|
+
}
|
|
105
|
+
return value
|
|
92
106
|
}
|
|
93
107
|
}
|
|
94
108
|
|
|
@@ -137,16 +151,10 @@ extension Dictionary: JSIRepresentable where Key == String, Value: JSIRepresenta
|
|
|
137
151
|
var result: Self = [:]
|
|
138
152
|
|
|
139
153
|
for index in 0..<size {
|
|
140
|
-
|
|
141
|
-
let
|
|
142
|
-
#if os(macOS)
|
|
143
|
-
// TODO: remove when bumping to react-native-macos 0.85
|
|
144
|
-
let jsiValue = expo.getProperty(runtime, object, key)
|
|
145
|
-
#else
|
|
154
|
+
// Look the value up by the key string the engine handed back, so any name round-trips exactly.
|
|
155
|
+
let jsiKey = propertyNames.getValueAtIndex(runtime, index).getString(runtime)
|
|
146
156
|
let jsiValue = object.getProperty(runtime, jsiKey)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
result[key] = Value.fromJSIValue(jsiValue, in: runtime)
|
|
157
|
+
result[String(jsiString: jsiKey, in: runtime)] = Value.fromJSIValue(jsiValue, in: runtime)
|
|
150
158
|
}
|
|
151
159
|
return result
|
|
152
160
|
}
|
|
@@ -155,8 +163,7 @@ extension Dictionary: JSIRepresentable where Key == String, Value: JSIRepresenta
|
|
|
155
163
|
let object = facebook.jsi.Object(runtime)
|
|
156
164
|
|
|
157
165
|
for (key, value) in self {
|
|
158
|
-
|
|
159
|
-
expo.setProperty(runtime, object, keyString, value.toJSIValue(in: runtime))
|
|
166
|
+
expo.setProperty(runtime, object, key.toJSIPropNameID(in: runtime), value.toJSIValue(in: runtime))
|
|
160
167
|
}
|
|
161
168
|
return facebook.jsi.Value(runtime, object)
|
|
162
169
|
}
|
|
@@ -39,11 +39,23 @@ extension Array: JavaScriptRepresentable where Element: JavaScriptRepresentable
|
|
|
39
39
|
|
|
40
40
|
extension Dictionary: JavaScriptRepresentable where Key == String, Value: JavaScriptRepresentable {
|
|
41
41
|
public static func fromJavaScriptValue(_ value: JavaScriptValue) -> Self {
|
|
42
|
-
let
|
|
42
|
+
guard let runtime = value.runtime else {
|
|
43
|
+
FatalError.runtimeLost()
|
|
44
|
+
}
|
|
45
|
+
let jsiRuntime = runtime.pointee
|
|
46
|
+
let object = value.pointee.getObject(jsiRuntime)
|
|
47
|
+
let propertyNames = object.getPropertyNames(jsiRuntime)
|
|
48
|
+
let size = propertyNames.size(jsiRuntime)
|
|
43
49
|
var result: Self = [:]
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
result.reserveCapacity(size)
|
|
52
|
+
|
|
53
|
+
for index in 0..<size {
|
|
54
|
+
// Look the value up by the key string the engine handed back instead of re-encoding the Swift
|
|
55
|
+
// key: it skips one engine string allocation per entry and round-trips any name exactly.
|
|
56
|
+
let jsiKey = propertyNames.getValueAtIndex(jsiRuntime, index).getString(jsiRuntime)
|
|
57
|
+
let jsiValue = JavaScriptValue(runtime, object.getProperty(jsiRuntime, jsiKey))
|
|
58
|
+
result[String(jsiString: jsiKey, in: jsiRuntime)] = Value.fromJavaScriptValue(jsiValue)
|
|
47
59
|
}
|
|
48
60
|
return result
|
|
49
61
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
internal import ExpoModulesJSI_Cxx
|
|
1
2
|
internal import jsi
|
|
2
3
|
|
|
3
4
|
/// Represents something that can be a JS property key.
|
|
@@ -14,23 +15,26 @@ public final class JavaScriptPropNameID: JavaScriptType {
|
|
|
14
15
|
/// Creates a PropNameID from the string.
|
|
15
16
|
public init(_ runtime: JavaScriptRuntime, string: String) {
|
|
16
17
|
self.runtime = runtime
|
|
17
|
-
self.pointee =
|
|
18
|
+
self.pointee = string.toJSIPropNameID(in: runtime.pointee)
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
/// Copies the
|
|
21
|
+
/// Copies the contents of the PropNameID into a string.
|
|
21
22
|
public func utf8() -> String {
|
|
22
23
|
guard let runtime else {
|
|
23
24
|
FatalError.runtimeLost()
|
|
24
25
|
}
|
|
26
|
+
// Property names are almost always short ASCII identifiers, and the engine's own `utf8()` is the
|
|
27
|
+
// fastest way to read those: the `std::string` stays inline and so does the resulting Swift
|
|
28
|
+
// string. Reading the internal representation through `getPropNameIdData`, as `String(jsiString:in:)`
|
|
29
|
+
// does for regular strings, measured about 40% slower for this case.
|
|
25
30
|
return String(pointee.utf8(runtime.pointee))
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
/// Copies the
|
|
33
|
+
/// Copies the contents of the PropNameID into a string. Same result as ``utf8()``.
|
|
29
34
|
public func utf16() -> String {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return String(pointee.utf16(runtime.pointee))
|
|
35
|
+
// The engine's `utf16()` builds a `std::u16string` that Swift then has to transcode, which is
|
|
36
|
+
// several times slower than going through UTF-8 for the same result.
|
|
37
|
+
return utf8()
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
// MARK: - JavaScriptType
|