expo-modules-jsi 58.0.7 → 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 +22 -0
- package/apple/Benchmarks/ContainerCodingBenchmarks.swift +74 -0
- package/apple/Benchmarks/HostFunctionBenchmarks.swift +41 -0
- package/apple/Benchmarks/RuntimeCacheBenchmarks.swift +71 -0
- package/apple/Benchmarks/UnownedDecodeBenchmarks.swift +121 -0
- package/apple/Benchmarks/ValueAccessBenchmarks.swift +23 -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 +210 -21
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Set.swift +79 -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 +43 -31
- 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 +97 -7
- 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 +54 -1
- package/apple/Sources/ExpoModulesJSI/Utilities/Errors.swift +19 -0
- package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +4 -3
- package/apple/Sources/ExpoModulesJSI/Utilities/UncheckedSendable.swift +13 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +68 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/RuntimeScheduler.h +18 -5
- package/apple/Tests/JavaScriptCodableSetTests.swift +115 -0
- package/apple/Tests/JavaScriptDecodableKindsTests.swift +132 -0
- package/apple/Tests/JavaScriptRuntimeCacheTests.swift +117 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +86 -3
- 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
|
@@ -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()
|
|
@@ -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
|
|
|
@@ -197,13 +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
|
-
try context.get(propertyName)
|
|
206
|
+
var result = try context.get(propertyName)
|
|
207
|
+
JavaScriptValue.write(&result, to: resultPtr.value)
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
}
|
|
@@ -692,8 +693,15 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
692
693
|
@discardableResult
|
|
693
694
|
@JavaScriptActor
|
|
694
695
|
public func evalAsync(label: String? = nil, _ source: String) async throws -> JavaScriptValue {
|
|
695
|
-
|
|
696
|
-
|
|
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
|
|
697
705
|
}
|
|
698
706
|
|
|
699
707
|
// MARK: - Garbage collection
|
|
@@ -789,12 +797,14 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
789
797
|
@JavaScriptActor
|
|
790
798
|
internal var propNameIdsRegistry: [String: JavaScriptPropNameID] = [:]
|
|
791
799
|
|
|
792
|
-
// MARK: -
|
|
800
|
+
// MARK: - Cache
|
|
793
801
|
|
|
794
|
-
///
|
|
795
|
-
///
|
|
802
|
+
/// Values cached with ``cached(_:_:)``. Unchecked exclusivity skips the dynamic access checks on
|
|
803
|
+
/// every lookup: the cache is only used on the JavaScript thread, and `cached(_:_:)` never keeps an
|
|
804
|
+
/// access open while it calls out, so accesses can't overlap.
|
|
796
805
|
@JavaScriptActor
|
|
797
|
-
|
|
806
|
+
@exclusivity(unchecked)
|
|
807
|
+
internal var cache = Cache()
|
|
798
808
|
|
|
799
809
|
// MARK: - Long-lived objects
|
|
800
810
|
|
|
@@ -826,12 +836,12 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
826
836
|
// hop back to the JavaScript thread first.
|
|
827
837
|
JavaScriptActor.assumeIsolated {
|
|
828
838
|
longLivedObjects.clear()
|
|
829
|
-
// Also flush the cached `jsi::PropNameID`s and the
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
839
|
+
// Also flush the cached `jsi::PropNameID`s and the cache: a non-owning wrapper can outlive its
|
|
840
|
+
// runtime (e.g. captured by a task abandoned on reload) and would otherwise destroy them against
|
|
841
|
+
// the freed runtime when it deallocates. `self` is weak so the teardown object doesn't retain the
|
|
842
|
+
// wrapper; the owning wrapper clears both in `deinit`.
|
|
833
843
|
self?.propNameIdsRegistry.removeAll()
|
|
834
|
-
self?.
|
|
844
|
+
self?.cache.clear()
|
|
835
845
|
}
|
|
836
846
|
}
|
|
837
847
|
let object = createObject()
|
|
@@ -868,22 +878,23 @@ private func createFunctionClosure(
|
|
|
868
878
|
// heap-allocated `JavaScriptRef` (Swift 6.2 rejects capturing/consuming a `~Copyable` value in the
|
|
869
879
|
// escaping closure that `withoutActuallyEscaping` synthesizes), the closure constructs the buffer
|
|
870
880
|
// locally from the raw pointer + count. Those are read-only call-scoped inputs that never outlive the
|
|
871
|
-
// synchronous call, so
|
|
881
|
+
// synchronous call, so capturing them through `UncheckedSendable` is sound. This removes a per-call class
|
|
872
882
|
// allocation + retain/release + dealloc that profiling showed dominating the no-op `@JS` host-call
|
|
873
883
|
// floor.
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
884
|
+
let thisPtr = UncheckedSendable(thisPtr)
|
|
885
|
+
let argumentsPtr = UncheckedSendable(argumentsPtr)
|
|
886
|
+
let resultPtr = UncheckedSendable(resultPtr)
|
|
877
887
|
|
|
878
888
|
// See `withGuaranteedContext` for why neither the context nor the runtime is retained here, and
|
|
879
889
|
// why the result is written to the caller's slot instead of being returned.
|
|
880
890
|
return withGuaranteedContext(context) { (context: HostFunctionContext, runtime) in
|
|
881
891
|
return JavaScriptActor.assumeIsolated {
|
|
882
892
|
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
883
|
-
let this = UnsafeMutablePointer(mutating: thisPtr).move()
|
|
884
|
-
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)
|
|
885
895
|
let thisValue = JavaScriptValue(runtime, this)
|
|
886
|
-
try context.call(thisValue, consume arguments)
|
|
896
|
+
var result = try context.call(thisValue, consume arguments)
|
|
897
|
+
JavaScriptValue.write(&result, to: resultPtr.value)
|
|
887
898
|
}
|
|
888
899
|
}
|
|
889
900
|
}
|
|
@@ -915,18 +926,19 @@ private func createFunctionClosure(
|
|
|
915
926
|
// handed in as a borrowed `JavaScriptUnownedValue` pointing straight at the C++-owned `this` slot:
|
|
916
927
|
// it is not moved out and no owning `JavaScriptValue` is allocated, so the closure avoids the
|
|
917
928
|
// per-call `weak`-runtime form/destroy and heap object that the owning `this` pays.
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
929
|
+
let thisPtr = UncheckedSendable(thisPtr)
|
|
930
|
+
let argumentsPtr = UncheckedSendable(argumentsPtr)
|
|
931
|
+
let resultPtr = UncheckedSendable(resultPtr)
|
|
921
932
|
|
|
922
933
|
// See `withGuaranteedContext` for why neither the context nor the runtime is retained here, and
|
|
923
934
|
// why the result is written to the caller's slot instead of being returned.
|
|
924
935
|
return withGuaranteedContext(context) { (context: UnownedThisHostFunctionContext, runtime) in
|
|
925
936
|
return JavaScriptActor.assumeIsolated {
|
|
926
937
|
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
927
|
-
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr, count: argumentsCount)
|
|
928
|
-
let thisValue = JavaScriptUnownedValue(runtime.pointee, thisPtr)
|
|
929
|
-
try context.call(thisValue, consume arguments)
|
|
938
|
+
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr.value, count: argumentsCount)
|
|
939
|
+
let thisValue = JavaScriptUnownedValue(runtime.pointee, thisPtr.value)
|
|
940
|
+
var result = try context.call(thisValue, consume arguments)
|
|
941
|
+
JavaScriptValue.write(&result, to: resultPtr.value)
|
|
930
942
|
}
|
|
931
943
|
}
|
|
932
944
|
}
|
|
@@ -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
|
|
@@ -146,6 +146,28 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
146
146
|
return JavaScriptValue(runtimeHandle, pointee.getProperty(jsiRuntime, name.toJSIPropNameID(in: jsiRuntime)))
|
|
147
147
|
}
|
|
148
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
|
+
|
|
149
171
|
/// Returns the property of the object with the given prop name id,
|
|
150
172
|
/// or `undefined` value if the name is not a property of the object.
|
|
151
173
|
public func getProperty(_ propName: JavaScriptPropNameID) -> JavaScriptValue {
|
|
@@ -353,19 +375,50 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
353
375
|
guard let runtime else {
|
|
354
376
|
FatalError.runtimeLost()
|
|
355
377
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
.
|
|
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
|
+
}
|
|
361
384
|
}
|
|
362
385
|
|
|
363
386
|
public func defineProperty(_ name: String, descriptor: consuming PropertyDescriptor = .init()) {
|
|
364
387
|
guard let runtime else {
|
|
365
388
|
FatalError.runtimeLost()
|
|
366
389
|
}
|
|
367
|
-
let
|
|
368
|
-
|
|
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
|
+
}
|
|
369
422
|
}
|
|
370
423
|
|
|
371
424
|
public func defineProperty<T: JavaScriptRepresentable & ~Copyable>(
|
|
@@ -652,3 +705,40 @@ extension JavaScriptObject {
|
|
|
652
705
|
}
|
|
653
706
|
}
|
|
654
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
|
+
}
|
|
@@ -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.
|
|
@@ -10,7 +10,10 @@ public final class JavaScriptValue: JavaScriptType, Equatable, Escapable {
|
|
|
10
10
|
/// Handle to the runtime the value belongs to. `nil` only for runtime-free values (undefined, null,
|
|
11
11
|
/// booleans and numbers).
|
|
12
12
|
internal let runtimeHandle: JavaScriptRuntimeHandle?
|
|
13
|
-
|
|
13
|
+
/// Mutable only so that ``write(_:to:)`` can move the engine value out of a uniquely referenced
|
|
14
|
+
/// instance on the JS thread, right before it is deallocated. Unchecked exclusivity keeps reads of
|
|
15
|
+
/// a mutable class property free of the dynamic exclusivity check.
|
|
16
|
+
@exclusivity(unchecked) nonisolated(unsafe) internal var pointee: facebook.jsi.Value
|
|
14
17
|
|
|
15
18
|
/// The runtime the value belongs to, or `nil` if it has been deallocated or the value is runtime-free.
|
|
16
19
|
/// Prefer ``jsiRuntime`` on hot paths: it costs no reference counting.
|
|
@@ -119,6 +122,45 @@ public final class JavaScriptValue: JavaScriptType, Equatable, Escapable {
|
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
/// Calls `body` with a `JavaScriptUnownedValue` that borrows this value's `jsi::Value`, without
|
|
126
|
+
/// copying it. The unowned value is valid only for the duration of the closure and must not be
|
|
127
|
+
/// stored or escaped.
|
|
128
|
+
///
|
|
129
|
+
/// `runtime` must be the runtime the value belongs to. It is passed in because a runtime-free value
|
|
130
|
+
/// (undefined, null, a boolean or a number) doesn't hold one.
|
|
131
|
+
///
|
|
132
|
+
/// Inlinable, so the closure and `R` specialize in the caller; only `borrowUnownedValue(in:)` is a
|
|
133
|
+
/// call into this module.
|
|
134
|
+
@inlinable
|
|
135
|
+
public func withUnownedValue<R>(
|
|
136
|
+
in runtime: borrowing JavaScriptRuntime,
|
|
137
|
+
_ body: (borrowing JavaScriptUnownedValue) throws -> R
|
|
138
|
+
) rethrows -> R {
|
|
139
|
+
let unownedValue = borrowUnownedValue(in: runtime)
|
|
140
|
+
// The unowned value points into `self`, so `self` must outlive `body`.
|
|
141
|
+
defer { withExtendedLifetime(self) {} }
|
|
142
|
+
return try body(unownedValue)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// A `JavaScriptUnownedValue` pointing at the stored `jsi::Value`. It stays valid while `self` is
|
|
146
|
+
/// alive: the value is a stored property of this instance, so its address doesn't change, and
|
|
147
|
+
/// `withUnsafeBytes(of:)` yields that address because a `jsi::Value` can't be copied. Not inlinable,
|
|
148
|
+
/// since it touches the JSI types; `withUnownedValue(in:_:)` is the only caller.
|
|
149
|
+
@usableFromInline
|
|
150
|
+
internal func borrowUnownedValue(in runtime: borrowing JavaScriptRuntime) -> JavaScriptUnownedValue {
|
|
151
|
+
let pointer = withUnsafeBytes(of: pointee) { bytes in
|
|
152
|
+
// `withUnsafeBytes(of:)` rather than `withUnsafePointer(to:)`, for the same SIL optimizer crash
|
|
153
|
+
// `withUnsafePointee(_:)` avoids.
|
|
154
|
+
guard let baseAddress = bytes.baseAddress else {
|
|
155
|
+
preconditionFailure(
|
|
156
|
+
"withUnsafeBytes(of:) gave an empty buffer for a jsi::Value, which can't happen for a non-zero-sized type"
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
return baseAddress.assumingMemoryBound(to: facebook.jsi.Value.self)
|
|
160
|
+
}
|
|
161
|
+
return JavaScriptUnownedValue(runtime.pointee, pointer)
|
|
162
|
+
}
|
|
163
|
+
|
|
122
164
|
// MARK: - Type checks
|
|
123
165
|
|
|
124
166
|
public func isUndefined() -> Bool {
|
|
@@ -534,6 +576,17 @@ public final class JavaScriptValue: JavaScriptType, Equatable, Escapable {
|
|
|
534
576
|
return copy()
|
|
535
577
|
}
|
|
536
578
|
|
|
579
|
+
/// Writes `value` into a host callback's result slot. A uniquely referenced instance, the normal
|
|
580
|
+
/// case for a value the callback just created, has its engine value moved out instead of cloned,
|
|
581
|
+
/// since the instance is deallocated right after. Shared instances go through ``writeJSIValue(to:)``.
|
|
582
|
+
internal static func write(_ value: inout JavaScriptValue, to slot: UnsafeMutablePointer<facebook.jsi.Value>) {
|
|
583
|
+
if value.runtimeHandle != nil, isKnownUniquelyReferenced(&value) {
|
|
584
|
+
expo.emplaceMovedValue(slot, &value.pointee)
|
|
585
|
+
} else {
|
|
586
|
+
value.writeJSIValue(to: slot)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
537
590
|
/// Writes this value into a host callback's result slot. Undefined, null, booleans and numbers are
|
|
538
591
|
/// emplaced with the engine's inline constructors, so the common results skip the out-of-line
|
|
539
592
|
/// `jsi::Value` move and destroy that assigning to the slot would cost; everything else is copied
|
|
@@ -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
|
}
|
|
@@ -69,9 +69,10 @@ private func appendEngineStringChunk(ctx: UnsafeMutableRawPointer?, ascii: Bool,
|
|
|
69
69
|
let chunk: String
|
|
70
70
|
if ascii {
|
|
71
71
|
let bytes = UnsafeBufferPointer(start: data.assumingMemoryBound(to: UInt8.self), count: count)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
// `String(decoding:as:)` re-validates bytes the engine already guarantees to be ASCII, but it
|
|
73
|
+
// builds short strings inline. `String(unsafeUninitializedCapacity:)` always allocates heap
|
|
74
|
+
// storage first, which measured 5 to 10 ns slower for 6 and 22 byte strings.
|
|
75
|
+
chunk = String(decoding: bytes, as: UTF8.self)
|
|
75
76
|
} else {
|
|
76
77
|
let units = UnsafeBufferPointer(start: data.assumingMemoryBound(to: UInt16.self), count: count)
|
|
77
78
|
if count < 512 {
|
|
@@ -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
|
+
}
|