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.
Files changed (34) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/apple/Benchmarks/ContainerCodingBenchmarks.swift +74 -0
  3. package/apple/Benchmarks/HostFunctionBenchmarks.swift +41 -0
  4. package/apple/Benchmarks/RuntimeCacheBenchmarks.swift +71 -0
  5. package/apple/Benchmarks/UnownedDecodeBenchmarks.swift +121 -0
  6. package/apple/Benchmarks/ValueAccessBenchmarks.swift +23 -0
  7. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Containers.swift +52 -10
  8. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Data.swift +5 -0
  9. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Date.swift +27 -5
  10. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +210 -21
  11. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Set.swift +79 -0
  12. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptDecodable.swift +34 -5
  13. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptValueKinds.swift +73 -0
  14. package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +4 -2
  15. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +43 -31
  16. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntimeCache.swift +74 -0
  17. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +26 -0
  18. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +97 -7
  19. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +14 -14
  20. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +17 -0
  21. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +54 -1
  22. package/apple/Sources/ExpoModulesJSI/Utilities/Errors.swift +19 -0
  23. package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +4 -3
  24. package/apple/Sources/ExpoModulesJSI/Utilities/UncheckedSendable.swift +13 -0
  25. package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +68 -0
  26. package/apple/Sources/ExpoModulesJSI-Cxx/include/RuntimeScheduler.h +18 -5
  27. package/apple/Tests/JavaScriptCodableSetTests.swift +115 -0
  28. package/apple/Tests/JavaScriptDecodableKindsTests.swift +132 -0
  29. package/apple/Tests/JavaScriptRuntimeCacheTests.swift +117 -0
  30. package/apple/Tests/JavaScriptRuntimeTests.swift +86 -3
  31. package/apple/Tests/JavaScriptUnownedDecodeTests.swift +99 -0
  32. package/apple/Tests/JavaScriptUnownedElementsTests.swift +76 -0
  33. package/apple/scripts/build-xcframework.sh +15 -0
  34. 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
- cachedDeferredPromiseFactory = nil
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
- nonisolated(unsafe) let resultPtr = resultPtr
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).writeJSIValue(to: resultPtr)
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
- let result = try eval(label: label, source)
696
- return result.is("Promise") ? try await result.getPromise().await() : result
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: - Deferred promise factory
800
+ // MARK: - Cache
793
801
 
794
- /// The JavaScript function ``JavaScriptPromise`` uses to create deferred promises, built on first
795
- /// use and released with the runtime. See `JavaScriptPromise.init(_:)` for why it exists.
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
- internal var cachedDeferredPromiseFactory: JavaScriptValue?
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 deferred-promise factory: a non-owning
830
- // wrapper can outlive its runtime (e.g. captured by a task abandoned on reload) and would
831
- // otherwise destroy them against the freed runtime when it deallocates. `self` is weak so
832
- // the teardown object doesn't retain the wrapper; the owning wrapper clears both in `deinit`.
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?.cachedDeferredPromiseFactory = nil
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 the `nonisolated(unsafe)` capture is sound. This removes a per-call class
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
- nonisolated(unsafe) let thisPtr = thisPtr
875
- nonisolated(unsafe) let argumentsPtr = argumentsPtr
876
- nonisolated(unsafe) let resultPtr = resultPtr
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).writeJSIValue(to: resultPtr)
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
- nonisolated(unsafe) let thisPtr = thisPtr
919
- nonisolated(unsafe) let argumentsPtr = argumentsPtr
920
- nonisolated(unsafe) let resultPtr = resultPtr
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).writeJSIValue(to: resultPtr)
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
- try! runtime
357
- .global()
358
- .getPropertyAsObject("Object")
359
- .getPropertyAsFunction("defineProperty")
360
- .call(arguments: self.asValue().ref(), JavaScriptValue(runtime, name).ref(), descriptor.ref())
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 descriptorObject = descriptor.toObject(runtime)
368
- defineProperty(name, descriptor: descriptorObject)
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
- if let factory = cachedDeferredPromiseFactory {
288
- return factory
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
- internal let pointee: facebook.jsi.Value
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
- chunk = String(unsafeUninitializedCapacity: count) { buffer in
73
- return buffer.initialize(fromContentsOf: bytes)
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
+ }