expo-modules-jsi 58.0.4 → 58.0.6

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 (34) hide show
  1. package/CHANGELOG.md +9 -5
  2. package/apple/Benchmarks/HostObjectBenchmarks.swift +57 -0
  3. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +4 -6
  4. package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +2 -2
  5. package/apple/Sources/ExpoModulesJSI/Protocols/JavaScriptRepresentable.swift +2 -3
  6. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +24 -17
  7. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntimeHandle.swift +67 -0
  8. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptValuesBuffer.swift +3 -2
  9. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +53 -35
  10. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptFunction.swift +2 -2
  11. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +79 -59
  12. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +63 -46
  13. package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +10 -0
  14. package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObject.h +2 -2
  15. package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObjectCallbacks.h +17 -8
  16. package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +20 -0
  17. package/apple/Tests/JavaScriptRuntimeTests.swift +40 -0
  18. package/apple/Tests/JavaScriptValueTests.swift +18 -0
  19. package/apple/Tests/JavaScriptValuesBufferTests.swift +14 -0
  20. package/package.json +1 -2
  21. package/apple/.generated/module.modulemap +0 -6
  22. package/apple/Products/ExpoModulesJSI.xcframework/Info.plist +0 -107
  23. package/apple/Products/ExpoModulesJSI.xcframework/ios-arm64/.build-hash +0 -0
  24. package/apple/Products/ExpoModulesJSI.xcframework/ios-arm64/ExpoModulesJSI.framework/ExpoModulesJSI +0 -0
  25. package/apple/Products/ExpoModulesJSI.xcframework/ios-arm64_x86_64-maccatalyst/.build-hash +0 -0
  26. package/apple/Products/ExpoModulesJSI.xcframework/ios-arm64_x86_64-maccatalyst/ExpoModulesJSI.framework/ExpoModulesJSI +0 -0
  27. package/apple/Products/ExpoModulesJSI.xcframework/ios-arm64_x86_64-simulator/.build-hash +0 -0
  28. package/apple/Products/ExpoModulesJSI.xcframework/ios-arm64_x86_64-simulator/ExpoModulesJSI.framework/ExpoModulesJSI +0 -0
  29. package/apple/Products/ExpoModulesJSI.xcframework/macos-arm64_x86_64/.build-hash +0 -0
  30. package/apple/Products/ExpoModulesJSI.xcframework/macos-arm64_x86_64/ExpoModulesJSI.framework/ExpoModulesJSI +0 -0
  31. package/apple/Products/ExpoModulesJSI.xcframework/tvos-arm64/.build-hash +0 -0
  32. package/apple/Products/ExpoModulesJSI.xcframework/tvos-arm64/ExpoModulesJSI.framework/ExpoModulesJSI +0 -0
  33. package/apple/Products/ExpoModulesJSI.xcframework/tvos-arm64_x86_64-simulator/.build-hash +0 -0
  34. package/apple/Products/ExpoModulesJSI.xcframework/tvos-arm64_x86_64-simulator/ExpoModulesJSI.framework/ExpoModulesJSI +0 -0
package/CHANGELOG.md CHANGED
@@ -1,14 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## Unpublished
3
+ ## 58.0.6
4
4
 
5
- ### 🛠 Breaking changes
5
+ ### Patch Changes
6
6
 
