expo-modules-jsi 57.0.7 → 57.1.0

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 (48) hide show
  1. package/CHANGELOG.md +40 -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/PromiseBenchmarks.swift +43 -0
  7. package/apple/Benchmarks/StringConversionBenchmarks.swift +245 -0
  8. package/apple/Benchmarks/ValueAccessBenchmarks.swift +132 -0
  9. package/apple/Package.swift +10 -0
  10. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Containers.swift +14 -8
  11. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Task.swift +35 -0
  12. package/apple/Sources/ExpoModulesJSI/Contexts/HostCallbackContext.swift +28 -0
  13. package/apple/Sources/ExpoModulesJSI/Contexts/HostFunctionContext.swift +18 -6
  14. package/apple/Sources/ExpoModulesJSI/Contexts/HostObjectContext.swift +7 -3
  15. package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +20 -13
  16. package/apple/Sources/ExpoModulesJSI/Protocols/JavaScriptRepresentable.swift +15 -3
  17. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptPropNameID.swift +11 -7
  18. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +97 -61
  19. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptValuesBuffer.swift +38 -18
  20. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +2 -2
  21. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptBigInt.swift +1 -2
  22. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +18 -8
  23. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +96 -20
  24. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +2 -2
  25. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +53 -11
  26. package/apple/Sources/ExpoModulesJSI/Utilities/ErrorHandling.swift +27 -20
  27. package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +133 -0
  28. package/apple/Sources/ExpoModulesJSI-Cxx/include/HostFunctionClosure.h +8 -3
  29. package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObject.h +9 -6
  30. package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObjectCallbacks.h +10 -6
  31. package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +75 -6
  32. package/apple/Tests/JavaScriptArrayBufferTests.swift +2 -2
  33. package/apple/Tests/JavaScriptCodableTaskTests.swift +116 -0
  34. package/apple/Tests/JavaScriptNativeStateTests.swift +10 -10
  35. package/apple/Tests/JavaScriptObjectTests.swift +79 -0
  36. package/apple/Tests/JavaScriptPromiseTests.swift +75 -0
  37. package/apple/Tests/JavaScriptPropNameIDTests.swift +123 -0
  38. package/apple/Tests/JavaScriptRuntimeTests.swift +79 -0
  39. package/apple/Tests/JavaScriptTypedArrayTests.swift +1 -1
  40. package/apple/Tests/JavaScriptUnownedValueTests.swift +9 -0
  41. package/apple/Tests/JavaScriptValueTests.swift +102 -0
  42. package/apple/Tests/JavaScriptValuesBufferTests.swift +2 -2
  43. package/apple/Tests/JavaScriptWeakObjectTests.swift +2 -2
  44. package/apple/Tests/Support/TestRuntimeScheduler.swift +151 -0
  45. package/apple/scripts/build-xcframework.sh +10 -0
  46. package/apple/scripts/generate-modulemap.sh +10 -1
  47. package/package.json +3 -2
  48. package/spm.config.json +1 -1
@@ -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],
@@ -1,15 +1,15 @@
1
1
  // Copyright 2025-present 650 Industries. All rights reserved.
2
2
 
3
- // `JavaScriptCodable` conformances for the standard container and wrapper types — `Array`,
4
- // `Optional`, and `Dictionary` — each conditional on its element/wrapped type conforming, and
5
- // each recursing statically into that element's conversion.
3
+ // `JavaScriptCodable` conformances for the standard container and wrapper types (`Array`,
4
+ // `Optional`, `Dictionary`), each recursing statically into its element/wrapped type's conversion.
6
5
  //
7
- // `JavaScriptCodable` is a composition type alias, so a conformance clause spells out both halves:
8
- // `extension Array: JavaScriptDecodable, JavaScriptEncodable where Element: JavaScriptCodable`.
6
+ // The decodable and encodable halves are separate conditional conformances, each gated only on the
7
+ // half it needs. A type conforming to both still gets both halves, but an encode-only element type
8
+ // (e.g. `Task`, which has no `decode`) can still be carried through a container's encode.
9
9
 
10
10
  // MARK: - Array
11
11
 
12
- extension Array: JavaScriptDecodable, JavaScriptEncodable where Element: JavaScriptCodable {
12
+ extension Array: JavaScriptDecodable where Element: JavaScriptDecodable {
13
13
  @JavaScriptActor
14
14
  @inlinable
15
15
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
@@ -26,7 +26,9 @@ extension Array: JavaScriptDecodable, JavaScriptEncodable where Element: JavaScr
26
26
  return try Element.decode(element, in: runtime)
27
27
  }
28
28
  }
29
+ }
29
30
 
