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.
Files changed (39) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/CLAUDE.md +35 -0
  3. package/apple/Benchmarks/BenchmarkRunner.swift +115 -0
  4. package/apple/Benchmarks/FunctionCallBenchmarks.swift +45 -0
  5. package/apple/Benchmarks/HostFunctionBenchmarks.swift +217 -0
  6. package/apple/Benchmarks/StringConversionBenchmarks.swift +245 -0
  7. package/apple/Benchmarks/ValueAccessBenchmarks.swift +132 -0
  8. package/apple/Package.swift +10 -0
  9. package/apple/Sources/ExpoModulesJSI/Contexts/HostCallbackContext.swift +28 -0
  10. package/apple/Sources/ExpoModulesJSI/Contexts/HostFunctionContext.swift +18 -6
  11. package/apple/Sources/ExpoModulesJSI/Contexts/HostObjectContext.swift +7 -3
  12. package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +20 -13
  13. package/apple/Sources/ExpoModulesJSI/Protocols/JavaScriptRepresentable.swift +15 -3
  14. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptPropNameID.swift +11 -7
  15. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +80 -58
  16. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptValuesBuffer.swift +7 -2
  17. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +2 -2
  18. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptBigInt.swift +1 -2
  19. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +18 -8
  20. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +2 -2
  21. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +18 -7
  22. package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +130 -0
  23. package/apple/Sources/ExpoModulesJSI-Cxx/include/HostFunctionClosure.h +6 -3
  24. package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObject.h +2 -1
  25. package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObjectCallbacks.h +5 -3
  26. package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +14 -3
  27. package/apple/Tests/JavaScriptArrayBufferTests.swift +2 -2
  28. package/apple/Tests/JavaScriptNativeStateTests.swift +10 -10
  29. package/apple/Tests/JavaScriptObjectTests.swift +79 -0
  30. package/apple/Tests/JavaScriptPropNameIDTests.swift +123 -0
  31. package/apple/Tests/JavaScriptRuntimeTests.swift +29 -0
  32. package/apple/Tests/JavaScriptTypedArrayTests.swift +1 -1
  33. package/apple/Tests/JavaScriptUnownedValueTests.swift +9 -0
  34. package/apple/Tests/JavaScriptValueTests.swift +102 -0
  35. package/apple/Tests/JavaScriptValuesBufferTests.swift +2 -2
  36. package/apple/Tests/JavaScriptWeakObjectTests.swift +2 -2
  37. package/apple/scripts/build-xcframework.sh +10 -0
  38. package/apple/scripts/generate-modulemap.sh +10 -1
  39. 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
+ }
@@ -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 let runtime: JavaScriptRuntime?
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 let runtime: JavaScriptRuntime?
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 let runtime: JavaScriptRuntime?
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).utf8(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
- return facebook.jsi.Value(runtime, facebook.jsi.String.createFromUtf8(runtime, std.string(self)))
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
- let jsiKey = propertyNames.getValueAtIndex(runtime, index)
141
- let key = String.fromJSIValue(jsiKey, in: runtime)
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
- #endif
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
- let keyString = String(describing: key)
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 object = value.getObject()
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
- for key in object.getPropertyNames() {
46
- result[key] = Value.fromJavaScriptValue(object.getProperty(key))
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 = facebook.jsi.PropNameID.forUtf8(runtime.pointee, string, string.count)
18
+ self.pointee = string.toJSIPropNameID(in: runtime.pointee)
18
19
  }
19
20
 
20
- /// Copies the data in a PropNameID as UTF8 into a string.
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 data in a PropNameID as UTF16 into a string.
33
+ /// Copies the contents of the PropNameID into a string. Same result as ``utf8()``.
29
34
  public func utf16() -> String {
30
- guard let runtime else {
31
- FatalError.runtimeLost()
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