expo-modules-jsi 57.0.8 → 57.1.1
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 +26 -0
- package/apple/Benchmarks/PromiseBenchmarks.swift +43 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Containers.swift +14 -8
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Task.swift +35 -0
- package/apple/Sources/ExpoModulesJSI/Contexts/HostCallbackContext.swift +9 -9
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +32 -18
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptValuesBuffer.swift +32 -17
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +96 -20
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +35 -4
- package/apple/Sources/ExpoModulesJSI/Utilities/ErrorHandling.swift +27 -20
- package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +4 -1
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostFunctionClosure.h +5 -3
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObject.h +8 -6
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObjectCallbacks.h +8 -6
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +62 -4
- package/apple/Tests/JavaScriptCodableTaskTests.swift +116 -0
- package/apple/Tests/JavaScriptPromiseTests.swift +75 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +50 -0
- package/apple/Tests/Support/TestRuntimeScheduler.swift +151 -0
- package/apple/scripts/create-stub-xcframework.sh +1 -1
- package/package.json +2 -2
- package/spm.config.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,31 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 57.1.1 — 2026-09-24
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [iOS] Fix `pod install` failing when the Command Line Tools SDK is newer than the selected Xcode. ([#50316](https://github.com/expo/expo/pull/50316) by [@alanjhughes](https://github.com/alanjhughes))
|
|
18
|
+
|
|
19
|
+
## 57.1.0 — 2026-09-08
|
|
20
|
+
|
|
21
|
+
### 🎉 New features
|
|
22
|
+
|
|
23
|
+
- [iOS] Add a `JavaScriptEncodable` conformance for `Task` that encodes it to a JS `Promise` settling with the task's result, so native code can hand JavaScript a promise as a value. ([#47861](https://github.com/expo/expo/pull/47861) by [@tsapeta](https://github.com/tsapeta))
|
|
24
|
+
- [iOS] Split the `Array`, `Optional`, and `Dictionary` `JavaScriptCodable` conformances into separate `JavaScriptDecodable` and `JavaScriptEncodable` halves so an encode-only element type such as `Task` can be carried through a container's encode. ([#47861](https://github.com/expo/expo/pull/47861) by [@tsapeta](https://github.com/tsapeta))
|
|
25
|
+
- [iOS] Add a `JavaScriptPromise.resolve` overload that takes a `JavaScriptEncodable` value, encoding it on the JavaScript thread and rejecting the promise if encoding or the resolver call throws. ([#47862](https://github.com/expo/expo/pull/47862) by [@tsapeta](https://github.com/tsapeta))
|
|
26
|
+
|
|
27
|
+
### 🐛 Bug fixes
|
|
28
|
+
|
|
29
|
+
- [iOS] Fixed a use-after-free when a non-owning `JavaScriptRuntime` wrapper outlives its runtime (e.g. it is captured by a task abandoned on reload): its cached `jsi::PropNameID`s were destroyed against the freed runtime when the wrapper deallocated. The teardown sweep now flushes the cache on the JavaScript thread while the runtime is still valid. ([#47927](https://github.com/expo/expo/pull/47927) by [@tsapeta](https://github.com/tsapeta))
|
|
30
|
+
- [iOS] `JavaScriptPromise` no longer traps when a resolve or reject call throws, which can realistically only happen against a runtime that is being torn down: a failed resolver call rejects the promise instead and a failed rejecter call is dropped. ([#47862](https://github.com/expo/expo/pull/47862) by [@tsapeta](https://github.com/tsapeta))
|
|
31
|
+
|
|
32
|
+
### 💡 Others
|
|
33
|
+
|
|
34
|
+
- [iOS] Made creating a deferred `JavaScriptPromise` ~1.2× faster by building it from a cached JavaScript closure instead of a host function executor. ([#49714](https://github.com/expo/expo/pull/49714) by [@tsapeta](https://github.com/tsapeta))
|
|
35
|
+
- [iOS] Reduced the native overhead of synchronous host function calls and host object property accessors that return `undefined`, `null`, a boolean or a number: the result is written into the engine's slot without engine calls, and errors are reported only when one was actually thrown instead of being checked on every call. ([#49761](https://github.com/expo/expo/pull/49761) by [@tsapeta](https://github.com/tsapeta))
|
|
36
|
+
- [iOS] Reduced the native overhead of synchronous host function calls whose closure receives `this` as a `JavaScriptValue`, by letting the calling module destroy the arguments buffer directly. ([#49769](https://github.com/expo/expo/pull/49769) by [@tsapeta](https://github.com/tsapeta))
|
|
37
|
+
|
|
13
38
|
## 57.0.8 — 2026-09-04
|
|
14
39
|
|
|
15
40
|
### 🎉 New features
|
|
@@ -18,6 +43,7 @@
|
|
|
18
43
|
|
|
19
44
|
### 🐛 Bug fixes
|
|
20
45
|
|
|
46
|
+
- [iOS] Fixed the build against React Native older than 0.86 (e.g. react-native-macos 0.81), where `jsi::Runtime::getStringData` is a protected member that Swift cannot call. String decoding now goes through a C++ wrapper that uses the public `jsi::String::getStringData` helper on those versions. ([#49790](https://github.com/expo/expo/pull/49790) by [@tsapeta](https://github.com/tsapeta))
|
|
21
47
|
- [iOS] Fixed `JavaScriptPropNameID(_:string:)` and the array's string-keyed subscript truncating non-ASCII property keys: they passed `String.count` (the grapheme-cluster count) as the UTF-8 byte length to `PropNameID::forUtf8`, so keys like `"café"` or `"🎉"` were built from mangled bytes and no longer matched the intended property. ([#48329](https://github.com/expo/expo/pull/48329) by [@tsapeta](https://github.com/tsapeta))
|
|
22
48
|
- [iOS] Fixed the prebuilt `ExpoModulesJSI.xcframework` shipping with code coverage instrumentation: building through the auto-generated SwiftPM scheme made Xcode pass `-profile-generate -profile-coverage-mapping` to swiftc even for a plain Release `build`, adding a counter increment to every function on the host function call path and about 40% to the binary size. The benchmark target had the same instrumentation and now runs without it. ([#49637](https://github.com/expo/expo/pull/49637) by [@tsapeta](https://github.com/tsapeta))
|
|
23
49
|
- [iOS] Fixed property names with non-ASCII characters being mangled when accessed by name from Swift, such as `getProperty`, `setProperty`, `hasProperty`, the array string subscript and dictionary conversions. ([#49679](https://github.com/expo/expo/pull/49679) by [@tsapeta](https://github.com/tsapeta))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Testing
|
|
5
|
+
|
|
6
|
+
/// Benchmarks for creating and settling deferred promises from Swift. The engine floor is measured
|
|
7
|
+
/// from JavaScript for comparison, so the wrapper's own overhead can be read off directly.
|
|
8
|
+
extension JSIBenchmarks {
|
|
9
|
+
@Test
|
|
10
|
+
func `engine floor: new Promise from JavaScript`() async throws {
|
|
11
|
+
try await benchmarkCase { runtime in
|
|
12
|
+
let driver = try runtime.eval(
|
|
13
|
+
"(function(n) { for (var i = 0; i < n; i++) new Promise(function (a, b) {}); })"
|
|
14
|
+
).getFunction()
|
|
15
|
+
try benchmark("engine floor: new Promise from JS", runtime: runtime) { iterations in
|
|
16
|
+
_ = try driver.call(arguments: iterations)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Test
|
|
22
|
+
func `create deferred promise`() async throws {
|
|
23
|
+
try await benchmarkCase { runtime in
|
|
24
|
+
try benchmark("JavaScriptPromise(runtime): create deferred", runtime: runtime) { iterations in
|
|
25
|
+
for _ in 0..<iterations {
|
|
26
|
+
_ = try JavaScriptPromise(runtime)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Test
|
|
33
|
+
func `create and resolve deferred promise`() async throws {
|
|
34
|
+
try await benchmarkCase { runtime in
|
|
35
|
+
try benchmark("JavaScriptPromise(runtime): create and resolve", runtime: runtime) { iterations in
|
|
36
|
+
for _ in 0..<iterations {
|
|
37
|
+
let promise = try JavaScriptPromise(runtime)
|
|
38
|
+
promise.resolve(42.0)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
2
|
|
|
3
|
-
// `JavaScriptCodable` conformances for the standard container and wrapper types
|
|
4
|
-
// `Optional`,
|
|
5
|
-
// each recursing statically into that element's conversion.
|
|
3
|
+
// `JavaScriptCodable` conformances for the standard container and wrapper types (`Array`,
|
|
4
|
+
// `Optional`, `Dictionary`), each recursing statically into its element/wrapped type's conversion.
|
|
6
5
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// The decodable and encodable halves are separate conditional conformances, each gated only on the
|
|
7
|
+
// half it needs. A type conforming to both still gets both halves, but an encode-only element type
|
|
8
|
+
// (e.g. `Task`, which has no `decode`) can still be carried through a container's encode.
|
|
9
9
|
|
|
10
10
|
// MARK: - Array
|
|
11
11
|
|
|
12
|
-
extension Array: JavaScriptDecodable
|
|
12
|
+
extension Array: JavaScriptDecodable where Element: JavaScriptDecodable {
|
|
13
13
|
@JavaScriptActor
|
|
14
14
|
@inlinable
|
|
15
15
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
@@ -26,7 +26,9 @@ extension Array: JavaScriptDecodable, JavaScriptEncodable where Element: JavaScr
|
|
|
26
26
|
return try Element.decode(element, in: runtime)
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
}
|
|
29
30
|
|
|
31
|
+
extension Array: JavaScriptEncodable where Element: JavaScriptEncodable {
|
|
30
32
|
@JavaScriptActor
|
|
31
33
|
@inlinable
|
|
32
34
|
public static func encode(_ value: [Element], in runtime: borrowing JavaScriptRuntime) throws
|
|
@@ -42,7 +44,7 @@ extension Array: JavaScriptDecodable, JavaScriptEncodable where Element: JavaScr
|
|
|
42
44
|
|
|
43
45
|
// MARK: - Optional
|
|
44
46
|
|
|
45
|
-
extension Optional: JavaScriptDecodable
|
|
47
|
+
extension Optional: JavaScriptDecodable where Wrapped: JavaScriptDecodable {
|
|
46
48
|
// Optional copies nothing itself, so it overrides the zero-copy overload too and forwards the
|
|
47
49
|
// borrowed value straight through — a wrapped primitive argument stays fully zero-copy.
|
|
48
50
|
@JavaScriptActor
|
|
@@ -66,7 +68,9 @@ extension Optional: JavaScriptDecodable, JavaScriptEncodable where Wrapped: Java
|
|
|
66
68
|
}
|
|
67
69
|
return try Wrapped.decode(value, in: runtime)
|
|
68
70
|
}
|
|
71
|
+
}
|
|
69
72
|
|
|
73
|
+
extension Optional: JavaScriptEncodable where Wrapped: JavaScriptEncodable {
|
|
70
74
|
@JavaScriptActor
|
|
71
75
|
@inlinable
|
|
72
76
|
public static func encode(_ value: Wrapped?, in runtime: borrowing JavaScriptRuntime) throws
|
|
@@ -82,7 +86,7 @@ extension Optional: JavaScriptDecodable, JavaScriptEncodable where Wrapped: Java
|
|
|
82
86
|
|
|
83
87
|
// MARK: - Dictionary
|
|
84
88
|
|
|
85
|
-
extension Dictionary: JavaScriptDecodable
|
|
89
|
+
extension Dictionary: JavaScriptDecodable where Key == String, Value: JavaScriptDecodable {
|
|
86
90
|
@JavaScriptActor
|
|
87
91
|
@inlinable
|
|
88
92
|
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
|
|
@@ -102,7 +106,9 @@ extension Dictionary: JavaScriptDecodable, JavaScriptEncodable where Key == Stri
|
|
|
102
106
|
}
|
|
103
107
|
return result
|
|
104
108
|
}
|
|
109
|
+
}
|
|
105
110
|
|
|
111
|
+
extension Dictionary: JavaScriptEncodable where Key == String, Value: JavaScriptEncodable {
|
|
106
112
|
@JavaScriptActor
|
|
107
113
|
@inlinable
|
|
108
114
|
public static func encode(_ value: [String: Value], in runtime: borrowing JavaScriptRuntime) throws
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
// `JavaScriptEncodable` conformance for `Task`, encoding it to a JavaScript `Promise`. This lets
|
|
4
|
+
// native code hand JavaScript a promise as a value: the return of a synchronous `@JS func` whose
|
|
5
|
+
// result type is `Task`, or a `Task` nested inside another encoded value. `createAsyncFunction`
|
|
6
|
+
// only wraps a function's own return in a promise, so a `Task` is how to produce one anywhere else.
|
|
7
|
+
//
|
|
8
|
+
// Encode-only: a promise decodes by awaiting it through `JavaScriptPromise`, not by reconstructing
|
|
9
|
+
// a `Task`. One conditional conformance covers throwing and non-throwing tasks (Swift forbids two
|
|
10
|
+
// conformances even with disjoint bounds, and `Task.value` is `async throws` for any `Failure`).
|
|
11
|
+
|
|
12
|
+
/// Encodes a `Task` to a JavaScript promise that settles with the task's result.
|
|
13
|
+
extension Task: JavaScriptEncodable where Success: JavaScriptEncodable, Failure: Error {
|
|
14
|
+
/// Returns a pending promise immediately and settles it on the JavaScript thread once the task
|
|
15
|
+
/// completes: fulfilling with the encoded `Success` value, or rejecting with the thrown error.
|
|
16
|
+
@JavaScriptActor
|
|
17
|
+
public static func encode(_ value: Task<Success, Failure>, in runtime: borrowing JavaScriptRuntime) throws
|
|
18
|
+
-> JavaScriptValue
|
|
19
|
+
{
|
|
20
|
+
let promise = try JavaScriptPromise(copy runtime)
|
|
21
|
+
// A detached task awaits the result off the JavaScript thread, then settles the promise. It must
|
|
22
|
+
// not inherit `@JavaScriptActor`: that actor runs jobs synchronously on the current thread rather
|
|
23
|
+
// than hopping to the JS thread, so a resumed continuation would land on whatever thread the task
|
|
24
|
+
// finished on. `resolve`/`reject` hop to the JS thread internally (and the encodable `resolve`
|
|
25
|
+
// encodes there), so settling from here is safe regardless of this task's thread.
|
|
26
|
+
Task<Void, Never>.detached {
|
|
27
|
+
do {
|
|
28
|
+
promise.resolve(try await value.value)
|
|
29
|
+
} catch {
|
|
30
|
+
promise.reject(error)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return promise.asValue()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -12,17 +12,17 @@ internal protocol HostCallbackContext: AnyObject {
|
|
|
12
12
|
/// `_withUnsafeGuaranteedRef` promises the compiler that both objects outlive the closure, so no
|
|
13
13
|
/// retain or release is emitted for either. Both promises hold for a JSI callback: the JSI owner
|
|
14
14
|
/// of the context is the caller, and a synchronous callback runs while the runtime executes JS,
|
|
15
|
-
/// with the wrapper owned for the runtime's whole lifetime. The body
|
|
16
|
-
/// `_withUnsafeGuaranteedRef`
|
|
17
|
-
///
|
|
15
|
+
/// with the wrapper owned for the runtime's whole lifetime. The body's result must be `Copyable`
|
|
16
|
+
/// because `_withUnsafeGuaranteedRef` requires it; callbacks write their `jsi::Value` result into the
|
|
17
|
+
/// slot the C++ caller provides and return only whether an error was stored.
|
|
18
18
|
@inline(__always)
|
|
19
|
-
internal func withGuaranteedContext<Context: HostCallbackContext>(
|
|
19
|
+
internal func withGuaranteedContext<Context: HostCallbackContext, Result>(
|
|
20
20
|
_ pointer: UnsafeMutableRawPointer,
|
|
21
|
-
_ body: (_ context: Context, _ runtime: JavaScriptRuntime) ->
|
|
22
|
-
) {
|
|
23
|
-
Unmanaged<Context>.fromOpaque(pointer)._withUnsafeGuaranteedRef { context in
|
|
24
|
-
context.runtime._withUnsafeGuaranteedRef { runtime in
|
|
25
|
-
body(context, runtime)
|
|
21
|
+
_ body: (_ context: Context, _ runtime: JavaScriptRuntime) -> Result
|
|
22
|
+
) -> Result {
|
|
23
|
+
return Unmanaged<Context>.fromOpaque(pointer)._withUnsafeGuaranteedRef { context in
|
|
24
|
+
return context.runtime._withUnsafeGuaranteedRef { runtime in
|
|
25
|
+
return body(context, runtime)
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -133,6 +133,7 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
133
133
|
// when the last reference is gone. `deinit` is `nonisolated`, so it can touch the actor-isolated
|
|
134
134
|
// registry directly given that exclusive access.
|
|
135
135
|
propNameIdsRegistry.removeAll()
|
|
136
|
+
cachedDeferredPromiseFactory = nil
|
|
136
137
|
expo.destroyRuntime(runtimePointee)
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -182,14 +183,14 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
182
183
|
context: UnsafeMutableRawPointer,
|
|
183
184
|
propertyName: UnsafePointer<CChar>,
|
|
184
185
|
resultPtr: UnsafeMutablePointer<facebook.jsi.Value>
|
|
185
|
-
) {
|
|
186
|
+
) -> Bool {
|
|
186
187
|
let propertyName = String(cString: propertyName)
|
|
187
188
|
nonisolated(unsafe) let resultPtr = resultPtr
|
|
188
189
|
|
|
189
|
-
withGuaranteedContext(context) { (context: HostObjectContext, runtime) in
|
|
190
|
-
|
|
190
|
+
return withGuaranteedContext(context) { (context: HostObjectContext, runtime) in
|
|
191
|
+
return JavaScriptActor.assumeIsolated {
|
|
191
192
|
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
192
|
-
|
|
193
|
+
try context.get(propertyName).writeJSIValue(to: resultPtr)
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
}
|
|
@@ -197,10 +198,10 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
197
198
|
|
|
198
199
|
func setter(
|
|
199
200
|
context: UnsafeMutableRawPointer, propertyName: UnsafePointer<CChar>, valuePointer: UnsafeMutableRawPointer
|
|
200
|
-
) {
|
|
201
|
+
) -> Bool {
|
|
201
202
|
let propertyName = String(cString: propertyName)
|
|
202
203
|
|
|
203
|
-
withGuaranteedContext(context) { (context: HostObjectContext, runtime) in
|
|
204
|
+
return withGuaranteedContext(context) { (context: HostObjectContext, runtime) in
|
|
204
205
|
guard let set = context.set else {
|
|
205
206
|
// Unreachable in practice: when the user passed `nil` for `set`, the call site
|
|
206
207
|
// below at `expo.HostObjectCallbacks(...)` also passes `nil` to C++, and
|
|
@@ -211,8 +212,8 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
211
212
|
}
|
|
212
213
|
let value = JavaScriptValue(runtime, valuePointer.assumingMemoryBound(to: facebook.jsi.Value.self).move())
|
|
213
214
|
|
|
214
|
-
JavaScriptActor.assumeIsolated {
|
|
215
|
-
forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
215
|
+
return JavaScriptActor.assumeIsolated {
|
|
216
|
+
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
216
217
|
try set(propertyName, value)
|
|
217
218
|
}
|
|
218
219
|
}
|
|
@@ -253,7 +254,7 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
253
254
|
let context = Unmanaged.passRetained(HostObjectContext(runtime: self, get, set, getPropertyNames, dealloc))
|
|
254
255
|
.toOpaque()
|
|
255
256
|
let setterPointer:
|
|
256
|
-
(@convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>, UnsafeMutableRawPointer) ->
|
|
257
|
+
(@convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>, UnsafeMutableRawPointer) -> Bool)? = setter
|
|
257
258
|
// Pass a null setter to C++ when the Swift setter is nil so that JS assignment
|
|
258
259
|
// raises a `jsi::JSError` directly, without crossing the Swift boundary.
|
|
259
260
|
let callbacks = expo.HostObjectCallbacks(
|
|
@@ -693,6 +694,13 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
693
694
|
@JavaScriptActor
|
|
694
695
|
internal var propNameIdsRegistry: [String: JavaScriptPropNameID] = [:]
|
|
695
696
|
|
|
697
|
+
// MARK: - Deferred promise factory
|
|
698
|
+
|
|
699
|
+
/// The JavaScript function ``JavaScriptPromise`` uses to create deferred promises, built on first
|
|
700
|
+
/// use and released with the runtime. See `JavaScriptPromise.init(_:)` for why it exists.
|
|
701
|
+
@JavaScriptActor
|
|
702
|
+
internal var cachedDeferredPromiseFactory: JavaScriptValue?
|
|
703
|
+
|
|
696
704
|
// MARK: - Long-lived objects
|
|
697
705
|
|
|
698
706
|
/// Registry of JSI objects (such as in-flight promises) that must outlive the native call that
|
|
@@ -716,13 +724,19 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
716
724
|
// collection keeps it alive for the sweep; it does not retain the runtime.
|
|
717
725
|
let longLivedObjects = self.longLivedObjects
|
|
718
726
|
let nativeState = JavaScriptNativeState()
|
|
719
|
-
nativeState.setDeallocator { nativeState in
|
|
727
|
+
nativeState.setDeallocator { [weak self] nativeState in
|
|
720
728
|
// Fires as the teardown object is released on the JavaScript thread with the runtime still
|
|
721
729
|
// valid, so releasing JSI state here is safe. Mirrors the caveat on `AppContext.NativeState`:
|
|
722
730
|
// a future cross-runtime path that could drop this state from another thread would have to
|
|
723
731
|
// hop back to the JavaScript thread first.
|
|
724
732
|
JavaScriptActor.assumeIsolated {
|
|
725
733
|
longLivedObjects.clear()
|
|
734
|
+
// Also flush the cached `jsi::PropNameID`s and the deferred-promise factory: a non-owning
|
|
735
|
+
// wrapper can outlive its runtime (e.g. captured by a task abandoned on reload) and would
|
|
736
|
+
// otherwise destroy them against the freed runtime when it deallocates. `self` is weak so
|
|
737
|
+
// the teardown object doesn't retain the wrapper; the owning wrapper clears both in `deinit`.
|
|
738
|
+
self?.propNameIdsRegistry.removeAll()
|
|
739
|
+
self?.cachedDeferredPromiseFactory = nil
|
|
726
740
|
}
|
|
727
741
|
}
|
|
728
742
|
let object = createObject()
|
|
@@ -750,7 +764,7 @@ private func createFunctionClosure(
|
|
|
750
764
|
argumentsPtr: UnsafePointer<facebook.jsi.Value>,
|
|
751
765
|
argumentsCount: Int,
|
|
752
766
|
resultPtr: UnsafeMutablePointer<facebook.jsi.Value>
|
|
753
|
-
) {
|
|
767
|
+
) -> Bool {
|
|
754
768
|
// `assumeIsolated` runs `operation` synchronously, in this very scope — it never escapes and never
|
|
755
769
|
// hops threads (see `JavaScriptActor.assumeIsolated`). So rather than materializing the move-only
|
|
756
770
|
// `JavaScriptValuesBuffer` out here and smuggling it across the closure boundary through a
|
|
@@ -766,13 +780,13 @@ private func createFunctionClosure(
|
|
|
766
780
|
|
|
767
781
|
// See `withGuaranteedContext` for why neither the context nor the runtime is retained here, and
|
|
768
782
|
// why the result is written to the caller's slot instead of being returned.
|
|
769
|
-
withGuaranteedContext(context) { (context: HostFunctionContext, runtime) in
|
|
770
|
-
|
|
783
|
+
return withGuaranteedContext(context) { (context: HostFunctionContext, runtime) in
|
|
784
|
+
return JavaScriptActor.assumeIsolated {
|
|
771
785
|
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
772
786
|
let this = UnsafeMutablePointer(mutating: thisPtr).move()
|
|
773
787
|
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr, count: argumentsCount)
|
|
774
788
|
let thisValue = JavaScriptValue(runtime, this)
|
|
775
|
-
|
|
789
|
+
try context.call(thisValue, consume arguments).writeJSIValue(to: resultPtr)
|
|
776
790
|
}
|
|
777
791
|
}
|
|
778
792
|
}
|
|
@@ -797,7 +811,7 @@ private func createFunctionClosure(
|
|
|
797
811
|
argumentsPtr: UnsafePointer<facebook.jsi.Value>,
|
|
798
812
|
argumentsCount: Int,
|
|
799
813
|
resultPtr: UnsafeMutablePointer<facebook.jsi.Value>
|
|
800
|
-
) {
|
|
814
|
+
) -> Bool {
|
|
801
815
|
// Same call-scoped reasoning as the owning-`this` overload above (see its comment) for why the
|
|
802
816
|
// buffer is built inside the synchronous `assumeIsolated` closure. Here `this` is additionally
|
|
803
817
|
// handed in as a borrowed `JavaScriptUnownedValue` pointing straight at the C++-owned `this` slot:
|
|
@@ -809,12 +823,12 @@ private func createFunctionClosure(
|
|
|
809
823
|
|
|
810
824
|
// See `withGuaranteedContext` for why neither the context nor the runtime is retained here, and
|
|
811
825
|
// why the result is written to the caller's slot instead of being returned.
|
|
812
|
-
withGuaranteedContext(context) { (context: UnownedThisHostFunctionContext, runtime) in
|
|
813
|
-
|
|
826
|
+
return withGuaranteedContext(context) { (context: UnownedThisHostFunctionContext, runtime) in
|
|
827
|
+
return JavaScriptActor.assumeIsolated {
|
|
814
828
|
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
815
829
|
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr, count: argumentsCount)
|
|
816
830
|
let thisValue = JavaScriptUnownedValue(runtime.pointee, thisPtr)
|
|
817
|
-
|
|
831
|
+
try context.call(thisValue, consume arguments).writeJSIValue(to: resultPtr)
|
|
818
832
|
}
|
|
819
833
|
}
|
|
820
834
|
}
|
|
@@ -3,24 +3,44 @@ internal import jsi
|
|
|
3
3
|
/// A buffer that stores instances of `facebook.jsi.Value` with the ability to convert them to `JavaScriptValue` on element access
|
|
4
4
|
/// without the need to create a new container (e.g. `std.vector<facebook.jsi.Value>` or `[JavaScriptValue]`).
|
|
5
5
|
/// Used mainly to pass function arguments from C++ to Swift.
|
|
6
|
+
// `@frozen` so client modules, which consume a buffer in every host function closure, can destroy it
|
|
7
|
+
// with a direct call to its `deinit` instead of a metadata accessor plus a value witness dispatch.
|
|
8
|
+
@frozen
|
|
6
9
|
public struct JavaScriptValuesBuffer: JavaScriptType, ~Copyable {
|
|
7
|
-
//
|
|
10
|
+
// MARK: - Layout
|
|
11
|
+
|
|
12
|
+
// Every stored property below is part of the frozen layout: adding, removing or reordering one
|
|
13
|
+
// changes the ABI that client modules compiled against, and each type has to be public, which is
|
|
14
|
+
// why the storage is kept as a raw pointer and rebuilt into its C++ view on access.
|
|
15
|
+
|
|
16
|
+
// Safe to use unowned: the buffer's lifetime is scoped to a host function call,
|
|
8
17
|
// so the runtime is always alive while the buffer exists.
|
|
9
18
|
//
|
|
10
19
|
// `unowned(unsafe)` rather than `unowned`: the runtime object carries a refcount side table (other
|
|
11
20
|
// wrappers hold it `weak`), so a safe `unowned` retain/release on it takes the slow side-table path
|
|
12
21
|
// on every buffer construction and destruction. Profiling showed that pair as roughly a third of the
|
|
13
|
-
// native-side cost of a no-op
|
|
22
|
+
// native-side cost of a no-op host function call. `unowned(unsafe)` is a plain pointer store/load.
|
|
14
23
|
internal unowned(unsafe) let runtime: JavaScriptRuntime
|
|
15
24
|
|
|
16
|
-
// The
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
// The start of the storage, type-erased because `facebook.jsi.Value` is not a public type.
|
|
26
|
+
internal nonisolated(unsafe) let start: UnsafeMutableRawPointer?
|
|
27
|
+
|
|
28
|
+
/// The number of values in the buffer.
|
|
29
|
+
public let count: Int
|
|
30
|
+
|
|
31
|
+
internal let ownsMemory: Bool
|
|
32
|
+
|
|
33
|
+
// MARK: - C++ views
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
// `IRuntime` is an immortal reference (`jsi.apinotes`) and `runtime` is `unowned(unsafe)`, so this is
|
|
36
|
+
// two plain loads with no reference counting.
|
|
37
|
+
internal var iRuntime: facebook.jsi.IRuntime {
|
|
38
|
+
return runtime.pointee
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
internal var bufferPointer: UnsafeMutableBufferPointer<facebook.jsi.Value> {
|
|
42
|
+
return UnsafeMutableBufferPointer(start: start?.assumingMemoryBound(to: facebook.jsi.Value.self), count: count)
|
|
43
|
+
}
|
|
24
44
|
|
|
25
45
|
/// A pointer to the first value of the buffer.
|
|
26
46
|
/// If the baseAddress of this buffer is `nil`, the `count` is zero.
|
|
@@ -32,12 +52,7 @@ public struct JavaScriptValuesBuffer: JavaScriptType, ~Copyable {
|
|
|
32
52
|
/// passing across Swift/ObjC++ boundaries where `facebook.jsi.Value` cannot
|
|
33
53
|
/// appear in a public signature.
|
|
34
54
|
public var rawBaseAddress: UnsafeRawPointer? {
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/// The number of values in the buffer.
|
|
39
|
-
public var count: Int {
|
|
40
|
-
return bufferPointer.count
|
|
55
|
+
return start.map { UnsafeRawPointer($0) }
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
internal init(
|
|
@@ -45,8 +60,8 @@ public struct JavaScriptValuesBuffer: JavaScriptType, ~Copyable {
|
|
|
45
60
|
ownsMemory: Bool = false
|
|
46
61
|
) {
|
|
47
62
|
self.runtime = runtime
|
|
48
|
-
self.
|
|
49
|
-
self.
|
|
63
|
+
self.start = UnsafeMutableRawPointer(buffer.baseAddress)
|
|
64
|
+
self.count = buffer.count
|
|
50
65
|
self.ownsMemory = ownsMemory
|
|
51
66
|
}
|
|
52
67
|
|
|
@@ -80,19 +80,15 @@ public struct JavaScriptPromise: JavaScriptType, ~Copyable {
|
|
|
80
80
|
public init(_ runtime: JavaScriptRuntime) throws {
|
|
81
81
|
self.runtime = runtime
|
|
82
82
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.global()
|
|
93
|
-
.getPropertyAsFunction(.cached(runtime, "Promise"))
|
|
94
|
-
.callAsConstructor(setup.asValue())
|
|
95
|
-
longLivedState.object.reset(object)
|
|
83
|
+
// The promise and its two settle functions come from a JavaScript closure that returns all three
|
|
84
|
+
// at once, rather than from `new Promise(executor)` with a host function as the executor. The
|
|
85
|
+
// host function route costs a native function and a Swift context object per promise, a re-entry
|
|
86
|
+
// from the engine into Swift while the constructor runs, and an owning copy of each settle
|
|
87
|
+
// function on the way back. The closure route is one JS call and three array reads.
|
|
88
|
+
let triple = try runtime.deferredPromiseFactory().getFunction().call().getArray()
|
|
89
|
+
longLivedState.object.reset(try triple.getValue(at: 0))
|
|
90
|
+
longLivedState.resolveFunction.reset(try triple.getValue(at: 1))
|
|
91
|
+
longLivedState.rejectFunction.reset(try triple.getValue(at: 2))
|
|
96
92
|
try setUpCallbacks()
|
|
97
93
|
// Register only after setup succeeds, so a failed initializer (e.g. `then` unavailable) doesn't
|
|
98
94
|
// leave the state pinned in the collection until teardown. Owns the promise's JSI values from
|
|
@@ -121,6 +117,13 @@ public struct JavaScriptPromise: JavaScriptType, ~Copyable {
|
|
|
121
117
|
} ?? .undefined
|
|
122
118
|
}
|
|
123
119
|
|
|
120
|
+
/// Resolves the promise with a value that has a direct JavaScript representation.
|
|
121
|
+
///
|
|
122
|
+
/// Preferred over the encodable overload for a type that is both ``JavaScriptRepresentable`` and
|
|
123
|
+
/// ``JavaScriptEncodable``, so existing values (primitives, containers, JSI value wrappers) keep
|
|
124
|
+
/// their established representation. For example a 64-bit integer stays a JS `number` here rather
|
|
125
|
+
/// than encoding to a `bigint` or rejecting for exceeding the safe-integer range.
|
|
126
|
+
/// If the resolver call throws, the promise is rejected instead.
|
|
124
127
|
public func resolve<V: JavaScriptRepresentable>(_ value: V) {
|
|
125
128
|
guard let runtime else {
|
|
126
129
|
return
|
|
@@ -132,13 +135,58 @@ public struct JavaScriptPromise: JavaScriptType, ~Copyable {
|
|
|
132
135
|
guard let resolver = longLivedState.resolveFunction.take() else {
|
|
133
136
|
return
|
|
134
137
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
do {
|
|
139
|
+
// Call the actual resolver given in the Promise setup.
|
|
140
|
+
// This will also call `deferredPromise.resolve` in the `then` handler.
|
|
141
|
+
_ = try resolver.getFunction().call(arguments: value)
|
|
138
142
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
// The rejecter can't be called anymore. The state stays registered so it keeps owning the
|
|
144
|
+
// object until the wrapper is dropped (or the teardown sweep runs).
|
|
145
|
+
longLivedState.rejectFunction.release()
|
|
146
|
+
} catch {
|
|
147
|
+
// The resolver call failed; reject with the error instead. The rejecter call itself can
|
|
148
|
+
// realistically only throw when the runtime is being torn down, where dropping the settle
|
|
149
|
+
// is harmless because the JS world is going away.
|
|
150
|
+
let errorValue = JavaScriptError.from(error, in: runtime).toValue()
|
|
151
|
+
_ = try? longLivedState.rejectFunction.take()?.getFunction().call(arguments: errorValue)
|
|
152
|
+
longLivedState.resolveFunction.release()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Resolves the promise with a ``JavaScriptEncodable`` value, encoding it on the JavaScript thread.
|
|
158
|
+
///
|
|
159
|
+
/// Encoding runs where `encode` is isolated to `@JavaScriptActor` and may touch the runtime, so a
|
|
160
|
+
/// caller need not hop there itself. If encoding or the resolver call throws, the promise is
|
|
161
|
+
/// rejected instead.
|
|
162
|
+
///
|
|
163
|
+
/// Disfavored so a type that is both ``JavaScriptRepresentable`` and ``JavaScriptEncodable`` keeps
|
|
164
|
+
/// resolving through the representable overload above; this serves the encodable-only types.
|
|
165
|
+
@_disfavoredOverload
|
|
166
|
+
public func resolve<V: JavaScriptEncodable>(_ value: sending V) {
|
|
167
|
+
guard let runtime else {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
// `resolve` is not isolated, so make sure to jump to JS thread; the encode happens there too.
|
|
171
|
+
runtime.schedule(priority: .immediate) { [longLivedState] in
|
|
172
|
+
// If the promise is already settled, do nothing.
|
|
173
|
+
guard let resolver = longLivedState.resolveFunction.take() else {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
do {
|
|
177
|
+
let encoded = try V.encode(value, in: runtime)
|
|
178
|
+
// Call the actual resolver, which also calls `deferredPromise.resolve` in the `then` handler.
|
|
179
|
+
_ = try resolver.getFunction().call(arguments: encoded)
|
|
180
|
+
// The state stays registered so it keeps owning the object until the wrapper is dropped.
|
|
181
|
+
longLivedState.rejectFunction.release()
|
|
182
|
+
} catch {
|
|
183
|
+
// Encoding or the resolver call failed; reject with the error instead. The rejecter call
|
|
184
|
+
// itself can realistically only throw when the runtime is being torn down, where dropping
|
|
185
|
+
// the settle is harmless because the JS world is going away.
|
|
186
|
+
let errorValue = JavaScriptError.from(error, in: runtime).toValue()
|
|
187
|
+
_ = try? longLivedState.rejectFunction.take()?.getFunction().call(arguments: errorValue)
|
|
188
|
+
longLivedState.resolveFunction.release()
|
|
189
|
+
}
|
|
142
190
|
}
|
|
143
191
|
}
|
|
144
192
|
|
|
@@ -161,7 +209,9 @@ public struct JavaScriptPromise: JavaScriptType, ~Copyable {
|
|
|
161
209
|
|
|
162
210
|
// Call the actual rejecter given in the Promise setup.
|
|
163
211
|
// This will also call `deferredPromise.reject` in the `then` handler.
|
|
164
|
-
|
|
212
|
+
// The call can realistically only throw when the runtime is being torn down, where dropping
|
|
213
|
+
// the settle is harmless because the JS world is going away.
|
|
214
|
+
_ = try? rejecter.getFunction().call(arguments: errorValue)
|
|
165
215
|
|
|
166
216
|
// The resolver can't be called anymore. The state stays registered so it keeps owning the
|
|
167
217
|
// object until the wrapper is dropped (or the teardown sweep runs).
|
|
@@ -201,3 +251,29 @@ public struct JavaScriptPromise: JavaScriptType, ~Copyable {
|
|
|
201
251
|
}
|
|
202
252
|
}
|
|
203
253
|
}
|
|
254
|
+
|
|
255
|
+
// MARK: - Deferred promise factory
|
|
256
|
+
|
|
257
|
+
extension JavaScriptRuntime {
|
|
258
|
+
/// Returns the cached `() => [promise, resolve, reject]` function as a value, creating it on first use.
|
|
259
|
+
/// Evaluated from source so it captures nothing native; it is plain JavaScript the engine can
|
|
260
|
+
/// optimize like any other closure.
|
|
261
|
+
@JavaScriptActor
|
|
262
|
+
fileprivate func deferredPromiseFactory() throws -> JavaScriptValue {
|
|
263
|
+
if let factory = cachedDeferredPromiseFactory {
|
|
264
|
+
return factory
|
|
265
|
+
}
|
|
266
|
+
let factory = try eval(
|
|
267
|
+
label: "expo-modules-jsi/deferred-promise.js",
|
|
268
|
+
"""
|
|
269
|
+
(function () {
|
|
270
|
+
let resolve, reject;
|
|
271
|
+
const promise = new Promise(function (a, b) { resolve = a; reject = b; });
|
|
272
|
+
return [promise, resolve, reject];
|
|
273
|
+
})
|
|
274
|
+
"""
|
|
275
|
+
)
|
|
276
|
+
cachedDeferredPromiseFactory = factory
|
|
277
|
+
return factory
|
|
278
|
+
}
|
|
279
|
+
}
|