7
- ### 🎉 New features
7
+ - [iOS] Read host object property names through `getPropNameIdData` instead of building a `std::string` for every access, making property access from JavaScript up to 14% faster for long names. ([#50805](https://github.com/expo/expo/pull/50805) by [@tsapeta](https://github.com/tsapeta))
8
+ - [iOS] The `JavaScriptValue`, `JavaScriptObject` and `JavaScriptArray` initializers now take the runtime as `borrowing`, so callers no longer retain it for the call, making host functions that return strings or numbers up to ~15% faster. ([#50844](https://github.com/expo/expo/pull/50844) by [@tsapeta](https://github.com/tsapeta))
9
+ - [iOS] `JavaScriptValue`, `JavaScriptObject` and `JavaScriptArray` now hold a strong runtime handle instead of a `weak` reference to the runtime, which removes the weak reference traffic and slow-path reference counting from their hot paths (for example `getObject()` ~16×, `getArray()` ~12× and `getProperty(_:)` ~1.8× faster). ([#50806](https://github.com/expo/expo/pull/50806) by [@tsapeta](https://github.com/tsapeta))
8
10
 
9
- ### 🐛 Bug fixes
11
+ ## 58.0.5
10
12
 
11
- ### 💡 Others
13
+ ### Patch Changes
14
+
15
+ - Force-bump all packages, due to migration to changesets. ([#50762](https://github.com/expo/expo/pull/50762) by [@kitten](https://github.com/kitten))
12
16
 
13
17
  ## 58.0.4 — 2026-09-25
14
18
 
@@ -0,0 +1,57 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesJSI
4
+ import Testing
5
+
6
+ /// End-to-end benchmarks for JavaScript accessing properties of a Swift host object. A JavaScript
7
+ /// driver function runs the measured loop, so each operation covers the engine's dispatch to the host
8
+ /// object, handing the property name to Swift, and the result or assigned value crossing back.
9
+ extension JSIBenchmarks {
10
+ @Test
11
+ func `host object property get`() async throws {
12
+ try await benchmarkCase { runtime in
13
+ let hostObject = runtime.createHostObject(get: { _ in
14
+ return JavaScriptValue.number(42)
15
+ })
16
+ runtime.global().setProperty("benchHostObject", value: hostObject)
17
+ let driver = try runtime.eval("(function(n) { for (var i = 0; i < n; i++) benchHostObject.answer; })")
18
+ .getFunction()
19
+ try benchmark("host object: get", runtime: runtime) { iterations in
20
+ _ = try driver.call(arguments: iterations)
21
+ }
22
+ }
23
+ }
24
+
25
+ @Test
26
+ func `host object property get with long name`() async throws {
27
+ try await benchmarkCase { runtime in
28
+ let hostObject = runtime.createHostObject(get: { _ in
29
+ return JavaScriptValue.number(42)
30
+ })
31
+ runtime.global().setProperty("benchHostObject", value: hostObject)
32
+ // Longer than the 22-byte inline capacity of libc++'s `std::string`.
33
+ let driver = try runtime.eval(
34
+ "(function(n) { for (var i = 0; i < n; i++) benchHostObject.aPropertyNameLongerThanTheInlineCapacity; })"
35
+ ).getFunction()
36
+ try benchmark("host object: get, 40B name", runtime: runtime) { iterations in
37
+ _ = try driver.call(arguments: iterations)
38
+ }
39
+ }
40
+ }
41
+
42
+ @Test
43
+ func `host object property set`() async throws {
44
+ try await benchmarkCase { runtime in
45
+ let hostObject = runtime.createHostObject(
46
+ get: { _ in .undefined },
47
+ set: { _, _ in }
48
+ )
49
+ runtime.global().setProperty("benchHostObject", value: hostObject)
50
+ let driver = try runtime.eval("(function(n) { for (var i = 0; i < n; i++) benchHostObject.answer = i; })")
51
+ .getFunction()
52
+ try benchmark("host object: set", runtime: runtime) { iterations in
53
+ _ = try driver.call(arguments: iterations)
54
+ }
55
+ }
56
+ }
57
+ }
@@ -61,9 +61,7 @@ extension String: JavaScriptCodable {
61
61
  public static func encode(_ value: String, in runtime: borrowing JavaScriptRuntime) throws
62
62
  -> JavaScriptValue
63
63
  {
64
- // The `JavaScriptValue(_:_:)` initializer takes the runtime by owned convention (it stores it),
65
- // so an owned copy is needed from the borrowed parameter.
66
- return JavaScriptValue(copy runtime, value)
64
+ return JavaScriptValue(runtime, value)
67
65
  }
68
66
  }
69
67
 
@@ -281,7 +279,7 @@ extension Int64: JavaScriptCodable {
281
279
  @JavaScriptActor
282
280
  @inlinable
283
281
  public static func encode(_ value: Int64, in runtime: borrowing JavaScriptRuntime) throws -> JavaScriptValue {
284
- return JavaScriptValue(copy runtime, bigInt: value)
282
+ return JavaScriptValue(runtime, bigInt: value)
285
283
  }
286
284
  }
287
285
 
@@ -405,7 +403,7 @@ extension UInt64: JavaScriptCodable {
405
403
  public static func encode(_ value: UInt64, in runtime: borrowing JavaScriptRuntime) throws
406
404
  -> JavaScriptValue
407
405
  {
408
- return JavaScriptValue(copy runtime, bigInt: value)
406
+ return JavaScriptValue(runtime, bigInt: value)
409
407
  }
410
408
  }
411
409
 
@@ -457,7 +455,7 @@ func decodeWideInteger<T: FixedWidthInteger>(
457
455
  // constructing `TypeError` here, which this inlinable helper can't reference.
458
456
  return try decodeInteger(value.asDouble(), as: T.self)
459
457
  }
460
- return try decodeBigInt(value.copied(in: copy runtime).getBigInt(), as: T.self)
458
+ return try decodeBigInt(value.copied(in: runtime).getBigInt(), as: T.self)
461
459
  }
462
460
  return try decodeInteger(value.getDouble(), as: T.self)
463
461
  }
@@ -12,10 +12,10 @@ internal protocol JSIRepresentable: JavaScriptRepresentable, Sendable, ~Copyable
12
12
 
13
13
  extension JSIRepresentable {
14
14
  public static func fromJavaScriptValue(_ value: JavaScriptValue) -> Self {
15
- guard let jsiRuntime = value.runtime else {
15
+ guard let jsiRuntime = value.jsiRuntime else {
16
16
  FatalError.runtimeLost()
17
17
  }
18
- return Self.fromJSIValue(value.pointee, in: jsiRuntime.pointee)
18
+ return Self.fromJSIValue(value.pointee, in: jsiRuntime)
19
19
  }
20
20
 
21
21
  public func toJavaScriptValue(in runtime: JavaScriptRuntime) -> JavaScriptValue {
@@ -39,10 +39,9 @@ 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
- guard let runtime = value.runtime else {
42
+ guard let runtimeHandle = value.runtimeHandle, let jsiRuntime = runtimeHandle.pointee else {
43
43
  FatalError.runtimeLost()
44
44
  }
45
- let jsiRuntime = runtime.pointee
46
45
  let object = value.pointee.getObject(jsiRuntime)
47
46
  let propertyNames = object.getPropertyNames(jsiRuntime)
48
47
  let size = propertyNames.size(jsiRuntime)
@@ -54,7 +53,7 @@ extension Dictionary: JavaScriptRepresentable where Key == String, Value: JavaSc
54
53
  // Look the value up by the key string the engine handed back instead of re-encoding the Swift
55
54
  // key: it skips one engine string allocation per entry and round-trips any name exactly.
56
55
  let jsiKey = propertyNames.getValueAtIndex(jsiRuntime, index).getString(jsiRuntime)
57
- let jsiValue = JavaScriptValue(runtime, object.getProperty(jsiRuntime, jsiKey))
56
+ let jsiValue = JavaScriptValue(runtimeHandle, object.getProperty(jsiRuntime, jsiKey))
58
57
  result[String(jsiString: jsiKey, in: jsiRuntime)] = Value.fromJavaScriptValue(jsiValue)
59
58
  }
60
59
  return result
@@ -38,6 +38,10 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
38
38
  internal let runtimePointee: facebook.jsi.Runtime
39
39
  internal let scheduler: expo.RuntimeScheduler
40
40
 
41
+ /// Strong handle that values hold instead of a `weak` reference to the runtime. See
42
+ /// ``JavaScriptRuntimeHandle`` for why.
43
+ internal let handle: JavaScriptRuntimeHandle
44
+
41
45
  /// Whether this wrapper owns the underlying `jsi::Runtime` and must destroy it on `deinit`. True
42
46
  /// only for the standalone `init()`, which creates the runtime via `createHermesRuntime()`. The
43
47
  /// other initializers adopt a runtime owned elsewhere (e.g. React Native), which must never be
@@ -62,8 +66,10 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
62
66
  internal init(_ runtime: facebook.jsi.Runtime) {
63
67
  self.runtimePointee = runtime
64
68
  self.pointee = expo.iruntime(runtime)
69
+ self.handle = JavaScriptRuntimeHandle(self.pointee)
65
70
  self.scheduler = expo.RuntimeScheduler()
66
71
  self.ownsRuntime = false
72
+ handle.attach(self)
67
73
  installLongLivedObjectsTeardown()
68
74
  }
69
75
 
@@ -73,8 +79,10 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
73
79
  let runtime = expo.createHermesRuntime()
74
80
  self.runtimePointee = runtime
75
81
  self.pointee = expo.iruntime(runtime)
82
+ self.handle = JavaScriptRuntimeHandle(self.pointee)
76
83
  self.scheduler = expo.RuntimeScheduler()
77
84
  self.ownsRuntime = true
85
+ handle.attach(self)
78
86
  installLongLivedObjectsTeardown()
79
87
  }
80
88
 
@@ -85,8 +93,10 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
85
93
  let runtime = unsafeBitCast(unsafePointer, to: facebook.jsi.Runtime.self)
86
94
  self.runtimePointee = runtime
87
95
  self.pointee = expo.iruntime(runtime)
96
+ self.handle = JavaScriptRuntimeHandle(self.pointee)
88
97
  self.scheduler = expo.RuntimeScheduler()
89
98
  self.ownsRuntime = false
99
+ handle.attach(self)
90
100
  installLongLivedObjectsTeardown()
91
101
  }
92
102
 
@@ -112,12 +122,15 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
112
122
  let fn = unsafeBitCast(dispatch, to: expo.RuntimeScheduler.ScheduleFn.self)
113
123
  self.runtimePointee = runtime
114
124
  self.pointee = expo.iruntime(runtime)
125
+ self.handle = JavaScriptRuntimeHandle(self.pointee)
115
126
  self.scheduler = expo.RuntimeScheduler(scheduler, fn)
116
127
  self.ownsRuntime = false
128
+ handle.attach(self)
117
129
  installLongLivedObjectsTeardown()
118
130
  }
119
131
 
120
132
  deinit {
133
+ handle.detach()
121
134
  // Destroy the runtime only if this wrapper created it (standalone `init()`); adopted runtimes
122
135
  // are owned elsewhere (e.g. React Native) and must not be freed here.
123
136
  guard ownsRuntime else {
@@ -181,13 +194,13 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
181
194
  ) -> JavaScriptObject {
182
195
  func getter(
183
196
  context: UnsafeMutableRawPointer,
184
- propertyName: UnsafePointer<CChar>,
197
+ propertyName: UnsafePointer<facebook.jsi.PropNameID>,
185
198
  resultPtr: UnsafeMutablePointer<facebook.jsi.Value>
186
199
  ) -> Bool {
187
- let propertyName = String(cString: propertyName)
188
200
  nonisolated(unsafe) let resultPtr = resultPtr
189
201
 
190
202
  return withGuaranteedContext(context) { (context: HostObjectContext, runtime) in
203
+ let propertyName = String(jsiPropNameID: propertyName.pointee, in: runtime.pointee)
191
204
  return JavaScriptActor.assumeIsolated {
192
205
  return forwardingSwiftErrorsToJS(runtime: runtime) {
193
206
  try context.get(propertyName).writeJSIValue(to: resultPtr)
@@ -198,15 +211,14 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
198
211
 
199
212
  func setter(
200
213
  context: UnsafeMutableRawPointer,
201
- propertyName: UnsafePointer<CChar>,
214
+ propertyName: UnsafePointer<facebook.jsi.PropNameID>,
202
215
  valuePointer: UnsafeMutableRawPointer
203
216
  ) -> Bool {
204
- let propertyName = String(cString: propertyName)
205
-
206
217
  return withGuaranteedContext(context) { (context: HostObjectContext, runtime) in
218
+ let propertyName = String(jsiPropNameID: propertyName.pointee, in: runtime.pointee)
207
219
  guard let set = context.set else {
208
220
  // Unreachable in practice: when the user passed `nil` for `set`, the call site
209
- // below at `expo.HostObjectCallbacks(...)` also passes `nil` to C++, and
221
+ // below creates the read-only `expo.HostObjectCallbacks` without a setter, and
210
222
  // `HostObjectCallbacks::set` throws a `jsi::JSError` directly instead of
211
223
  // calling back into Swift. Trap loudly so a future C++ refactor can't silently
212
224
  // swallow assignments.
@@ -255,17 +267,12 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
255
267
 
256
268
  let context = Unmanaged.passRetained(HostObjectContext(runtime: self, get, set, getPropertyNames, dealloc))
257
269
  .toOpaque()
258
- let setterPointer:
259
- (@convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>, UnsafeMutableRawPointer) -> Bool)? = setter
260
- // Pass a null setter to C++ when the Swift setter is nil so that JS assignment
261
- // raises a `jsi::JSError` directly, without crossing the Swift boundary.
262
- let callbacks = expo.HostObjectCallbacks(
263
- context,
264
- getter,
265
- set == nil ? nil : setterPointer,
266
- propertyNamesGetter,
267
- deallocate
268
- )
270
+ // Without a Swift setter, use the read-only callbacks so that JS assignment raises a
271
+ // `jsi::JSError` directly, without crossing the Swift boundary.
272
+ let callbacks =
273
+ set == nil
274
+ ? expo.HostObjectCallbacks(context, getter, propertyNamesGetter, deallocate)
275
+ : expo.HostObjectCallbacks(context, getter, setter, propertyNamesGetter, deallocate)
269
276
  let hostObject = expo.HostObject.makeObject(pointee, consume callbacks)
270
277
 
271
278
  return JavaScriptObject(self, hostObject)
@@ -0,0 +1,67 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ internal import jsi
4
+
5
+ /// A strong handle to a ``JavaScriptRuntime`` that values hold instead of a `weak` reference to the
6
+ /// runtime itself.
7
+ ///
8
+ /// A `weak` reference gives the runtime a side table, and from then on every strong retain and
9
+ /// release of the runtime takes the slow atomic path, on top of the cost of the weak loads, stores and
10
+ /// destroys. Values did all of these on every access. The handle is never weakly referenced, so the
11
+ /// retain and release that storing it costs stay on the inline fast path, and reading the engine
12
+ /// runtime through it costs no ARC at all: `IRuntime` is imported as an immortal reference.
13
+ ///
14
+ /// The runtime owns its handle and marks it dead in `deinit`, so values that outlive the runtime still
15
+ /// detect it through ``runtime`` or ``pointee`` returning `nil`.
16
+ ///
17
+ /// Unlike a `weak` reference, the liveness check is not atomic and does not keep the runtime alive for
18
+ /// the rest of the call. Values must not be used concurrently with the runtime's `deinit`, which holds
19
+ /// as long as the last reference to the runtime is released on the JavaScript thread (as
20
+ /// `AppContext.destroy()` does).
21
+ ///
22
+ /// Not to be confused with the engine's handles to JavaScript values (`jsi::PointerValue`).
23
+ internal final class JavaScriptRuntimeHandle: @unchecked Sendable {
24
+ /// The runtime that owns this handle, cleared by the runtime's `deinit`. Read it through ``runtime``
25
+ /// instead.
26
+ private unowned(unsafe) var unsafeRuntime: JavaScriptRuntime?
27
+
28
+ /// The engine runtime, cleared by the runtime's `deinit`. Liveness is checked on this field rather
29
+ /// than on ``unsafeRuntime``: `IRuntime` is an immortal reference, so reading it and checking it for
30
+ /// `nil` costs no reference counting, whereas checking the runtime reference for `nil` retains the
31
+ /// runtime, which is the slow-path traffic this handle exists to avoid.
32
+ nonisolated(unsafe) private var jsiRuntime: facebook.jsi.IRuntime?
33
+
34
+ internal init(_ pointee: facebook.jsi.IRuntime) {
35
+ self.jsiRuntime = pointee
36
+ }
37
+
38
+ /// Links the handle to its runtime. Called at the end of the runtime's initializers, once `self`
39
+ /// is available.
40
+ internal func attach(_ runtime: JavaScriptRuntime) {
41
+ unsafeRuntime = runtime
42
+ }
43
+
44
+ /// Marks the runtime as gone. Called from the runtime's `deinit`.
45
+ internal func detach() {
46
+ jsiRuntime = nil
47
+ unsafeRuntime = nil
48
+ }
49
+
50
+ /// Whether the runtime that owns this handle is still alive.
51
+ @inline(__always)
52
+ internal var isAlive: Bool {
53
+ return jsiRuntime != nil
54
+ }
55
+
56
+ /// The runtime, or `nil` if it has been deallocated.
57
+ @inline(__always)
58
+ internal var runtime: JavaScriptRuntime? {
59
+ return isAlive ? unsafeRuntime : nil
60
+ }
61
+
62
+ /// The engine runtime, or `nil` if the runtime has been deallocated.
63
+ @inline(__always)
64
+ internal var pointee: facebook.jsi.IRuntime? {
65
+ return jsiRuntime
66
+ }
67
+ }
@@ -191,13 +191,14 @@ public struct JavaScriptValuesBuffer: JavaScriptType, ~Copyable {
191
191
 
192
192
  /// Allocates a new owning buffer holding a runtime-aware copy of each value's
193
193
  /// underlying `facebook.jsi.Value`. The given `JavaScriptValue`s must all belong
194
- /// to `runtime`; mixing runtimes will crash deep inside JSI.
194
+ /// to `runtime` or be runtime-free; mixing runtimes will crash deep inside JSI.
195
195
  @JavaScriptActor
196
196
  public static func copying(in runtime: JavaScriptRuntime, values: [JavaScriptValue]) -> JavaScriptValuesBuffer {
197
197
  let buffer = UnsafeMutableBufferPointer<facebook.jsi.Value>.allocate(capacity: values.count)
198
198
  for (index, value) in values.enumerated() {
199
+ // Runtime-free values (undefined, null, booleans and numbers) have no handle and fit any runtime.
199
200
  assert(
200
- value.runtime === runtime,
201
+ value.runtimeHandle == nil || value.runtimeHandle === runtime.handle,
201
202
  "JavaScriptValue belongs to a different runtime than the buffer being initialized"
202
203
  )
203
204
  buffer.initializeElement(at: index, to: facebook.jsi.Value(runtime.pointee, value.pointee))
@@ -5,7 +5,19 @@ internal import jsi
5
5
  /// and Swift, allowing you to access and manipulate JavaScript array elements from Swift code. It maintains a reference
6
6
  /// to the underlying JavaScript array and provides Swift-friendly APIs for common array operations.
7
7
  public struct JavaScriptArray: JavaScriptType, ~Copyable {
8
- internal weak let runtime: JavaScriptRuntime?
8
+ /// Handle to the runtime the array belongs to.
9
+ internal let runtimeHandle: JavaScriptRuntimeHandle
10
+
11
+ /// The runtime the array belongs to, or `nil` if it has been deallocated. Prefer ``jsiRuntime`` on
12
+ /// hot paths: it costs no reference counting.
13
+ internal var runtime: JavaScriptRuntime? {
14
+ return runtimeHandle.runtime
15
+ }
16
+
17
+ /// The engine runtime the array belongs to, or `nil` if the runtime has been deallocated.
18
+ internal var jsiRuntime: facebook.jsi.IRuntime? {
19
+ return runtimeHandle.pointee
20
+ }
9
21
  internal let pointee: facebook.jsi.Array
10
22
 
11
23
  /// Creates a new JavaScript array with the specified length.
@@ -33,7 +45,7 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
33
45
  /// - Note: This initializer creates a new JavaScript array object in the runtime's heap.
34
46
  /// The array's length can be modified later using the `length` property.
35
47
  /// - SeeAlso: `init(_:items:)` for creating arrays with initial values
36
- public init(_ runtime: JavaScriptRuntime, length: Int = 0) {
48
+ public init(_ runtime: borrowing JavaScriptRuntime, length: Int = 0) {
37
49
  self.init(runtime, facebook.jsi.Array(runtime.pointee, length))
38
50
  }
39
51
 
@@ -52,8 +64,14 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
52
64
  /// by library consumers.
53
65
  /// - Note: The `pointee` parameter uses consuming ownership, meaning the JSI array
54
66
  /// object is moved into this structure and the caller's copy is invalidated.
55
- internal init(_ runtime: JavaScriptRuntime, _ pointee: consuming facebook.jsi.Array) {
56
- self.runtime = runtime
67
+ internal init(_ runtime: borrowing JavaScriptRuntime, _ pointee: consuming facebook.jsi.Array) {
68
+ self.runtimeHandle = runtime.handle
69
+ self.pointee = pointee
70
+ }
71
+
72
+ /// Creates an array from existing JSI array, which belongs to the runtime behind `runtimeHandle`.
73
+ internal init(_ runtimeHandle: JavaScriptRuntimeHandle, _ pointee: consuming facebook.jsi.Array) {
74
+ self.runtimeHandle = runtimeHandle
57
75
  self.pointee = pointee
58
76
  }
59
77
 
@@ -85,7 +103,7 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
85
103
  /// implementation creates an empty array first and then populates it element by element,
86
104
  /// rather than using JSI's `createWithElements` method directly.
87
105
  /// - SeeAlso: `init(_:items:)` for the variadic argument version
88
- public init(_ runtime: JavaScriptRuntime, items: [JavaScriptValue]) {
106
+ public init(_ runtime: borrowing JavaScriptRuntime, items: [JavaScriptValue]) {
89
107
  self.init(runtime, length: items.count)
90
108
 
91
109
  for (i, item) in items.enumerated() {
@@ -121,7 +139,7 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
121
139
  /// array-based initializer. If you already have an array of values, consider using
122
140
  /// `init(_:items:)` directly for cleaner syntax.
123
141
  /// - SeeAlso: `init(_:items:)` for the array version
124
- public init(_ runtime: JavaScriptRuntime, items: JavaScriptValue...) {
142
+ public init(_ runtime: borrowing JavaScriptRuntime, items: JavaScriptValue...) {
125
143
  self.init(runtime, items: items)
126
144
  }
127
145
 
@@ -159,7 +177,7 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
159
177
  /// - Note: Unlike the `JavaScriptValue` variadic initializer, this version accepts heterogeneous
160
178
  /// types directly without requiring explicit `JavaScriptValue` wrapping.
161
179
  /// - SeeAlso: `init(_:items:)` for the `JavaScriptValue` array version
162
- public init<each T: JavaScriptRepresentable>(_ runtime: JavaScriptRuntime, items: repeat each T) {
180
+ public init<each T: JavaScriptRepresentable>(_ runtime: borrowing JavaScriptRuntime, items: repeat each T) {
163
181
  var length: Int = 0
164
182
  for _ in repeat each items {
165
183
  length += 1
@@ -210,16 +228,16 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
210
228
  /// providing the same semantics as `array.length = newValue` in JavaScript.
211
229
  public var length: Int {
212
230
  get {
213
- guard let runtime else {
231
+ guard let jsiRuntime else {
214
232
  FatalError.runtimeLost()
215
233
  }
216
- return pointee.size(runtime.pointee)
234
+ return pointee.size(jsiRuntime)
217
235
  }
218
236
  nonmutating set(newLength) {
219
- guard let runtime else {
237
+ guard let jsiRuntime else {
220
238
  FatalError.runtimeLost()
221
239
  }
222
- expo.setArrayLength(runtime.pointee, pointee, newLength)
240
+ expo.setArrayLength(jsiRuntime, pointee, newLength)
223
241
  }
224
242
  }
225
243
 
@@ -230,22 +248,22 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
230
248
  /// - Throws: `JavaScriptArray.Errors.indexOutOfRange` if the index is negative or
231
249
  /// greater than or equal to the array's length
232
250
  public func getValue(at index: Int) throws -> JavaScriptValue {
233
- guard let runtime else {
251
+ guard let jsiRuntime else {
234
252
  FatalError.runtimeLost()
235
253
  }
236
254
  let length = self.length
237
255
  guard (0..<length).contains(index) else {
238
256
  throw Errors.indexOutOfRange(index: index, length: length)
239
257
  }
240
- return JavaScriptValue(runtime, pointee.getValueAtIndex(runtime.pointee, index))
258
+ return JavaScriptValue(runtimeHandle, pointee.getValueAtIndex(jsiRuntime, index))
241
259
  }
242
260
 
243
261
  /// Returns the value at the given index without bounds-checking. Used by the iteration
244
262
  /// helpers (`map`, `forEach`, `reduce`, `filter`, `enumerated`) which iterate
245
263
  /// `0..<length` and never produce an out-of-range index. Skipping the redundant bounds
246
264
  /// check avoids a weak-runtime load on every element on the hot path.
247
- internal func getValueUnchecked(at index: Int, in runtime: JavaScriptRuntime) -> JavaScriptValue {
248
- return JavaScriptValue(runtime, pointee.getValueAtIndex(runtime.pointee, index))
265
+ internal func getValueUnchecked(at index: Int, in jsiRuntime: facebook.jsi.IRuntime) -> JavaScriptValue {
266
+ return JavaScriptValue(runtimeHandle, pointee.getValueAtIndex(jsiRuntime, index))
249
267
  }
250
268
 
251
269
  /// Sets the element at the specified index.
@@ -305,13 +323,13 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
305
323
  return (try? self.getValue(at: index)) ?? .undefined
306
324
  }
307
325
  nonmutating set {
308
- guard let runtime else {
326
+ guard let jsiRuntime else {
309
327
  FatalError.runtimeLost()
310
328
  }
311
329
  if index >= length {
312
330
  self.length = index + 1
313
331
  }
314
- expo.setValueAtIndex(runtime.pointee, pointee, index, newValue.pointee)
332
+ expo.setValueAtIndex(jsiRuntime, pointee, index, newValue.pointee)
315
333
  }
316
334
  }
317
335
 
@@ -401,18 +419,18 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
401
419
  /// array elements by index, use the numeric subscript `array[0]` instead of `array["0"]`.
402
420
  public subscript(key: String) -> JavaScriptValue {
403
421
  get {
404
- guard let runtime else {
422
+ guard let jsiRuntime else {
405
423
  FatalError.runtimeLost()
406
424
  }
407
- let jsiValue = expo.getProperty(runtime.pointee, pointee, key.toJSIPropNameID(in: runtime.pointee))
408
- return JavaScriptValue(runtime, jsiValue)
425
+ let jsiValue = expo.getProperty(jsiRuntime, pointee, key.toJSIPropNameID(in: jsiRuntime))
426
+ return JavaScriptValue(runtimeHandle, jsiValue)
409
427
  }
410
428
  nonmutating set(newValue) {
411
- guard let runtime else {
429
+ guard let jsiRuntime else {
412
430
  FatalError.runtimeLost()
413
431
  }
414
- let jsiValue = newValue.toJSIValue(in: runtime.pointee)
415
- expo.setProperty(runtime.pointee, pointee, key.toJSIPropNameID(in: runtime.pointee), jsiValue)
432
+ let jsiValue = newValue.toJSIValue(in: jsiRuntime)
433
+ expo.setProperty(jsiRuntime, pointee, key.toJSIPropNameID(in: jsiRuntime), jsiValue)
416
434
  }
417
435
  }
418
436
 
@@ -429,14 +447,14 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
429
447
  /// - Note: This method uses Swift's standard `map` semantics and follows the `rethrows`
430
448
  /// pattern, meaning it only throws if the transform closure throws.
431
449
  public func map<T>(_ transform: (_ value: JavaScriptValue) throws -> T) rethrows -> [T] {
432
- guard let runtime else {
450
+ guard let jsiRuntime else {
433
451
  FatalError.runtimeLost()
434
452
  }
435
453
  let count = self.length
436
454
  var result: [T] = []
437
455
  result.reserveCapacity(count)
438
456
  for index in 0..<count {
439
- let value = getValueUnchecked(at: index, in: runtime)
457
+ let value = getValueUnchecked(at: index, in: jsiRuntime)
440
458
  result.append(try transform(value))
441
459
  }
442
460
  return result
@@ -449,10 +467,10 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
449
467
  /// array, so modifications to the array in JavaScript will be reflected in the value.
450
468
  /// - SeeAlso: `JavaScriptValue.getArray()` for the inverse operation
451
469
  public func asValue() -> JavaScriptValue {
452
- guard let runtime else {
470
+ guard let jsiRuntime else {
453
471
  FatalError.runtimeLost()
454
472
  }
455
- return JavaScriptValue(runtime, expo.valueFromArray(runtime.pointee, pointee))
473
+ return JavaScriptValue(runtimeHandle, expo.valueFromArray(jsiRuntime, pointee))
456
474
  }
457
475
 
458
476
  /// Converts the array to a `JavaScriptObject`.
@@ -509,38 +527,38 @@ extension JavaScriptArray {
509
527
  ///
510
528
  /// - Note: Eagerly evaluates all elements. For large arrays, prefer `forEach(_:)`.
511
529
  public func enumerated() -> [(offset: Int, element: JavaScriptValue)] {
512
- guard let runtime else {
530
+ guard let jsiRuntime else {
513
531
  FatalError.runtimeLost()
514
532
  }
515
533
  let count = self.length
516
534
  var result: [(offset: Int, element: JavaScriptValue)] = []
517
535
  result.reserveCapacity(count)
518
536
  for index in 0..<count {
519
- result.append((offset: index, element: getValueUnchecked(at: index, in: runtime)))
537
+ result.append((offset: index, element: getValueUnchecked(at: index, in: jsiRuntime)))
520
538
  }
521
539
  return result
522
540
  }
523
541
 
524
542
  /// Calls the given closure on each element in the array.
525
543
  public func forEach(_ body: (JavaScriptValue) throws -> Void) rethrows {
526
- guard let runtime else {
544
+ guard let jsiRuntime else {
527
545
  FatalError.runtimeLost()
528
546
  }
529
547
  let count = self.length
530
548
  for index in 0..<count {
531
- try body(getValueUnchecked(at: index, in: runtime))
549
+ try body(getValueUnchecked(at: index, in: jsiRuntime))
532
550
  }
533
551
  }
534
552
 
535
553
  /// Returns an array of elements satisfying the given predicate.
536
554
  public func filter(_ isIncluded: (JavaScriptValue) throws -> Bool) rethrows -> [JavaScriptValue] {
537
- guard let runtime else {
555
+ guard let jsiRuntime else {
538
556
  FatalError.runtimeLost()
539
557
  }
540
558
  let count = self.length
541
559
  var result: [JavaScriptValue] = []
542
560
  for index in 0..<count {
543
- let value = getValueUnchecked(at: index, in: runtime)
561
+ let value = getValueUnchecked(at: index, in: jsiRuntime)
544
562
  if try isIncluded(value) {
545
563
  result.append(value)
546
564
  }
@@ -552,13 +570,13 @@ extension JavaScriptArray {
552
570
  public func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, JavaScriptValue) throws -> Result)
553
571
  rethrows -> Result
554
572
  {
555
- guard let runtime else {
573
+ guard let jsiRuntime else {
556
574
  FatalError.runtimeLost()
557
575
  }
558
576
  let count = self.length
559
577
  var result = initialResult
560
578
  for index in 0..<count {
561
- result = try nextPartialResult(result, getValueUnchecked(at: index, in: runtime))
579
+ result = try nextPartialResult(result, getValueUnchecked(at: index, in: jsiRuntime))
562
580
  }
563
581
  return result
564
582
  }
@@ -92,10 +92,10 @@ public struct JavaScriptFunction: JavaScriptType, ~Copyable {
92
92
  // MARK: - Conversions
93
93
 
94
94
  public func asValue() -> JavaScriptValue {
95
- guard let jsiRuntime = runtime?.pointee else {
95
+ guard let runtime else {
96
96
  FatalError.runtimeLost()
97
97
  }
98
- return JavaScriptValue(runtime, expo.valueFromFunction(jsiRuntime, pointee))
98
+ return JavaScriptValue(runtime, expo.valueFromFunction(runtime.pointee, pointee))
99
99
  }
100
100
 
101
101
  /// Returns the function as a `facebook.jsi.Value` instance.