expo-modules-jsi 58.0.8 → 58.0.9
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 +10 -0
- package/apple/Benchmarks/ContainerCodingBenchmarks.swift +74 -0
- package/apple/Benchmarks/HostFunctionBenchmarks.swift +41 -0
- package/apple/Benchmarks/ValueAccessBenchmarks.swift +23 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +135 -21
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Set.swift +79 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +28 -21
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +75 -7
- package/apple/Sources/ExpoModulesJSI/Utilities/Errors.swift +19 -0
- package/apple/Sources/ExpoModulesJSI/Utilities/UncheckedSendable.swift +13 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +41 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/RuntimeScheduler.h +18 -5
- package/apple/Tests/JavaScriptCodableSetTests.swift +115 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +27 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 58.0.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [iOS] Add a `JavaScriptCodable` conformance for `Set`: it encodes to a JS `Set` and decodes from a JS `Set` or an array, collapsing duplicate elements. ([#50859](https://github.com/expo/expo/pull/50859) by [@tsapeta](https://github.com/tsapeta))
|
|
8
|
+
- `JavaScriptRuntime.evalAsync` now evaluates on the JavaScript thread when called from another thread on a runtime with a scheduler, instead of on the calling thread. ([#51015](https://github.com/expo/expo/pull/51015) by [@tsapeta](https://github.com/tsapeta))
|
|
9
|
+
- [iOS] Speed up `JavaScriptObject.defineProperty` by looking up `Object.defineProperty` once per runtime and building the descriptor in C++. ([#51004](https://github.com/expo/expo/pull/51004) by [@tsapeta](https://github.com/tsapeta))
|
|
10
|
+
- [iOS] Fix the xcframework failing to build with Xcode 26 (Swift 6.2): `RuntimeScheduler` constructors annotated with `SWIFT_RETURNS_RETAINED` were rejected, and host function and host object getter callbacks failed with `sending '...' risks causing data races`. ([#51040](https://github.com/expo/expo/pull/51040) by [@tsapeta](https://github.com/tsapeta))
|
|
11
|
+
- [iOS] Speed up integer arguments and results of native functions by running the integer conversions specialized inside `ExpoModulesJSI` instead of unspecialized in the calling module. A host function that adds two `Int`s runs about 7× faster. ([#51055](https://github.com/expo/expo/pull/51055) by [@tsapeta](https://github.com/tsapeta))
|
|
12
|
+
|
|
3
13
|
## 58.0.8
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Testing
|
|
5
|
+
|
|
6
|
+
/// Benchmarks comparing the `Array` and `Set` `JavaScriptCodable` conformances. `Set` decoding from a
|
|
7
|
+
/// JS array shares the `Array` fast path and adds only the hashing, while decoding from a JS `Set` and
|
|
8
|
+
/// encoding to one go through the global `Array.from` and `Set` constructor, so each case is measured
|
|
9
|
+
/// against its `Array` counterpart at a small and a large element count.
|
|
10
|
+
extension JSIBenchmarks {
|
|
11
|
+
// MARK: - Decode
|
|
12
|
+
|
|
13
|
+
@Test(arguments: [10, 1000])
|
|
14
|
+
func `decode an array of ints as Array`(count: Int) async throws {
|
|
15
|
+
try await benchmarkCase { runtime in
|
|
16
|
+
let value = try runtime.eval("Array.from({ length: \(count) }, (_, i) => i)")
|
|
17
|
+
try benchmark("[Int].decode: JS array of \(count)", runtime: runtime) { iterations in
|
|
18
|
+
for _ in 0..<iterations {
|
|
19
|
+
_ = try [Int].decode(value, in: runtime)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test(arguments: [10, 1000])
|
|
26
|
+
func `decode an array of ints as Set`(count: Int) async throws {
|
|
27
|
+
try await benchmarkCase { runtime in
|
|
28
|
+
let value = try runtime.eval("Array.from({ length: \(count) }, (_, i) => i)")
|
|
29
|
+
try benchmark("Set<Int>.decode: JS array of \(count)", runtime: runtime) { iterations in
|
|
30
|
+
for _ in 0..<iterations {
|
|
31
|
+
_ = try Set<Int>.decode(value, in: runtime)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test(arguments: [10, 1000])
|
|
38
|
+
func `decode a set of ints as Set`(count: Int) async throws {
|
|
39
|
+
try await benchmarkCase { runtime in
|
|
40
|
+
let value = try runtime.eval("new Set(Array.from({ length: \(count) }, (_, i) => i))")
|
|
41
|
+
try benchmark("Set<Int>.decode: JS Set of \(count)", runtime: runtime) { iterations in
|
|
42
|
+
for _ in 0..<iterations {
|
|
43
|
+
_ = try Set<Int>.decode(value, in: runtime)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// MARK: - Encode
|
|
50
|
+
|
|
51
|
+
@Test(arguments: [10, 1000])
|
|
52
|
+
func `encode an Array of ints`(count: Int) async throws {
|
|
53
|
+
try await benchmarkCase { runtime in
|
|
54
|
+
let array = Array(0..<count)
|
|
55
|
+
try benchmark("[Int].encode: \(count) elements", runtime: runtime) { iterations in
|
|
56
|
+
for _ in 0..<iterations {
|
|
57
|
+
_ = try [Int].encode(array, in: runtime)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test(arguments: [10, 1000])
|
|
64
|
+
func `encode a Set of ints`(count: Int) async throws {
|
|
65
|
+
try await benchmarkCase { runtime in
|
|
66
|
+
let set = Set(0..<count)
|
|
67
|
+
try benchmark("Set<Int>.encode: \(count) elements", runtime: runtime) { iterations in
|
|
68
|
+
for _ in 0..<iterations {
|
|
69
|
+
_ = try Set<Int>.encode(set, in: runtime)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -87,6 +87,47 @@ extension JSIBenchmarks {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// The integer codables, as bindings call them: `Int.decode` for an `Int` argument and `Int?.decode`
|
|
91
|
+
// for an optional one.
|
|
92
|
+
|
|
93
|
+
@Test
|
|
94
|
+
func `host function adding two Ints`() async throws {
|
|
95
|
+
try await benchmarkCase { runtime in
|
|
96
|
+
let fn = runtime.createFunction("add") {
|
|
97
|
+
[runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
|
|
98
|
+
let a = try Int.decode(arguments.unownedValue(at: 0), in: runtime)
|
|
99
|
+
let b = try Int.decode(arguments.unownedValue(at: 1), in: runtime)
|
|
100
|
+
return try Int.encode(a + b, in: runtime)
|
|
101
|
+
}
|
|
102
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
103
|
+
let driver = try runtime.eval(
|
|
104
|
+
"(function(n) { for (var i = 0; i < n; i++) benchFn(3, 38); })"
|
|
105
|
+
).getFunction()
|
|
106
|
+
try benchmark("host function: add two Ints", runtime: runtime) { iterations in
|
|
107
|
+
_ = try driver.call(arguments: iterations)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Test
|
|
113
|
+
func `host function adding two optional Ints`() async throws {
|
|
114
|
+
try await benchmarkCase { runtime in
|
|
115
|
+
let fn = runtime.createFunction("add") {
|
|
116
|
+
[runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
|
|
117
|
+
let a = try Int?.decode(arguments.unownedValue(at: 0), in: runtime) ?? 0
|
|
118
|
+
let b = try Int?.decode(arguments.unownedValue(at: 1), in: runtime) ?? 0
|
|
119
|
+
return try Int.encode(a + b, in: runtime)
|
|
120
|
+
}
|
|
121
|
+
runtime.global().setProperty("benchFn", value: fn)
|
|
122
|
+
let driver = try runtime.eval(
|
|
123
|
+
"(function(n) { for (var i = 0; i < n; i++) benchFn(3, 38); })"
|
|
124
|
+
).getFunction()
|
|
125
|
+
try benchmark("host function: add two optional Ints", runtime: runtime) { iterations in
|
|
126
|
+
_ = try driver.call(arguments: iterations)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
90
131
|
// MARK: - Unowned-this form with string arguments
|
|
91
132
|
|
|
92
133
|
@Test
|
|
@@ -129,4 +129,27 @@ extension JSIBenchmarks {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
@Test
|
|
134
|
+
func `define a property`() async throws {
|
|
135
|
+
try await benchmarkCase { runtime in
|
|
136
|
+
try benchmark("JavaScriptObject.defineProperty(_:value:options:)", runtime: runtime) { iterations in
|
|
137
|
+
for index in 0..<iterations {
|
|
138
|
+
let object = runtime.createObject()
|
|
139
|
+
object.defineProperty("__expo_shared_object_id__", value: index, options: [.writable])
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Test
|
|
146
|
+
func `create an object`() async throws {
|
|
147
|
+
try await benchmarkCase { runtime in
|
|
148
|
+
try benchmark("JavaScriptRuntime.createObject()", runtime: runtime) { iterations in
|
|
149
|
+
for _ in 0..<iterations {
|
|
150
|
+
_ = runtime.createObject()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
132
155
|
}
|
|
@@ -482,13 +482,134 @@ extension UInt64: JavaScriptCodable {
|
|
|
482
482
|
}
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
+
// MARK: - Integer conversion entry points
|
|
486
|
+
|
|
487
|
+
// The inlinable integer witnesses call these non-generic overloads rather than the generic
|
|
488
|
+
// implementations below. A client can devirtualize a witness and inline it only when its body calls
|
|
489
|
+
// non-generic functions; through a generic, non-inlinable call, it falls back to dynamic dispatch, and a
|
|
490
|
+
// direct call runs the generic implementation unspecialized. Each overload runs a copy of the generic
|
|
491
|
+
// implementation specialized inside the framework.
|
|
492
|
+
|
|
493
|
+
@usableFromInline
|
|
494
|
+
@JavaScriptActor
|
|
495
|
+
func decodeInteger(_ number: Double, as type: Int8.Type) throws -> Int8 {
|
|
496
|
+
return try genericDecodeInteger(number, as: Int8.self)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
@usableFromInline
|
|
500
|
+
@JavaScriptActor
|
|
501
|
+
func decodeInteger(_ number: Double, as type: Int16.Type) throws -> Int16 {
|
|
502
|
+
return try genericDecodeInteger(number, as: Int16.self)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
@usableFromInline
|
|
506
|
+
@JavaScriptActor
|
|
507
|
+
func decodeInteger(_ number: Double, as type: Int32.Type) throws -> Int32 {
|
|
508
|
+
return try genericDecodeInteger(number, as: Int32.self)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
@usableFromInline
|
|
512
|
+
@JavaScriptActor
|
|
513
|
+
func decodeInteger(_ number: Double, as type: UInt8.Type) throws -> UInt8 {
|
|
514
|
+
return try genericDecodeInteger(number, as: UInt8.self)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
@usableFromInline
|
|
518
|
+
@JavaScriptActor
|
|
519
|
+
func decodeInteger(_ number: Double, as type: UInt16.Type) throws -> UInt16 {
|
|
520
|
+
return try genericDecodeInteger(number, as: UInt16.self)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
@usableFromInline
|
|
524
|
+
@JavaScriptActor
|
|
525
|
+
func decodeInteger(_ number: Double, as type: UInt32.Type) throws -> UInt32 {
|
|
526
|
+
return try genericDecodeInteger(number, as: UInt32.self)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
@usableFromInline
|
|
530
|
+
@JavaScriptActor
|
|
531
|
+
func decodeWideInteger(_ value: borrowing JavaScriptValue, as type: Int.Type) throws -> Int {
|
|
532
|
+
return try genericDecodeWideInteger(value, as: Int.self)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
@usableFromInline
|
|
536
|
+
@JavaScriptActor
|
|
537
|
+
func decodeWideInteger(
|
|
538
|
+
_ value: borrowing JavaScriptUnownedValue,
|
|
539
|
+
as type: Int.Type,
|
|
540
|
+
runtime: borrowing JavaScriptRuntime
|
|
541
|
+
) throws -> Int {
|
|
542
|
+
return try genericDecodeWideInteger(value, as: Int.self, runtime: runtime)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
@usableFromInline
|
|
546
|
+
@JavaScriptActor
|
|
547
|
+
func decodeWideInteger(_ value: borrowing JavaScriptValue, as type: Int64.Type) throws -> Int64 {
|
|
548
|
+
return try genericDecodeWideInteger(value, as: Int64.self)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
@usableFromInline
|
|
552
|
+
@JavaScriptActor
|
|
553
|
+
func decodeWideInteger(
|
|
554
|
+
_ value: borrowing JavaScriptUnownedValue,
|
|
555
|
+
as type: Int64.Type,
|
|
556
|
+
runtime: borrowing JavaScriptRuntime
|
|
557
|
+
) throws -> Int64 {
|
|
558
|
+
return try genericDecodeWideInteger(value, as: Int64.self, runtime: runtime)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
@usableFromInline
|
|
562
|
+
@JavaScriptActor
|
|
563
|
+
func decodeWideInteger(_ value: borrowing JavaScriptValue, as type: UInt.Type) throws -> UInt {
|
|
564
|
+
return try genericDecodeWideInteger(value, as: UInt.self)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
@usableFromInline
|
|
568
|
+
@JavaScriptActor
|
|
569
|
+
func decodeWideInteger(
|
|
570
|
+
_ value: borrowing JavaScriptUnownedValue,
|
|
571
|
+
as type: UInt.Type,
|
|
572
|
+
runtime: borrowing JavaScriptRuntime
|
|
573
|
+
) throws -> UInt {
|
|
574
|
+
return try genericDecodeWideInteger(value, as: UInt.self, runtime: runtime)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
@usableFromInline
|
|
578
|
+
@JavaScriptActor
|
|
579
|
+
func decodeWideInteger(_ value: borrowing JavaScriptValue, as type: UInt64.Type) throws -> UInt64 {
|
|
580
|
+
return try genericDecodeWideInteger(value, as: UInt64.self)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
@usableFromInline
|
|
584
|
+
@JavaScriptActor
|
|
585
|
+
func decodeWideInteger(
|
|
586
|
+
_ value: borrowing JavaScriptUnownedValue,
|
|
587
|
+
as type: UInt64.Type,
|
|
588
|
+
runtime: borrowing JavaScriptRuntime
|
|
589
|
+
) throws -> UInt64 {
|
|
590
|
+
return try genericDecodeWideInteger(value, as: UInt64.self, runtime: runtime)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
@usableFromInline
|
|
594
|
+
@JavaScriptActor
|
|
595
|
+
func encodeSafeInteger(_ value: Int) throws -> JavaScriptValue {
|
|
596
|
+
return try genericEncodeSafeInteger(value)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
@usableFromInline
|
|
600
|
+
@JavaScriptActor
|
|
601
|
+
func encodeSafeInteger(_ value: UInt) throws -> JavaScriptValue {
|
|
602
|
+
return try genericEncodeSafeInteger(value)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// MARK: - Generic integer conversions
|
|
606
|
+
|
|
485
607
|
/// Rounds a JavaScript number to the nearest integer and narrows it to `T`, throwing instead of
|
|
486
608
|
/// trapping when the value is non-finite or outside `T`'s representable range. `T(exactly:)` on the
|
|
487
609
|
/// already-rounded value is nil only when out of range, sidestepping the lossy `Double(T.max)`
|
|
488
610
|
/// boundary comparison for 64-bit widths.
|
|
489
|
-
@usableFromInline
|
|
490
611
|
@JavaScriptActor
|
|
491
|
-
func
|
|
612
|
+
func genericDecodeInteger<T: FixedWidthInteger>(_ number: Double, as type: T.Type) throws -> T {
|
|
492
613
|
guard number.isFinite, let result = T(exactly: number.rounded()) else {
|
|
493
614
|
throw IntegerOutOfRangeException(value: number, type: "\(T.self)")
|
|
494
615
|
}
|
|
@@ -499,47 +620,41 @@ func decodeInteger<T: FixedWidthInteger>(_ number: Double, as type: T.Type) thro
|
|
|
499
620
|
/// case is checked first since it's the common one: a single `isNumber()` tag check, then the
|
|
500
621
|
/// assert-only `getDouble()` reads the value without re-checking the tag. A `bigint` is read losslessly
|
|
501
622
|
/// through its 64-bit accessor; anything else throws the same `TypeError` the `number` read would.
|
|
502
|
-
@usableFromInline
|
|
503
623
|
@JavaScriptActor
|
|
504
|
-
func
|
|
624
|
+
func genericDecodeWideInteger<T: FixedWidthInteger>(_ value: borrowing JavaScriptValue, as type: T.Type) throws -> T {
|
|
505
625
|
guard value.isNumber() else {
|
|
506
626
|
guard value.isBigInt() else {
|
|
507
|
-
// Neither a number nor a bigint: `asDouble()` throws the canonical `TypeError`.
|
|
508
|
-
|
|
509
|
-
// referencing `TypeError`'s internal memberwise initializer.
|
|
510
|
-
return try decodeInteger(value.asDouble(), as: T.self)
|
|
627
|
+
// Neither a number nor a bigint: `asDouble()` throws the canonical `TypeError`.
|
|
628
|
+
return try genericDecodeInteger(value.asDouble(), as: T.self)
|
|
511
629
|
}
|
|
512
|
-
return try
|
|
630
|
+
return try genericDecodeBigInt(value.getBigInt(), as: T.self)
|
|
513
631
|
}
|
|
514
|
-
return try
|
|
632
|
+
return try genericDecodeInteger(value.getDouble(), as: T.self)
|
|
515
633
|
}
|
|
516
634
|
|
|
517
635
|
/// `JavaScriptUnownedValue` overload of `decodeWideInteger`. The common `number` path stays zero-copy
|
|
518
636
|
/// and pays a single tag check (see the owning overload); only a `bigint` materializes an owning value
|
|
519
637
|
/// (the borrowed value exposes no BigInt accessor), which is acceptable on this rare branch.
|
|
520
|
-
@usableFromInline
|
|
521
638
|
@JavaScriptActor
|
|
522
|
-
func
|
|
639
|
+
func genericDecodeWideInteger<T: FixedWidthInteger>(
|
|
523
640
|
_ value: borrowing JavaScriptUnownedValue,
|
|
524
641
|
as type: T.Type,
|
|
525
642
|
runtime: borrowing JavaScriptRuntime
|
|
526
643
|
) throws -> T {
|
|
527
644
|
guard value.isNumber() else {
|
|
528
645
|
guard value.isBigInt() else {
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
return try decodeInteger(value.asDouble(), as: T.self)
|
|
646
|
+
// Neither a number nor a bigint: `asDouble()` throws the canonical `TypeError`.
|
|
647
|
+
return try genericDecodeInteger(value.asDouble(), as: T.self)
|
|
532
648
|
}
|
|
533
|
-
return try
|
|
649
|
+
return try genericDecodeBigInt(value.copied(in: runtime).getBigInt(), as: T.self)
|
|
534
650
|
}
|
|
535
|
-
return try
|
|
651
|
+
return try genericDecodeInteger(value.getDouble(), as: T.self)
|
|
536
652
|
}
|
|
537
653
|
|
|
538
654
|
/// Narrows a `JavaScriptBigInt` to `T`, reading it through the signed or unsigned 64-bit accessor to
|
|
539
655
|
/// match `T`'s signedness and throwing rather than truncating when it falls outside `T`'s range.
|
|
540
|
-
@usableFromInline
|
|
541
656
|
@JavaScriptActor
|
|
542
|
-
func
|
|
657
|
+
func genericDecodeBigInt<T: FixedWidthInteger>(_ bigInt: borrowing JavaScriptBigInt, as type: T.Type) throws -> T {
|
|
543
658
|
let wide: T? =
|
|
544
659
|
if T.isSigned {
|
|
545
660
|
bigInt.isInt64() ? T(exactly: bigInt.getInt64()) : nil
|
|
@@ -555,9 +670,8 @@ func decodeBigInt<T: FixedWidthInteger>(_ bigInt: borrowing JavaScriptBigInt, as
|
|
|
555
670
|
/// Encodes a 64-bit-wide integer to a JS `number`, throwing when it falls outside JavaScript's
|
|
556
671
|
/// safe-integer range where a `Double` could no longer represent it exactly. Used by `Int`/`UInt`,
|
|
557
672
|
/// whose JS mapping stays a `number`; the explicitly-sized 64-bit types encode as a `bigint` instead.
|
|
558
|
-
@usableFromInline
|
|
559
673
|
@JavaScriptActor
|
|
560
|
-
func
|
|
674
|
+
func genericEncodeSafeInteger<T: FixedWidthInteger>(_ value: T) throws -> JavaScriptValue {
|
|
561
675
|
// `Number.MAX_SAFE_INTEGER` (2^53 - 1): the largest magnitude where every integer up to it, and the
|
|
562
676
|
// next one, is representable as a JS number. 2^53 itself is representable but unsafe (it collides with
|
|
563
677
|
// 2^53 + 1), so the bound is exclusive above this. Used by `Int`/`UInt`, both 64-bit, so 2^53 fits.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
internal import ExpoModulesJSI_Cxx
|
|
4
|
+
internal import jsi
|
|
5
|
+
|
|
6
|
+
// `Set` encodes to a JS `Set` and decodes from a JS `Set` or, like `Array`, from an array or an arrayized
|
|
7
|
+
// scalar. JSI has no `Set` API, so the conversions go through the runtime's global `Set` and `Array`
|
|
8
|
+
// constructors. Equality differs between the two sides: a JS `Set` compares objects by reference while
|
|
9
|
+
// a Swift `Set` compares by `Hashable`, so two distinct JS objects that decode to equal values collapse
|
|
10
|
+
// into a single element.
|
|
11
|
+
|
|
12
|
+
extension Set: JavaScriptDecodable where Element: JavaScriptDecodable {
|
|
13
|
+
@JavaScriptActor
|
|
14
|
+
@inlinable
|
|
15
|
+
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
16
|
+
-> Set<Element>
|
|
17
|
+
{
|
|
18
|
+
// The cheap tag checks come first: an array takes the `Array` path directly, and a primitive
|
|
19
|
+
// can't be a JS `Set`, so both skip the JS call below. Any other value that is not a JS `Set` is
|
|
20
|
+
// arrayized, as in `Array`. Duplicates in an array collapse without an error.
|
|
21
|
+
guard !value.isArray(), value.isObject(), let entries = try runtime.setEntries(of: value) else {
|
|
22
|
+
return Set(try [Element].decode(value, in: runtime))
|
|
23
|
+
}
|
|
24
|
+
return Set(try [Element].decode(entries, in: runtime))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
extension Set: JavaScriptEncodable where Element: JavaScriptEncodable {
|
|
29
|
+
@JavaScriptActor
|
|
30
|
+
@inlinable
|
|
31
|
+
public static func encode(_ value: Set<Element>, in runtime: borrowing JavaScriptRuntime) throws
|
|
32
|
+
-> JavaScriptValue
|
|
33
|
+
{
|
|
34
|
+
// Constructing from an array fills the set in a single call, rather than one `add` call per element.
|
|
35
|
+
let entries = try [Element].encode(Array(value), in: runtime)
|
|
36
|
+
return try runtime.setConstructor().callAsConstructor(entries)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
extension JavaScriptRuntime {
|
|
41
|
+
/// Copies the entries of a JS `Set` out into a JS array, or returns `nil` when the value is not a
|
|
42
|
+
/// JS `Set`. The instance check and the copy run in a single call to a cached JS function, which
|
|
43
|
+
/// saves the global lookups and the separate `instanceof` call of `value.is("Set")` followed by
|
|
44
|
+
/// `Array.from`. For a small `Set` those fixed costs are most of the decode time.
|
|
45
|
+
@usableFromInline
|
|
46
|
+
@JavaScriptActor
|
|
47
|
+
func setEntries(of value: borrowing JavaScriptValue) throws -> JavaScriptValue? {
|
|
48
|
+
let function = try cached(setEntriesFunctionKey) {
|
|
49
|
+
return try eval(
|
|
50
|
+
label: "expo-modules-jsi/set-entries.js",
|
|
51
|
+
"(function (value) { return value instanceof Set ? Array.from(value) : undefined; })"
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
let setEntries = function.getFunction()
|
|
55
|
+
// Pass the borrowed `jsi::Value` straight to the call, rather than copying it into an owned value
|
|
56
|
+
// and again into an arguments buffer.
|
|
57
|
+
let entries = try withUnsafePointer(to: value.pointee) { argument in
|
|
58
|
+
return try capturingCppErrors {
|
|
59
|
+
return JavaScriptValue(self, expo.callFunction(pointee, setEntries.pointee, argument, 1))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return entries.isUndefined() ? nil : entries
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Returns the global `Set` constructor, looked up once and cached, so an encode doesn't pay for
|
|
66
|
+
/// the global property lookup.
|
|
67
|
+
@usableFromInline
|
|
68
|
+
@JavaScriptActor
|
|
69
|
+
func setConstructor() throws -> JavaScriptFunction {
|
|
70
|
+
let constructor = try cached(setConstructorKey) {
|
|
71
|
+
return try global().getPropertyAsFunction("Set").asValue()
|
|
72
|
+
}
|
|
73
|
+
return constructor.getFunction()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Keys of the `Set` entries function and the `Set` constructor in each runtime's cache.
|
|
78
|
+
private let setEntriesFunctionKey = JavaScriptRuntime.Cache.Key<JavaScriptValue>()
|
|
79
|
+
private let setConstructorKey = JavaScriptRuntime.Cache.Key<JavaScriptValue>()
|
|
@@ -67,7 +67,7 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
67
67
|
self.runtimePointee = runtime
|
|
68
68
|
self.pointee = expo.iruntime(runtime)
|
|
69
69
|
self.handle = JavaScriptRuntimeHandle(self.pointee)
|
|
70
|
-
self.scheduler = expo.RuntimeScheduler()
|
|
70
|
+
self.scheduler = expo.RuntimeScheduler.create()
|
|
71
71
|
self.ownsRuntime = false
|
|
72
72
|
handle.attach(self)
|
|
73
73
|
installLongLivedObjectsTeardown()
|
|
@@ -80,7 +80,7 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
80
80
|
self.runtimePointee = runtime
|
|
81
81
|
self.pointee = expo.iruntime(runtime)
|
|
82
82
|
self.handle = JavaScriptRuntimeHandle(self.pointee)
|
|
83
|
-
self.scheduler = expo.RuntimeScheduler()
|
|
83
|
+
self.scheduler = expo.RuntimeScheduler.create()
|
|
84
84
|
self.ownsRuntime = true
|
|
85
85
|
handle.attach(self)
|
|
86
86
|
installLongLivedObjectsTeardown()
|
|
@@ -94,7 +94,7 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
94
94
|
self.runtimePointee = runtime
|
|
95
95
|
self.pointee = expo.iruntime(runtime)
|
|
96
96
|
self.handle = JavaScriptRuntimeHandle(self.pointee)
|
|
97
|
-
self.scheduler = expo.RuntimeScheduler()
|
|
97
|
+
self.scheduler = expo.RuntimeScheduler.create()
|
|
98
98
|
self.ownsRuntime = false
|
|
99
99
|
handle.attach(self)
|
|
100
100
|
installLongLivedObjectsTeardown()
|
|
@@ -123,7 +123,7 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
123
123
|
self.runtimePointee = runtime
|
|
124
124
|
self.pointee = expo.iruntime(runtime)
|
|
125
125
|
self.handle = JavaScriptRuntimeHandle(self.pointee)
|
|
126
|
-
self.scheduler = expo.RuntimeScheduler(scheduler, fn)
|
|
126
|
+
self.scheduler = expo.RuntimeScheduler.create(scheduler, fn)
|
|
127
127
|
self.ownsRuntime = false
|
|
128
128
|
handle.attach(self)
|
|
129
129
|
installLongLivedObjectsTeardown()
|
|
@@ -197,14 +197,14 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
197
197
|
propertyName: UnsafePointer<facebook.jsi.PropNameID>,
|
|
198
198
|
resultPtr: UnsafeMutablePointer<facebook.jsi.Value>
|
|
199
199
|
) -> Bool {
|
|
200
|
-
|
|
200
|
+
let resultPtr = UncheckedSendable(resultPtr)
|
|
201
201
|
|
|
202
202
|
return withGuaranteedContext(context) { (context: HostObjectContext, runtime) in
|
|
203
203
|
let propertyName = String(jsiPropNameID: propertyName.pointee, in: runtime.pointee)
|
|
204
204
|
return JavaScriptActor.assumeIsolated {
|
|
205
205
|
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
206
206
|
var result = try context.get(propertyName)
|
|
207
|
-
JavaScriptValue.write(&result, to: resultPtr)
|
|
207
|
+
JavaScriptValue.write(&result, to: resultPtr.value)
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
}
|
|
@@ -693,8 +693,15 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
693
693
|
@discardableResult
|
|
694
694
|
@JavaScriptActor
|
|
695
695
|
public func evalAsync(label: String? = nil, _ source: String) async throws -> JavaScriptValue {
|
|
696
|
-
|
|
697
|
-
|
|
696
|
+
// `@JavaScriptActor` runs this on the caller's thread, so go through `execute` to evaluate on the
|
|
697
|
+
// JavaScript thread. It runs the closure in place when already there, or when the runtime has no
|
|
698
|
+
// scheduler and thus no other thread to go to.
|
|
699
|
+
// The result is boxed because `execute` needs a `Sendable` result and `JavaScriptValue` is not one.
|
|
700
|
+
let result = try await execute { () async throws -> NonisolatedUnsafeVar<JavaScriptValue> in
|
|
701
|
+
let value = try self.eval(label: label, source)
|
|
702
|
+
return NonisolatedUnsafeVar(value.is("Promise") ? try await value.getPromise().await() : value)
|
|
703
|
+
}
|
|
704
|
+
return result.value
|
|
698
705
|
}
|
|
699
706
|
|
|
700
707
|
// MARK: - Garbage collection
|
|
@@ -871,23 +878,23 @@ private func createFunctionClosure(
|
|
|
871
878
|
// heap-allocated `JavaScriptRef` (Swift 6.2 rejects capturing/consuming a `~Copyable` value in the
|
|
872
879
|
// escaping closure that `withoutActuallyEscaping` synthesizes), the closure constructs the buffer
|
|
873
880
|
// locally from the raw pointer + count. Those are read-only call-scoped inputs that never outlive the
|
|
874
|
-
// synchronous call, so
|
|
881
|
+
// synchronous call, so capturing them through `UncheckedSendable` is sound. This removes a per-call class
|
|
875
882
|
// allocation + retain/release + dealloc that profiling showed dominating the no-op `@JS` host-call
|
|
876
883
|
// floor.
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
884
|
+
let thisPtr = UncheckedSendable(thisPtr)
|
|
885
|
+
let argumentsPtr = UncheckedSendable(argumentsPtr)
|
|
886
|
+
let resultPtr = UncheckedSendable(resultPtr)
|
|
880
887
|
|
|
881
888
|
// See `withGuaranteedContext` for why neither the context nor the runtime is retained here, and
|
|
882
889
|
// why the result is written to the caller's slot instead of being returned.
|
|
883
890
|
return withGuaranteedContext(context) { (context: HostFunctionContext, runtime) in
|
|
884
891
|
return JavaScriptActor.assumeIsolated {
|
|
885
892
|
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
886
|
-
let this = UnsafeMutablePointer(mutating: thisPtr).move()
|
|
887
|
-
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr, count: argumentsCount)
|
|
893
|
+
let this = UnsafeMutablePointer(mutating: thisPtr.value).move()
|
|
894
|
+
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr.value, count: argumentsCount)
|
|
888
895
|
let thisValue = JavaScriptValue(runtime, this)
|
|
889
896
|
var result = try context.call(thisValue, consume arguments)
|
|
890
|
-
JavaScriptValue.write(&result, to: resultPtr)
|
|
897
|
+
JavaScriptValue.write(&result, to: resultPtr.value)
|
|
891
898
|
}
|
|
892
899
|
}
|
|
893
900
|
}
|
|
@@ -919,19 +926,19 @@ private func createFunctionClosure(
|
|
|
919
926
|
// handed in as a borrowed `JavaScriptUnownedValue` pointing straight at the C++-owned `this` slot:
|
|
920
927
|
// it is not moved out and no owning `JavaScriptValue` is allocated, so the closure avoids the
|
|
921
928
|
// per-call `weak`-runtime form/destroy and heap object that the owning `this` pays.
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
929
|
+
let thisPtr = UncheckedSendable(thisPtr)
|
|
930
|
+
let argumentsPtr = UncheckedSendable(argumentsPtr)
|
|
931
|
+
let resultPtr = UncheckedSendable(resultPtr)
|
|
925
932
|
|
|
926
933
|
// See `withGuaranteedContext` for why neither the context nor the runtime is retained here, and
|
|
927
934
|
// why the result is written to the caller's slot instead of being returned.
|
|
928
935
|
return withGuaranteedContext(context) { (context: UnownedThisHostFunctionContext, runtime) in
|
|
929
936
|
return JavaScriptActor.assumeIsolated {
|
|
930
937
|
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
931
|
-
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr, count: argumentsCount)
|
|
932
|
-
let thisValue = JavaScriptUnownedValue(runtime.pointee, thisPtr)
|
|
938
|
+
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr.value, count: argumentsCount)
|
|
939
|
+
let thisValue = JavaScriptUnownedValue(runtime.pointee, thisPtr.value)
|
|
933
940
|
var result = try context.call(thisValue, consume arguments)
|
|
934
|
-
JavaScriptValue.write(&result, to: resultPtr)
|
|
941
|
+
JavaScriptValue.write(&result, to: resultPtr.value)
|
|
935
942
|
}
|
|
936
943
|
}
|
|
937
944
|
}
|
|
@@ -375,19 +375,50 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
375
375
|
guard let runtime else {
|
|
376
376
|
FatalError.runtimeLost()
|
|
377
377
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
.
|
|
378
|
+
do {
|
|
379
|
+
try definePropertyFunction(in: runtime).function
|
|
380
|
+
.call(arguments: self.asValue().ref(), JavaScriptValue(runtime, name).ref(), descriptor.ref())
|
|
381
|
+
} catch {
|
|
382
|
+
FatalError.definePropertyFailed(name, error)
|
|
383
|
+
}
|
|
383
384
|
}
|
|
384
385
|
|
|
385
386
|
public func defineProperty(_ name: String, descriptor: consuming PropertyDescriptor = .init()) {
|
|
386
387
|
guard let runtime else {
|
|
387
388
|
FatalError.runtimeLost()
|
|
388
389
|
}
|
|
389
|
-
let
|
|
390
|
-
|
|
390
|
+
let defineProperty = definePropertyFunction(in: runtime)
|
|
391
|
+
let jsiRuntime = runtime.pointee
|
|
392
|
+
let hasValue = descriptor.value != nil
|
|
393
|
+
let value = descriptor.value?.toJSIValue(in: jsiRuntime) ?? facebook.jsi.Value.undefined()
|
|
394
|
+
var utf8Name = name
|
|
395
|
+
utf8Name.withUTF8 { nameUtf8 in
|
|
396
|
+
// A JS error can't propagate through Swift, so `capturingCppErrors` captures it and rethrows it
|
|
397
|
+
// here; `defineProperty` doesn't throw, so it stops execution with that error.
|
|
398
|
+
do {
|
|
399
|
+
try capturingCppErrors {
|
|
400
|
+
expo.defineProperty(
|
|
401
|
+
jsiRuntime,
|
|
402
|
+
defineProperty.function.pointee,
|
|
403
|
+
defineProperty.configurableKey.pointee,
|
|
404
|
+
defineProperty.enumerableKey.pointee,
|
|
405
|
+
defineProperty.writableKey.pointee,
|
|
406
|
+
defineProperty.valueKey.pointee,
|
|
407
|
+
pointee,
|
|
408
|
+
// An empty buffer may have no base address; any non-null pointer works with a zero length.
|
|
409
|
+
nameUtf8.baseAddress ?? UnsafePointer(bitPattern: 1)!,
|
|
410
|
+
nameUtf8.count,
|
|
411
|
+
value,
|
|
412
|
+
hasValue,
|
|
413
|
+
descriptor.writable,
|
|
414
|
+
descriptor.enumerable,
|
|
415
|
+
descriptor.configurable
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
FatalError.definePropertyFailed(name, error)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
391
422
|
}
|
|
392
423
|
|
|
393
424
|
public func defineProperty<T: JavaScriptRepresentable & ~Copyable>(
|
|
@@ -674,3 +705,40 @@ extension JavaScriptObject {
|
|
|
674
705
|
}
|
|
675
706
|
}
|
|
676
707
|
}
|
|
708
|
+
|
|
709
|
+
// MARK: - Object.defineProperty
|
|
710
|
+
|
|
711
|
+
/// Holds the runtime's `Object.defineProperty` and the property names of a descriptor, created once per
|
|
712
|
+
/// runtime instead of on every `defineProperty` call.
|
|
713
|
+
private final class DefinePropertyFunction {
|
|
714
|
+
let function: JavaScriptFunction
|
|
715
|
+
let configurableKey: JavaScriptPropNameID
|
|
716
|
+
let enumerableKey: JavaScriptPropNameID
|
|
717
|
+
let writableKey: JavaScriptPropNameID
|
|
718
|
+
let valueKey: JavaScriptPropNameID
|
|
719
|
+
|
|
720
|
+
init(_ function: consuming JavaScriptFunction, in runtime: JavaScriptRuntime) {
|
|
721
|
+
self.function = function
|
|
722
|
+
self.configurableKey = JavaScriptPropNameID(runtime, string: "configurable")
|
|
723
|
+
self.enumerableKey = JavaScriptPropNameID(runtime, string: "enumerable")
|
|
724
|
+
self.writableKey = JavaScriptPropNameID(runtime, string: "writable")
|
|
725
|
+
self.valueKey = JavaScriptPropNameID(runtime, string: "value")
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private let definePropertyFunctionKey = JavaScriptRuntime.Cache.Key<DefinePropertyFunction>()
|
|
730
|
+
|
|
731
|
+
/// Returns the runtime's `Object.defineProperty`, cached in the runtime. Property definitions run on the
|
|
732
|
+
/// JavaScript thread like every other JSI call, which is where the runtime's cache is isolated.
|
|
733
|
+
private func definePropertyFunction(in runtime: JavaScriptRuntime) -> DefinePropertyFunction {
|
|
734
|
+
return JavaScriptActor.assumeIsolated {
|
|
735
|
+
runtime.cached(definePropertyFunctionKey) {
|
|
736
|
+
do {
|
|
737
|
+
let function = try runtime.global().getPropertyAsObject("Object").getPropertyAsFunction("defineProperty")
|
|
738
|
+
return DefinePropertyFunction(function, in: runtime)
|
|
739
|
+
} catch {
|
|
740
|
+
FatalError.definePropertyUnavailable(error)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
@@ -30,4 +30,23 @@ internal struct FatalError {
|
|
|
30
30
|
+ "Check that its null-setter short-circuit is still in place."
|
|
31
31
|
)
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
/// Stops program execution when `Object.defineProperty` throws, for example when the property already
|
|
35
|
+
/// exists and isn't configurable, or the object isn't extensible. `defineProperty` doesn't throw, so
|
|
36
|
+
/// its callers are expected to define properties that JavaScript accepts.
|
|
37
|
+
internal static func definePropertyFailed(_ name: String, _ error: any Error) -> Never {
|
|
38
|
+
fatalError(
|
|
39
|
+
"Object.defineProperty failed to define '\(name)': \(error). "
|
|
40
|
+
+ "Make sure the object is extensible and doesn't already have a non-configurable property with this name."
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Stops program execution when the runtime has no `Object.defineProperty` function, which every
|
|
45
|
+
/// JavaScript engine provides unless the global `Object` has been replaced.
|
|
46
|
+
internal static func definePropertyUnavailable(_ error: any Error) -> Never {
|
|
47
|
+
fatalError(
|
|
48
|
+
"Couldn't look up Object.defineProperty in the JavaScript runtime: \(error). "
|
|
49
|
+
+ "Check whether the global Object has been replaced."
|
|
50
|
+
)
|
|
51
|
+
}
|
|
33
52
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// Immutable wrapper that makes a non-Sendable value capturable by a `@Sendable` closure without compiler enforcement.
|
|
2
|
+
/// Unlike `NonisolatedUnsafeVar`, it is a struct, so wrapping a value doesn't allocate.
|
|
3
|
+
///
|
|
4
|
+
/// Use it instead of a `nonisolated(unsafe) let` local captured by such a closure: Swift 6.2 still reports
|
|
5
|
+
/// "sending '...' risks causing data races" for those, while a capture of a `Sendable` value passes on every version.
|
|
6
|
+
/// The caller is responsible for making sure the value is never accessed concurrently.
|
|
7
|
+
internal struct UncheckedSendable<Value>: @unchecked Sendable {
|
|
8
|
+
let value: Value
|
|
9
|
+
|
|
10
|
+
init(_ value: Value) {
|
|
11
|
+
self.value = value
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -220,6 +220,47 @@ inline jsi::Value callFunctionWithThis(jsi::IRuntime &runtime, const jsi::Functi
|
|
|
220
220
|
});
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
Calls `defineProperty`, the runtime's `Object.defineProperty`, on `object` with a new descriptor that holds
|
|
225
|
+
`value` (when `hasValue` is set) and only the attributes that are `true`, like a descriptor built in
|
|
226
|
+
JavaScript. Taking the function and the descriptor keys from the caller lets it create them once per runtime,
|
|
227
|
+
and building the descriptor and the arguments here avoids a round trip through Swift for each of them.
|
|
228
|
+
*/
|
|
229
|
+
inline void defineProperty(
|
|
230
|
+
jsi::IRuntime &runtime,
|
|
231
|
+
const jsi::Function &defineProperty,
|
|
232
|
+
const jsi::PropNameID &configurableKey,
|
|
233
|
+
const jsi::PropNameID &enumerableKey,
|
|
234
|
+
const jsi::PropNameID &writableKey,
|
|
235
|
+
const jsi::PropNameID &valueKey,
|
|
236
|
+
const jsi::Object &object,
|
|
237
|
+
const uint8_t *_Nonnull nameUtf8,
|
|
238
|
+
size_t nameLength,
|
|
239
|
+
const jsi::Value &value,
|
|
240
|
+
bool hasValue,
|
|
241
|
+
bool writable,
|
|
242
|
+
bool enumerable,
|
|
243
|
+
bool configurable
|
|
244
|
+
) {
|
|
245
|
+
expo::CppError::tryCatch(runtime, [&] {
|
|
246
|
+
jsi::Object descriptor(runtime);
|
|
247
|
+
if (configurable) {
|
|
248
|
+
descriptor.setProperty(runtime, configurableKey, true);
|
|
249
|
+
}
|
|
250
|
+
if (enumerable) {
|
|
251
|
+
descriptor.setProperty(runtime, enumerableKey, true);
|
|
252
|
+
}
|
|
253
|
+
if (writable) {
|
|
254
|
+
descriptor.setProperty(runtime, writableKey, true);
|
|
255
|
+
}
|
|
256
|
+
if (hasValue) {
|
|
257
|
+
descriptor.setProperty(runtime, valueKey, value);
|
|
258
|
+
}
|
|
259
|
+
defineProperty.call(runtime, object, jsi::String::createFromUtf8(runtime, nameUtf8, nameLength), descriptor);
|
|
260
|
+
return jsi::Value::undefined();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
223
264
|
inline jsi::Value callAsConstructor(jsi::IRuntime &runtime, const jsi::Function &function, const jsi::Value *_Nullable args, size_t count) {
|
|
224
265
|
return expo::CppError::tryCatch(runtime, [&] {
|
|
225
266
|
return function.callAsConstructor(runtime, args, count);
|
|
@@ -44,21 +44,34 @@ private:
|
|
|
44
44
|
|
|
45
45
|
std::atomic<int> refCount{1};
|
|
46
46
|
|
|
47
|
+
RuntimeScheduler(void *scheduler, ScheduleFn fn) noexcept
|
|
48
|
+
: nativeScheduler(scheduler), scheduleFn(fn) {}
|
|
49
|
+
|
|
50
|
+
RuntimeScheduler() noexcept {}
|
|
51
|
+
|
|
47
52
|
public:
|
|
53
|
+
// Swift creates instances through the static `create` functions rather than the constructors.
|
|
54
|
+
// Annotating the constructors with `SWIFT_RETURNS_RETAINED` silences the Swift 6.4 "cannot infer
|
|
55
|
+
// ownership" warning, but Swift 6.2 rejects that annotation on a constructor as an error. Static
|
|
56
|
+
// functions returning a shared reference accept the annotation on every Swift version.
|
|
57
|
+
|
|
48
58
|
/**
|
|
49
|
-
|
|
59
|
+
Creates a scheduler bound to a host-provided native RuntimeScheduler.
|
|
50
60
|
`scheduleTask` dispatches through `fn`, which the host implements against
|
|
51
61
|
the real react::RuntimeScheduler.
|
|
52
62
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
static RuntimeScheduler *create(void *scheduler, ScheduleFn fn) noexcept SWIFT_RETURNS_RETAINED {
|
|
64
|
+
return new RuntimeScheduler(scheduler, fn);
|
|
65
|
+
}
|
|
55
66
|
|
|
56
67
|
/**
|
|
57
|
-
|
|
68
|
+
Creates a no-op scheduler. Scheduled tasks run synchronously on the
|
|
58
69
|
caller's thread — intended for standalone runtimes (e.g. tests) that have
|
|
59
70
|
no React scheduler.
|
|
60
71
|
*/
|
|
61
|
-
|
|
72
|
+
static RuntimeScheduler *create() noexcept SWIFT_RETURNS_RETAINED {
|
|
73
|
+
return new RuntimeScheduler();
|
|
74
|
+
}
|
|
62
75
|
|
|
63
76
|
RuntimeScheduler(const RuntimeScheduler &) = delete;
|
|
64
77
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Testing
|
|
5
|
+
|
|
6
|
+
@Suite("JavaScriptCodable+Set")
|
|
7
|
+
@JavaScriptActor
|
|
8
|
+
struct JavaScriptCodableSetTests {
|
|
9
|
+
let runtime = JavaScriptRuntime()
|
|
10
|
+
|
|
11
|
+
@Test
|
|
12
|
+
func `decodes a set of strings from a JS Set`() throws {
|
|
13
|
+
let decoded = try Set<String>.decode(runtime.eval("new Set(['a', 'b'])"), in: runtime)
|
|
14
|
+
#expect(decoded == ["a", "b"])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Test
|
|
18
|
+
func `decodes a set from a subclass of Set`() throws {
|
|
19
|
+
let decoded = try Set<Int>.decode(runtime.eval("new (class extends Set {})([1, 2])"), in: runtime)
|
|
20
|
+
#expect(decoded == [1, 2])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test
|
|
24
|
+
func `arrayizes a non-set object into a single-element set`() throws {
|
|
25
|
+
let decoded = try Set<[String: Int]>.decode(runtime.eval("({ a: 1 })"), in: runtime)
|
|
26
|
+
#expect(decoded == [["a": 1]])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
func `decoding many sets leaves no helper on the global object`() throws {
|
|
31
|
+
let runtime = JavaScriptRuntime()
|
|
32
|
+
for _ in 0..<100 {
|
|
33
|
+
_ = try Set<Int>.decode(runtime.eval("new Set([1, 2, 3])"), in: runtime)
|
|
34
|
+
}
|
|
35
|
+
// The only object the wrapper may leave on `globalThis` is the long-lived-objects anchor.
|
|
36
|
+
let ownGlobals = try runtime.eval("Object.getOwnPropertyNames(globalThis).length").getInt()
|
|
37
|
+
let freshGlobals = try JavaScriptRuntime().eval("Object.getOwnPropertyNames(globalThis).length").getInt()
|
|
38
|
+
#expect(ownGlobals - freshGlobals <= 1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Test
|
|
42
|
+
func `decodes a set from an array, collapsing duplicates`() throws {
|
|
43
|
+
let decoded = try Set<Int>.decode(runtime.eval("[1, 2, 2, 3, 1]"), in: runtime)
|
|
44
|
+
#expect(decoded == [1, 2, 3])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
func `arrayizes a non-array, non-set scalar into a set`() throws {
|
|
49
|
+
#expect(try Set<Int>.decode(runtime.eval("42"), in: runtime) == [42])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
func `encodes a set to a JS Set`() throws {
|
|
54
|
+
let encoded = try Set<Int>.encode([1, 2, 3], in: runtime)
|
|
55
|
+
#expect(encoded.is("Set"))
|
|
56
|
+
let object = encoded.getObject()
|
|
57
|
+
#expect(object.getProperty("size").getInt() == 3)
|
|
58
|
+
#expect(try object.callFunction("has", arguments: 2).getBool() == true)
|
|
59
|
+
#expect(try object.callFunction("has", arguments: 4).getBool() == false)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
func `round-trips a set of sets`() throws {
|
|
64
|
+
let value: Set<Set<String>> = [["a"], ["b", "c"]]
|
|
65
|
+
let encoded = try Set<Set<String>>.encode(value, in: runtime)
|
|
66
|
+
#expect(try Set<Set<String>>.decode(encoded, in: runtime) == value)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Test
|
|
70
|
+
func `round-trips an empty set`() throws {
|
|
71
|
+
#expect(try Set<Int>.decode(runtime.eval("new Set()"), in: runtime) == [])
|
|
72
|
+
let encoded = try Set<Int>.encode([], in: runtime)
|
|
73
|
+
#expect(encoded.is("Set"))
|
|
74
|
+
#expect(encoded.getObject().getProperty("size").getInt() == 0)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Test
|
|
78
|
+
func `set decode rejects an element of the wrong type`() throws {
|
|
79
|
+
#expect(throws: (any Error).self) {
|
|
80
|
+
try Set<Int>.decode(runtime.eval("new Set([1, 'a'])"), in: runtime)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test
|
|
85
|
+
func `set decode collapses distinct JS objects that decode to equal values`() throws {
|
|
86
|
+
// JS `Set` compares objects by reference, so it holds both objects; Swift compares the decoded
|
|
87
|
+
// dictionaries by value, so they collapse into one element.
|
|
88
|
+
let decoded = try Set<[String: Int]>.decode(runtime.eval("new Set([{ a: 1 }, { a: 1 }, { b: 2 }])"), in: runtime)
|
|
89
|
+
#expect(decoded == [["a": 1], ["b": 2]])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Test
|
|
93
|
+
func `encodes a set of dictionaries to a JS Set of distinct objects`() throws {
|
|
94
|
+
let encoded = try Set<[String: Int]>.encode([["a": 1], ["b": 2]], in: runtime)
|
|
95
|
+
runtime.global().setProperty("encodedSet", value: encoded)
|
|
96
|
+
let summary = try runtime.eval(
|
|
97
|
+
"Array.from(encodedSet, (entry) => JSON.stringify(entry)).sort().join(',')"
|
|
98
|
+
)
|
|
99
|
+
#expect(summary.getString() == #"{"a":1},{"b":2}"#)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test
|
|
103
|
+
func `decodes a set argument and encodes a set result through a host function`() throws {
|
|
104
|
+
let fn = runtime.createFunction("uppercased") { [self] _, arguments in
|
|
105
|
+
let strings = try Set<String>.decode(arguments.unownedValue(at: 0), in: runtime)
|
|
106
|
+
return try Set<String>.encode(Set(strings.map { $0.uppercased() }), in: runtime)
|
|
107
|
+
}
|
|
108
|
+
runtime.global().setProperty("uppercased", value: fn.asValue())
|
|
109
|
+
let result = try runtime.eval(
|
|
110
|
+
"const result = uppercased(new Set(['a', 'b']))",
|
|
111
|
+
"result instanceof Set && result.size === 2 && result.has('A') && result.has('B')"
|
|
112
|
+
)
|
|
113
|
+
#expect(result.getBool() == true)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -92,6 +92,23 @@ struct JavaScriptRuntimeTests {
|
|
|
92
92
|
#expect(result.getString() == "hello")
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
@Test
|
|
96
|
+
func `evaluate to promise on the JavaScript thread when called from another thread`() async throws {
|
|
97
|
+
// The test body runs on a cooperative thread, not on the scheduler's thread. Evaluating there
|
|
98
|
+
// would run JavaScript concurrently with the work the scheduler runs, such as settling a promise.
|
|
99
|
+
let testRuntime = await TestRuntimeScheduler().makeRuntime()
|
|
100
|
+
let runtime = testRuntime.runtime
|
|
101
|
+
try await testRuntime.scheduler.runIsolated {
|
|
102
|
+
let isOnJavaScriptThread = runtime.createFunction("isOnJavaScriptThread") { _, _ in
|
|
103
|
+
return JavaScriptValue(runtime, runtime.isOnJavaScriptThread())
|
|
104
|
+
}
|
|
105
|
+
runtime.global().setProperty("isOnJavaScriptThread", value: isOnJavaScriptThread.asValue())
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let result = try await runtime.evalAsync("Promise.resolve(isOnJavaScriptThread())")
|
|
109
|
+
#expect(result.getBool() == true)
|
|
110
|
+
}
|
|
111
|
+
|
|
95
112
|
@Test
|
|
96
113
|
func `is equatable`() {
|
|
97
114
|
#expect((runtime == runtime) == true)
|
|
@@ -170,11 +187,11 @@ struct JavaScriptRuntimeTests {
|
|
|
170
187
|
}
|
|
171
188
|
|
|
172
189
|
await scheduler.run {
|
|
173
|
-
|
|
190
|
+
let didRunInline = InlineRunFlag()
|
|
174
191
|
runtime.runOrSchedule {
|
|
175
|
-
didRunInline = true
|
|
192
|
+
didRunInline.value = true
|
|
176
193
|
}
|
|
177
|
-
#expect(didRunInline)
|
|
194
|
+
#expect(didRunInline.value)
|
|
178
195
|
}
|
|
179
196
|
|
|
180
197
|
await withCheckedContinuation { continuation in
|
|
@@ -1148,6 +1165,13 @@ struct JavaScriptRuntimeTests {
|
|
|
1148
1165
|
}
|
|
1149
1166
|
}
|
|
1150
1167
|
|
|
1168
|
+
/// Records whether a `runOrSchedule` block ran. A class instead of a `nonisolated(unsafe) var` captured
|
|
1169
|
+
/// by the block, which Swift 6.2 rejects as a data race. Safe without synchronization: the test only
|
|
1170
|
+
/// reads it after the block ran inline on the same thread.
|
|
1171
|
+
private final class InlineRunFlag: @unchecked Sendable {
|
|
1172
|
+
var value = false
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1151
1175
|
/// Tasks captured by `holdSchedulerTask` instead of being executed, emulating a React
|
|
1152
1176
|
/// `RuntimeScheduler` that is torn down with work still queued (the #47716 reload scenario).
|
|
1153
1177
|
/// Safe without synchronization: the dispatch always runs synchronously on the test's thread.
|