31
+ extension Array: JavaScriptEncodable where Element: JavaScriptEncodable {
30
32
  @JavaScriptActor
31
33
  @inlinable
32
34
  public static func encode(_ value: [Element], in runtime: borrowing JavaScriptRuntime) throws
@@ -42,7 +44,7 @@ extension Array: JavaScriptDecodable, JavaScriptEncodable where Element: JavaScr
42
44
 
43
45
  // MARK: - Optional
44
46
 
45
- extension Optional: JavaScriptDecodable, JavaScriptEncodable where Wrapped: JavaScriptCodable {
47
+ extension Optional: JavaScriptDecodable where Wrapped: JavaScriptDecodable {
46
48
  // Optional copies nothing itself, so it overrides the zero-copy overload too and forwards the
47
49
  // borrowed value straight through — a wrapped primitive argument stays fully zero-copy.
48
50
  @JavaScriptActor
@@ -66,7 +68,9 @@ extension Optional: JavaScriptDecodable, JavaScriptEncodable where Wrapped: Java
66
68
  }
67
69
  return try Wrapped.decode(value, in: runtime)
68
70
  }
71
+ }
69
72
 
73
+ extension Optional: JavaScriptEncodable where Wrapped: JavaScriptEncodable {
70
74
  @JavaScriptActor
71
75
  @inlinable
72
76
  public static func encode(_ value: Wrapped?, in runtime: borrowing JavaScriptRuntime) throws
@@ -82,7 +86,7 @@ extension Optional: JavaScriptDecodable, JavaScriptEncodable where Wrapped: Java
82
86
 
83
87
  // MARK: - Dictionary
84
88
 
85
- extension Dictionary: JavaScriptDecodable, JavaScriptEncodable where Key == String, Value: JavaScriptCodable {
89
+ extension Dictionary: JavaScriptDecodable where Key == String, Value: JavaScriptDecodable {
86
90
  @JavaScriptActor
87
91
  @inlinable
88
92
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
@@ -102,7 +106,9 @@ extension Dictionary: JavaScriptDecodable, JavaScriptEncodable where Key == Stri
102
106
  }
103
107
  return result
104
108
  }
109
+ }
105
110
 
111
+ extension Dictionary: JavaScriptEncodable where Key == String, Value: JavaScriptEncodable {
106
112
  @JavaScriptActor
107
113
  @inlinable
108
114
  public static func encode(_ value: [String: Value], in runtime: borrowing JavaScriptRuntime) throws
@@ -0,0 +1,35 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ // `JavaScriptEncodable` conformance for `Task`, encoding it to a JavaScript `Promise`. This lets
4
+ // native code hand JavaScript a promise as a value: the return of a synchronous `@JS func` whose
5
+ // result type is `Task`, or a `Task` nested inside another encoded value. `createAsyncFunction`
6
+ // only wraps a function's own return in a promise, so a `Task` is how to produce one anywhere else.
7
+ //
8
+ // Encode-only: a promise decodes by awaiting it through `JavaScriptPromise`, not by reconstructing
9
+ // a `Task`. One conditional conformance covers throwing and non-throwing tasks (Swift forbids two
10
+ // conformances even with disjoint bounds, and `Task.value` is `async throws` for any `Failure`).
11
+
12
+ /// Encodes a `Task` to a JavaScript promise that settles with the task's result.
13
+ extension Task: JavaScriptEncodable where Success: JavaScriptEncodable, Failure: Error {
14
+ /// Returns a pending promise immediately and settles it on the JavaScript thread once the task
15
+ /// completes: fulfilling with the encoded `Success` value, or rejecting with the thrown error.
16
+ @JavaScriptActor
17
+ public static func encode(_ value: Task<Success, Failure>, in runtime: borrowing JavaScriptRuntime) throws
18
+ -> JavaScriptValue
19
+ {
20
+ let promise = try JavaScriptPromise(copy runtime)
21
+ // A detached task awaits the result off the JavaScript thread, then settles the promise. It must
22
+ // not inherit `@JavaScriptActor`: that actor runs jobs synchronously on the current thread rather
23
+ // than hopping to the JS thread, so a resumed continuation would land on whatever thread the task
24
+ // finished on. `resolve`/`reject` hop to the JS thread internally (and the encodable `resolve`
25
+ // encodes there), so settling from here is safe regardless of this task's thread.
26
+ Task<Void, Never>.detached {
27
+ do {
28
+ promise.resolve(try await value.value)
29
+ } catch {
30
+ promise.reject(error)
31
+ }
32
+ }
33
+ return promise.asValue()
34
+ }
35
+ }
@@ -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's result must be `Copyable`
16
+ /// because `_withUnsafeGuaranteedRef` requires it; callbacks write their `jsi::Value` result into the
17
+ /// slot the C++ caller provides and return only whether an error was stored.
18
+ @inline(__always)
19
+ internal func withGuaranteedContext<Context: HostCallbackContext, Result>(
20
+ _ pointer: UnsafeMutableRawPointer,
21
+ _ body: (_ context: Context, _ runtime: JavaScriptRuntime) -> Result
22
+ ) -> Result {
23
+ return Unmanaged<Context>.fromOpaque(pointer)._withUnsafeGuaranteedRef { context in
24
+ return context.runtime._withUnsafeGuaranteedRef { runtime in
25
+ return 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
  }