expo-modules-jsi 58.0.5 → 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.
- package/CHANGELOG.md +8 -0
- package/apple/Benchmarks/HostObjectBenchmarks.swift +57 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +4 -6
- package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +2 -2
- package/apple/Sources/ExpoModulesJSI/Protocols/JavaScriptRepresentable.swift +2 -3
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +24 -17
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntimeHandle.swift +67 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptValuesBuffer.swift +3 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +53 -35
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptFunction.swift +2 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +79 -59
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +63 -46
- package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +10 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObject.h +2 -2
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObjectCallbacks.h +17 -8
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +20 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +40 -0
- package/apple/Tests/JavaScriptValueTests.swift +18 -0
- package/apple/Tests/JavaScriptValuesBufferTests.swift +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 58.0.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
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))
|
|
10
|
+
|
|
3
11
|
## 58.0.5
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
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.
|
|
15
|
+
guard let jsiRuntime = value.jsiRuntime else {
|
|
16
16
|
FatalError.runtimeLost()
|
|
17
17
|
}
|
|
18
|
-
return Self.fromJSIValue(value.pointee, in: jsiRuntime
|
|
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
|
|
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(
|
|
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<
|
|
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<
|
|
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
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
231
|
+
guard let jsiRuntime else {
|
|
214
232
|
FatalError.runtimeLost()
|
|
215
233
|
}
|
|
216
|
-
return pointee.size(
|
|
234
|
+
return pointee.size(jsiRuntime)
|
|
217
235
|
}
|
|
218
236
|
nonmutating set(newLength) {
|
|
219
|
-
guard let
|
|
237
|
+
guard let jsiRuntime else {
|
|
220
238
|
FatalError.runtimeLost()
|
|
221
239
|
}
|
|
222
|
-
expo.setArrayLength(
|
|
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
|
|
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(
|
|
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
|
|
248
|
-
return JavaScriptValue(
|
|
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
|
|
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(
|
|
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
|
|
422
|
+
guard let jsiRuntime else {
|
|
405
423
|
FatalError.runtimeLost()
|
|
406
424
|
}
|
|
407
|
-
let jsiValue = expo.getProperty(
|
|
408
|
-
return JavaScriptValue(
|
|
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
|
|
429
|
+
guard let jsiRuntime else {
|
|
412
430
|
FatalError.runtimeLost()
|
|
413
431
|
}
|
|
414
|
-
let jsiValue = newValue.toJSIValue(in:
|
|
415
|
-
expo.setProperty(
|
|
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
|
|
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:
|
|
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
|
|
470
|
+
guard let jsiRuntime else {
|
|
453
471
|
FatalError.runtimeLost()
|
|
454
472
|
}
|
|
455
|
-
return JavaScriptValue(
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
95
|
+
guard let runtime else {
|
|
96
96
|
FatalError.runtimeLost()
|
|
97
97
|
}
|
|
98
|
-
return JavaScriptValue(runtime, expo.valueFromFunction(
|
|
98
|
+
return JavaScriptValue(runtime, expo.valueFromFunction(runtime.pointee, pointee))
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/// Returns the function as a `facebook.jsi.Value` instance.
|