expo-modules-jsi 58.0.6 → 58.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/apple/Benchmarks/FreeFormBenchmarks.swift +61 -0
- package/apple/Benchmarks/RuntimeCacheBenchmarks.swift +71 -0
- package/apple/Benchmarks/UnownedDecodeBenchmarks.swift +121 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Any.swift +500 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Containers.swift +52 -10
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Data.swift +5 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Date.swift +27 -5
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +75 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptDecodable.swift +34 -5
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptValueKinds.swift +73 -0
- package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +4 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +18 -13
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntimeCache.swift +74 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +26 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +30 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +14 -14
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +17 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +64 -2
- package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +4 -3
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +27 -0
- package/apple/Tests/JavaScriptCodableAnyTests.swift +531 -0
- package/apple/Tests/JavaScriptDecodableKindsTests.swift +132 -0
- package/apple/Tests/JavaScriptRuntimeCacheTests.swift +117 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +59 -0
- package/apple/Tests/JavaScriptUnownedDecodeTests.swift +99 -0
- package/apple/Tests/JavaScriptUnownedElementsTests.swift +76 -0
- package/apple/scripts/build-xcframework.sh +15 -0
- package/package.json +1 -1
|
@@ -15,6 +15,11 @@ import CoreGraphics
|
|
|
15
15
|
// MARK: - Bool
|
|
16
16
|
|
|
17
17
|
extension Bool: JavaScriptCodable {
|
|
18
|
+
@inlinable
|
|
19
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
20
|
+
return .bool
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
@JavaScriptActor
|
|
19
24
|
@inlinable
|
|
20
25
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Bool
|
|
@@ -40,6 +45,11 @@ extension Bool: JavaScriptCodable {
|
|
|
40
45
|
// MARK: - String
|
|
41
46
|
|
|
42
47
|
extension String: JavaScriptCodable {
|
|
48
|
+
@inlinable
|
|
49
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
50
|
+
return .string
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
@JavaScriptActor
|
|
44
54
|
@inlinable
|
|
45
55
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
@@ -74,6 +84,11 @@ extension String: JavaScriptCodable {
|
|
|
74
84
|
// undefined behavior — a native crash, not a catchable error.
|
|
75
85
|
|
|
76
86
|
extension Double: JavaScriptCodable {
|
|
87
|
+
@inlinable
|
|
88
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
89
|
+
return .number
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
@JavaScriptActor
|
|
78
93
|
@inlinable
|
|
79
94
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
@@ -100,6 +115,11 @@ extension Double: JavaScriptCodable {
|
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
extension Float: JavaScriptCodable {
|
|
118
|
+
@inlinable
|
|
119
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
120
|
+
return .number
|
|
121
|
+
}
|
|
122
|
+
|
|
103
123
|
@JavaScriptActor
|
|
104
124
|
@inlinable
|
|
105
125
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Float
|
|
@@ -123,6 +143,11 @@ extension Float: JavaScriptCodable {
|
|
|
123
143
|
}
|
|
124
144
|
|
|
125
145
|
extension CGFloat: JavaScriptCodable {
|
|
146
|
+
@inlinable
|
|
147
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
148
|
+
return .number
|
|
149
|
+
}
|
|
150
|
+
|
|
126
151
|
@JavaScriptActor
|
|
127
152
|
@inlinable
|
|
128
153
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
@@ -170,6 +195,11 @@ extension CGFloat: JavaScriptCodable {
|
|
|
170
195
|
// back, and a JS caller may pass either form.
|
|
171
196
|
|
|
172
197
|
extension Int: JavaScriptCodable {
|
|
198
|
+
@inlinable
|
|
199
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
200
|
+
return [.number, .bigint]
|
|
201
|
+
}
|
|
202
|
+
|
|
173
203
|
@JavaScriptActor
|
|
174
204
|
@inlinable
|
|
175
205
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Int {
|
|
@@ -192,6 +222,11 @@ extension Int: JavaScriptCodable {
|
|
|
192
222
|
}
|
|
193
223
|
|
|
194
224
|
extension Int8: JavaScriptCodable {
|
|
225
|
+
@inlinable
|
|
226
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
227
|
+
return .number
|
|
228
|
+
}
|
|
229
|
+
|
|
195
230
|
@JavaScriptActor
|
|
196
231
|
@inlinable
|
|
197
232
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Int8
|
|
@@ -215,6 +250,11 @@ extension Int8: JavaScriptCodable {
|
|
|
215
250
|
}
|
|
216
251
|
|
|
217
252
|
extension Int16: JavaScriptCodable {
|
|
253
|
+
@inlinable
|
|
254
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
255
|
+
return .number
|
|
256
|
+
}
|
|
257
|
+
|
|
218
258
|
@JavaScriptActor
|
|
219
259
|
@inlinable
|
|
220
260
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Int16
|
|
@@ -238,6 +278,11 @@ extension Int16: JavaScriptCodable {
|
|
|
238
278
|
}
|
|
239
279
|
|
|
240
280
|
extension Int32: JavaScriptCodable {
|
|
281
|
+
@inlinable
|
|
282
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
283
|
+
return .number
|
|
284
|
+
}
|
|
285
|
+
|
|
241
286
|
@JavaScriptActor
|
|
242
287
|
@inlinable
|
|
243
288
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Int32
|
|
@@ -261,6 +306,11 @@ extension Int32: JavaScriptCodable {
|
|
|
261
306
|
}
|
|
262
307
|
|
|
263
308
|
extension Int64: JavaScriptCodable {
|
|
309
|
+
@inlinable
|
|
310
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
311
|
+
return [.number, .bigint]
|
|
312
|
+
}
|
|
313
|
+
|
|
264
314
|
@JavaScriptActor
|
|
265
315
|
@inlinable
|
|
266
316
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Int64
|
|
@@ -284,6 +334,11 @@ extension Int64: JavaScriptCodable {
|
|
|
284
334
|
}
|
|
285
335
|
|
|
286
336
|
extension UInt: JavaScriptCodable {
|
|
337
|
+
@inlinable
|
|
338
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
339
|
+
return [.number, .bigint]
|
|
340
|
+
}
|
|
341
|
+
|
|
287
342
|
@JavaScriptActor
|
|
288
343
|
@inlinable
|
|
289
344
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> UInt
|
|
@@ -307,6 +362,11 @@ extension UInt: JavaScriptCodable {
|
|
|
307
362
|
}
|
|
308
363
|
|
|
309
364
|
extension UInt8: JavaScriptCodable {
|
|
365
|
+
@inlinable
|
|
366
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
367
|
+
return .number
|
|
368
|
+
}
|
|
369
|
+
|
|
310
370
|
@JavaScriptActor
|
|
311
371
|
@inlinable
|
|
312
372
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> UInt8
|
|
@@ -330,6 +390,11 @@ extension UInt8: JavaScriptCodable {
|
|
|
330
390
|
}
|
|
331
391
|
|
|
332
392
|
extension UInt16: JavaScriptCodable {
|
|
393
|
+
@inlinable
|
|
394
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
395
|
+
return .number
|
|
396
|
+
}
|
|
397
|
+
|
|
333
398
|
@JavaScriptActor
|
|
334
399
|
@inlinable
|
|
335
400
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
@@ -356,6 +421,11 @@ extension UInt16: JavaScriptCodable {
|
|
|
356
421
|
}
|
|
357
422
|
|
|
358
423
|
extension UInt32: JavaScriptCodable {
|
|
424
|
+
@inlinable
|
|
425
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
426
|
+
return .number
|
|
427
|
+
}
|
|
428
|
+
|
|
359
429
|
@JavaScriptActor
|
|
360
430
|
@inlinable
|
|
361
431
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
@@ -382,6 +452,11 @@ extension UInt32: JavaScriptCodable {
|
|
|
382
452
|
}
|
|
383
453
|
|
|
384
454
|
extension UInt64: JavaScriptCodable {
|
|
455
|
+
@inlinable
|
|
456
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
457
|
+
return [.number, .bigint]
|
|
458
|
+
}
|
|
459
|
+
|
|
385
460
|
@JavaScriptActor
|
|
386
461
|
@inlinable
|
|
387
462
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
public protocol JavaScriptDecodable {
|
|
14
14
|
/// Decodes an owning `JavaScriptValue` into `Self`.
|
|
15
15
|
///
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
///
|
|
16
|
+
/// Defaulted (see the extension below) to borrow the value and decode it through the
|
|
17
|
+
/// `JavaScriptUnownedValue` overload, so a conformer may implement just that one. A conformer must
|
|
18
|
+
/// implement at least one of the two overloads: each default forwards to the other, so with neither
|
|
19
|
+
/// implemented a decode recurses until the stack overflows.
|
|
19
20
|
@JavaScriptActor
|
|
20
21
|
static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Self
|
|
21
22
|
|
|
@@ -24,13 +25,35 @@ public protocol JavaScriptDecodable {
|
|
|
24
25
|
///
|
|
25
26
|
/// This is the argument-decode fast path: the value borrows the argument the
|
|
26
27
|
/// `JavaScriptValuesBuffer` already owns for the duration of the call. It is defaulted (see the
|
|
27
|
-
/// extension below)
|
|
28
|
-
/// can read straight from the borrowed value
|
|
28
|
+
/// extension below) to materialize an owning value and decode that, for conformers that implement
|
|
29
|
+
/// only the owning overload; types that can read straight from the borrowed value implement it to
|
|
30
|
+
/// stay zero-copy.
|
|
29
31
|
@JavaScriptActor
|
|
30
32
|
static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws -> Self
|
|
33
|
+
|
|
34
|
+
/// The kinds of JavaScript value `decode` can accept.
|
|
35
|
+
///
|
|
36
|
+
/// A kind outside the set is definitive: `decode` would throw for a value of that kind. A kind in the
|
|
37
|
+
/// set only means the type may decode it, so `decode` can still throw, for example for an
|
|
38
|
+
/// out-of-range number or a missing record field. Code that picks between several types, like a
|
|
39
|
+
/// union, reads it to skip the ones that can't match without paying for a thrown error. Defaults to
|
|
40
|
+
/// `.all`, so a type that doesn't declare it is always tried.
|
|
41
|
+
static var decodableKinds: JavaScriptValueKinds { get }
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
extension JavaScriptDecodable {
|
|
45
|
+
/// Default owning decode: borrow the value and forward to the unowned overload, so a conformer that
|
|
46
|
+
/// implements only that one decodes owning values without a copy.
|
|
47
|
+
@JavaScriptActor
|
|
48
|
+
@inlinable
|
|
49
|
+
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Self
|
|
50
|
+
{
|
|
51
|
+
let runtime = copy runtime
|
|
52
|
+
return try value.withUnownedValue(in: runtime) { unownedValue in
|
|
53
|
+
return try decode(unownedValue, in: runtime)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
/// Default fast-path implementation: materialize an owning value and forward to the requirement.
|
|
35
58
|
/// This is the "explicitly copy" behavior for types that cannot (or need not) read directly from
|
|
36
59
|
/// the borrowed value.
|
|
@@ -44,4 +67,10 @@ extension JavaScriptDecodable {
|
|
|
44
67
|
{
|
|
45
68
|
return try decode(value.copied(in: runtime), in: runtime)
|
|
46
69
|
}
|
|
70
|
+
|
|
71
|
+
/// Default: every kind, so a conformer that doesn't declare its kinds is always tried.
|
|
72
|
+
@inlinable
|
|
73
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
74
|
+
return .all
|
|
75
|
+
}
|
|
47
76
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
internal import jsi
|
|
4
|
+
|
|
5
|
+
/// A set of JavaScript value kinds, as told apart by the value's type tag alone.
|
|
6
|
+
///
|
|
7
|
+
/// Unlike `JavaScriptValue.Kind`, a function is an `object` here: telling it apart needs a call into
|
|
8
|
+
/// the runtime, while every kind in this set is read from the tag without one. Used by
|
|
9
|
+
/// `JavaScriptDecodable.decodableKinds` to describe the kinds a type can decode from. Frozen, so a check
|
|
10
|
+
/// against it compiles to a mask test in the client.
|
|
11
|
+
@frozen
|
|
12
|
+
public struct JavaScriptValueKinds: OptionSet, Sendable {
|
|
13
|
+
public let rawValue: UInt16
|
|
14
|
+
|
|
15
|
+
// `UInt16` leaves room for more kinds without changing the frozen layout.
|
|
16
|
+
//
|
|
17
|
+
// The kinds are computed and inlinable rather than stored `static let`s, which a client would reach
|
|
18
|
+
// through a lazily initialized global, so a mask built from them folds to a constant.
|
|
19
|
+
|
|
20
|
+
@inlinable
|
|
21
|
+
public init(rawValue: UInt16) {
|
|
22
|
+
self.rawValue = rawValue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@inlinable public static var undefined: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 0) }
|
|
26
|
+
@inlinable public static var null: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 1) }
|
|
27
|
+
@inlinable public static var bool: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 2) }
|
|
28
|
+
@inlinable public static var number: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 3) }
|
|
29
|
+
@inlinable public static var bigint: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 4) }
|
|
30
|
+
@inlinable public static var string: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 5) }
|
|
31
|
+
@inlinable public static var symbol: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 6) }
|
|
32
|
+
/// Any object, including arrays, typed arrays and functions.
|
|
33
|
+
@inlinable public static var object: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 7) }
|
|
34
|
+
|
|
35
|
+
/// Every kind, including any added later: the bits above the current kinds are set too, so a type that
|
|
36
|
+
/// accepts every kind keeps doing so.
|
|
37
|
+
@inlinable public static var all: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: .max) }
|
|
38
|
+
|
|
39
|
+
/// The kind of `value`.
|
|
40
|
+
///
|
|
41
|
+
/// Not inlinable on purpose: the type checks read the `jsi::Value`, which clients can't see, so an
|
|
42
|
+
/// inlined initializer would make one call into this module per check. Here it is a single call.
|
|
43
|
+
public init(of value: borrowing JavaScriptValue) {
|
|
44
|
+
self.init(of: value.pointee)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// The kind of the borrowed `value`, read the same way as from an owning value.
|
|
48
|
+
public init(of value: borrowing JavaScriptUnownedValue) {
|
|
49
|
+
self.init(of: value.pointer.pointee)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Reads the kind from the `jsi::Value` both public initializers wrap. The checks go from the most to
|
|
53
|
+
/// the least common kind of an argument.
|
|
54
|
+
private init(of value: borrowing facebook.jsi.Value) {
|
|
55
|
+
if value.isNumber() {
|
|
56
|
+
self = .number
|
|
57
|
+
} else if value.isString() {
|
|
58
|
+
self = .string
|
|
59
|
+
} else if value.isObject() {
|
|
60
|
+
self = .object
|
|
61
|
+
} else if value.isBool() {
|
|
62
|
+
self = .bool
|
|
63
|
+
} else if value.isNull() {
|
|
64
|
+
self = .null
|
|
65
|
+
} else if value.isUndefined() {
|
|
66
|
+
self = .undefined
|
|
67
|
+
} else if value.isBigInt() {
|
|
68
|
+
self = .bigint
|
|
69
|
+
} else {
|
|
70
|
+
self = .symbol
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -93,14 +93,16 @@ extension String: JSIRepresentable {
|
|
|
93
93
|
// allocation, copy and free per string. `withUTF8` is mutating (it makes a bridged string
|
|
94
94
|
// contiguous first), hence the local copy; native strings are already contiguous and pay nothing.
|
|
95
95
|
// The value is moved out through a local because `withUTF8` needs a `Copyable` closure result.
|
|
96
|
+
// The C++ helper moves the engine's `jsi::String` into the value, so this costs one engine handle;
|
|
97
|
+
// `jsi::Value(runtime, string)` would clone it and then release the original.
|
|
96
98
|
var string = self
|
|
97
99
|
var value = facebook.jsi.Value.undefined()
|
|
98
100
|
string.withUTF8 { utf8 in
|
|
99
101
|
guard let base = utf8.baseAddress else {
|
|
100
|
-
value =
|
|
102
|
+
value = expo.createStringValueFromAscii(runtime, "", 0)
|
|
101
103
|
return
|
|
102
104
|
}
|
|
103
|
-
value =
|
|
105
|
+
value = expo.createStringValueFromUtf8(runtime, base, utf8.count)
|
|
104
106
|
}
|
|
105
107
|
return value
|
|
106
108
|
}
|
|
@@ -146,7 +146,7 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
146
146
|
// when the last reference is gone. `deinit` is `nonisolated`, so it can touch the actor-isolated
|
|
147
147
|
// registry directly given that exclusive access.
|
|
148
148
|
propNameIdsRegistry.removeAll()
|
|
149
|
-
|
|
149
|
+
cache.clear()
|
|
150
150
|
expo.destroyRuntime(runtimePointee)
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -203,7 +203,8 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
203
203
|
let propertyName = String(jsiPropNameID: propertyName.pointee, in: runtime.pointee)
|
|
204
204
|
return JavaScriptActor.assumeIsolated {
|
|
205
205
|
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
206
|
-
try context.get(propertyName)
|
|
206
|
+
var result = try context.get(propertyName)
|
|
207
|
+
JavaScriptValue.write(&result, to: resultPtr)
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
}
|
|
@@ -789,12 +790,14 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
789
790
|
@JavaScriptActor
|
|
790
791
|
internal var propNameIdsRegistry: [String: JavaScriptPropNameID] = [:]
|
|
791
792
|
|
|
792
|
-
// MARK: -
|
|
793
|
+
// MARK: - Cache
|
|
793
794
|
|
|
794
|
-
///
|
|
795
|
-
///
|
|
795
|
+
/// Values cached with ``cached(_:_:)``. Unchecked exclusivity skips the dynamic access checks on
|
|
796
|
+
/// every lookup: the cache is only used on the JavaScript thread, and `cached(_:_:)` never keeps an
|
|
797
|
+
/// access open while it calls out, so accesses can't overlap.
|
|
796
798
|
@JavaScriptActor
|
|
797
|
-
|
|
799
|
+
@exclusivity(unchecked)
|
|
800
|
+
internal var cache = Cache()
|
|
798
801
|
|
|
799
802
|
// MARK: - Long-lived objects
|
|
800
803
|
|
|
@@ -826,12 +829,12 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
826
829
|
// hop back to the JavaScript thread first.
|
|
827
830
|
JavaScriptActor.assumeIsolated {
|
|
828
831
|
longLivedObjects.clear()
|
|
829
|
-
// Also flush the cached `jsi::PropNameID`s and the
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
832
|
+
// Also flush the cached `jsi::PropNameID`s and the cache: a non-owning wrapper can outlive its
|
|
833
|
+
// runtime (e.g. captured by a task abandoned on reload) and would otherwise destroy them against
|
|
834
|
+
// the freed runtime when it deallocates. `self` is weak so the teardown object doesn't retain the
|
|
835
|
+
// wrapper; the owning wrapper clears both in `deinit`.
|
|
833
836
|
self?.propNameIdsRegistry.removeAll()
|
|
834
|
-
self?.
|
|
837
|
+
self?.cache.clear()
|
|
835
838
|
}
|
|
836
839
|
}
|
|
837
840
|
let object = createObject()
|
|
@@ -883,7 +886,8 @@ private func createFunctionClosure(
|
|
|
883
886
|
let this = UnsafeMutablePointer(mutating: thisPtr).move()
|
|
884
887
|
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr, count: argumentsCount)
|
|
885
888
|
let thisValue = JavaScriptValue(runtime, this)
|
|
886
|
-
try context.call(thisValue, consume arguments)
|
|
889
|
+
var result = try context.call(thisValue, consume arguments)
|
|
890
|
+
JavaScriptValue.write(&result, to: resultPtr)
|
|
887
891
|
}
|
|
888
892
|
}
|
|
889
893
|
}
|
|
@@ -926,7 +930,8 @@ private func createFunctionClosure(
|
|
|
926
930
|
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
927
931
|
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr, count: argumentsCount)
|
|
928
932
|
let thisValue = JavaScriptUnownedValue(runtime.pointee, thisPtr)
|
|
929
|
-
try context.call(thisValue, consume arguments)
|
|
933
|
+
var result = try context.call(thisValue, consume arguments)
|
|
934
|
+
JavaScriptValue.write(&result, to: resultPtr)
|
|
930
935
|
}
|
|
931
936
|
}
|
|
932
937
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
extension JavaScriptRuntime {
|
|
6
|
+
/// Values cached by a runtime, stored by their keys' indices. Isolated to the JavaScript thread
|
|
7
|
+
/// together with the runtime that owns it. Values are read and created through
|
|
8
|
+
/// ``JavaScriptRuntime/cached(_:_:)``.
|
|
9
|
+
public struct Cache: ~Copyable {
|
|
10
|
+
/// A key for a value that a runtime creates once and then reuses, such as a JavaScript constructor
|
|
11
|
+
/// or a property name. Declare keys as `static let`s and pass them to ``JavaScriptRuntime/cached(_:_:)``.
|
|
12
|
+
///
|
|
13
|
+
/// Each key gets a fixed index from a process-wide counter when it is created, so a lookup reads one
|
|
14
|
+
/// array slot instead of hashing a string. Every runtime keeps its own values for the same keys.
|
|
15
|
+
public struct Key<Value: AnyObject>: Sendable {
|
|
16
|
+
internal let index: Int
|
|
17
|
+
|
|
18
|
+
public init() {
|
|
19
|
+
self.index = nextCacheKeyIndex.withLock { index in
|
|
20
|
+
defer { index += 1 }
|
|
21
|
+
return index
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A slot per key index, up to the highest index used so far in this runtime. Slots of keys that
|
|
27
|
+
// haven't been used here yet are `nil`. `ContiguousArray` stores the references directly, without
|
|
28
|
+
// the bridging checks that `Array` can do for class elements on Apple platforms.
|
|
29
|
+
private var storage = ContiguousArray<AnyObject?>()
|
|
30
|
+
|
|
31
|
+
internal func value<Value>(for key: Key<Value>) -> Value? {
|
|
32
|
+
guard key.index < storage.count, let value = storage[key.index] else {
|
|
33
|
+
return nil
|
|
34
|
+
}
|
|
35
|
+
// The key's type guarantees the type of the value stored under it.
|
|
36
|
+
return unsafeDowncast(value, to: Value.self)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
internal mutating func store<Value>(_ value: Value, for key: Key<Value>) {
|
|
40
|
+
if key.index >= storage.count {
|
|
41
|
+
storage.append(contentsOf: repeatElement(nil, count: key.index - storage.count + 1))
|
|
42
|
+
}
|
|
43
|
+
storage[key.index] = value
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Releases all cached values. They often hold JSI objects, which must be destroyed before the
|
|
47
|
+
/// runtime they belong to.
|
|
48
|
+
internal mutating func clear() {
|
|
49
|
+
storage.removeAll()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Returns the value cached under `key`, creating it with `make` the first time the key is used in
|
|
54
|
+
/// this runtime. If `make` throws, nothing is cached and the next call tries again.
|
|
55
|
+
///
|
|
56
|
+
/// `make` may look up other cached values, for example to build a prototype from a cached
|
|
57
|
+
/// constructor.
|
|
58
|
+
@JavaScriptActor
|
|
59
|
+
public func cached<Value>(_ key: Cache.Key<Value>, _ make: () throws -> Value) rethrows -> Value {
|
|
60
|
+
if let value = cache.value(for: key) {
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
// Call `make` without accessing the cache, so that the lookups it does itself don't overlap with
|
|
64
|
+
// an access in progress here.
|
|
65
|
+
let value = try make()
|
|
66
|
+
cache.store(value, for: key)
|
|
67
|
+
return value
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The index for the next created ``JavaScriptRuntime/Cache/Key``. Keys can be created on any thread,
|
|
72
|
+
/// for example by initializing a `static let`, so the counter is behind a lock. That costs nothing on
|
|
73
|
+
/// the lookup path, since each key takes an index only once.
|
|
74
|
+
private let nextCacheKeyIndex = OSAllocatedUnfairLock(initialState: 0)
|
|
@@ -460,6 +460,32 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
|
|
|
460
460
|
return result
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
/// Transforms each element like `map(_:)`, but lends each element to `transform` as a
|
|
464
|
+
/// `JavaScriptUnownedValue` instead of wrapping it in a new `JavaScriptValue`. An element is valid only
|
|
465
|
+
/// for the duration of its `transform` call and must not be stored or escaped.
|
|
466
|
+
public func mapUnowned<T>(_ transform: (borrowing JavaScriptUnownedValue) throws -> T) rethrows -> [T] {
|
|
467
|
+
guard let jsiRuntime else {
|
|
468
|
+
FatalError.runtimeLost()
|
|
469
|
+
}
|
|
470
|
+
let count = self.length
|
|
471
|
+
var result: [T] = []
|
|
472
|
+
result.reserveCapacity(count)
|
|
473
|
+
for index in 0..<count {
|
|
474
|
+
let element = pointee.getValueAtIndex(jsiRuntime, index)
|
|
475
|
+
// `withUnsafeBytes(of:)` rather than `withUnsafePointer(to:)`; see `JavaScriptValue.withUnsafePointee(_:)`.
|
|
476
|
+
try withUnsafeBytes(of: element) { bytes in
|
|
477
|
+
guard let baseAddress = bytes.baseAddress else {
|
|
478
|
+
preconditionFailure(
|
|
479
|
+
"withUnsafeBytes(of:) gave an empty buffer for a jsi::Value, which can't happen for a non-zero-sized type"
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
let pointer = baseAddress.assumingMemoryBound(to: facebook.jsi.Value.self)
|
|
483
|
+
result.append(try transform(JavaScriptUnownedValue(jsiRuntime, pointer)))
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return result
|
|
487
|
+
}
|
|
488
|
+
|
|
463
489
|
/// Converts the JavaScript array to a `JavaScriptValue`.
|
|
464
490
|
///
|
|
465
491
|
/// - Returns: A `JavaScriptValue` representing this array
|
|
@@ -92,6 +92,14 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
92
92
|
return pointee.isArrayBuffer(jsiRuntime)
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/// Returns `true` if the object is a typed array, such as `Uint8Array`.
|
|
96
|
+
internal func isTypedArray() -> Bool {
|
|
97
|
+
guard let runtime else {
|
|
98
|
+
FatalError.runtimeLost()
|
|
99
|
+
}
|
|
100
|
+
return expo.isTypedArray(runtime.pointee, pointee)
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
/// Returns the object as an array buffer, or asserts if not an array buffer.
|
|
96
104
|
public func getArrayBuffer() -> JavaScriptArrayBuffer {
|
|
97
105
|
guard let runtime else {
|
|
@@ -138,6 +146,28 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
138
146
|
return JavaScriptValue(runtimeHandle, pointee.getProperty(jsiRuntime, name.toJSIPropNameID(in: jsiRuntime)))
|
|
139
147
|
}
|
|
140
148
|
|
|
149
|
+
/// Calls `body` with the property of the object with the given name, or `undefined` if there is no
|
|
150
|
+
/// such property, lent as a `JavaScriptUnownedValue` instead of wrapped in a new `JavaScriptValue`.
|
|
151
|
+
/// The value is valid only for the duration of the closure and must not be stored or escaped.
|
|
152
|
+
public func withUnownedProperty<R>(_ name: String, _ body: (borrowing JavaScriptUnownedValue) throws -> R) rethrows
|
|
153
|
+
-> R
|
|
154
|
+
{
|
|
155
|
+
guard let jsiRuntime else {
|
|
156
|
+
FatalError.runtimeLost()
|
|
157
|
+
}
|
|
158
|
+
let property = pointee.getProperty(jsiRuntime, name.toJSIPropNameID(in: jsiRuntime))
|
|
159
|
+
// `withUnsafeBytes(of:)` rather than `withUnsafePointer(to:)`; see `JavaScriptValue.withUnsafePointee(_:)`.
|
|
160
|
+
return try withUnsafeBytes(of: property) { bytes in
|
|
161
|
+
guard let baseAddress = bytes.baseAddress else {
|
|
162
|
+
preconditionFailure(
|
|
163
|
+
"withUnsafeBytes(of:) gave an empty buffer for a jsi::Value, which can't happen for a non-zero-sized type"
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
let pointer = baseAddress.assumingMemoryBound(to: facebook.jsi.Value.self)
|
|
167
|
+
return try body(JavaScriptUnownedValue(jsiRuntime, pointer))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
141
171
|
/// Returns the property of the object with the given prop name id,
|
|
142
172
|
/// or `undefined` value if the name is not a property of the object.
|
|
143
173
|
public func getProperty(_ propName: JavaScriptPropNameID) -> JavaScriptValue {
|
|
@@ -284,20 +284,20 @@ extension JavaScriptRuntime {
|
|
|
284
284
|
/// optimize like any other closure.
|
|
285
285
|
@JavaScriptActor
|
|
286
286
|
fileprivate func deferredPromiseFactory() throws -> JavaScriptValue {
|
|
287
|
-
|
|
288
|
-
return
|
|
287
|
+
return try cached(deferredPromiseFactoryKey) {
|
|
288
|
+
return try eval(
|
|
289
|
+
label: "expo-modules-jsi/deferred-promise.js",
|
|
290
|
+
"""
|
|
291
|
+
(function () {
|
|
292
|
+
let resolve, reject;
|
|
293
|
+
const promise = new Promise(function (a, b) { resolve = a; reject = b; });
|
|
294
|
+
return [promise, resolve, reject];
|
|
295
|
+
})
|
|
296
|
+
"""
|
|
297
|
+
)
|
|
289
298
|
}
|
|
290
|
-
let factory = try eval(
|
|
291
|
-
label: "expo-modules-jsi/deferred-promise.js",
|
|
292
|
-
"""
|
|
293
|
-
(function () {
|
|
294
|
-
let resolve, reject;
|
|
295
|
-
const promise = new Promise(function (a, b) { resolve = a; reject = b; });
|
|
296
|
-
return [promise, resolve, reject];
|
|
297
|
-
})
|
|
298
|
-
"""
|
|
299
|
-
)
|
|
300
|
-
cachedDeferredPromiseFactory = factory
|
|
301
|
-
return factory
|
|
302
299
|
}
|
|
303
300
|
}
|
|
301
|
+
|
|
302
|
+
/// Key of the deferred promise factory in each runtime's cache.
|
|
303
|
+
private let deferredPromiseFactoryKey = JavaScriptRuntime.Cache.Key<JavaScriptValue>()
|
|
@@ -130,6 +130,23 @@ public struct JavaScriptUnownedValue: ~Copyable {
|
|
|
130
130
|
return JavaScriptObject(runtime, pointer.pointee.getObject(self.runtime))
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/// Whether the value is an array. The zero-copy counterpart of ``JavaScriptValue/isArray()``.
|
|
134
|
+
public func isArray() -> Bool {
|
|
135
|
+
return pointer.pointee.isObject() && pointer.pointee.getObject(runtime).isArray(runtime)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Returns the value as a ``JavaScriptArray``, or asserts if it is not an array. The zero-copy
|
|
139
|
+
/// counterpart of ``JavaScriptValue/getArray()``, with the same runtime contract as
|
|
140
|
+
/// ``getObject(in:)``.
|
|
141
|
+
public func getArray(in runtime: JavaScriptRuntime) -> JavaScriptArray {
|
|
142
|
+
assert(isArray(), "Value is not an array")
|
|
143
|
+
assert(
|
|
144
|
+
Unmanaged.passUnretained(runtime.pointee).toOpaque() == Unmanaged.passUnretained(self.runtime).toOpaque(),
|
|
145
|
+
"`getArray(in:)` must be passed the runtime that owns the borrowed value"
|
|
146
|
+
)
|
|
147
|
+
return JavaScriptArray(runtime, pointer.pointee.getObject(self.runtime).getArray(self.runtime))
|
|
148
|
+
}
|
|
149
|
+
|
|
133
150
|
// MARK: - Throwing conversions ("as functions")
|
|
134
151
|
|
|
135
152
|
/// Returns the value as a boolean, or throws `TypeError` if it is not a boolean.
|