expo-modules-jsi 57.0.7 → 57.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/CLAUDE.md +35 -0
- package/apple/Benchmarks/BenchmarkRunner.swift +115 -0
- package/apple/Benchmarks/FunctionCallBenchmarks.swift +45 -0
- package/apple/Benchmarks/HostFunctionBenchmarks.swift +217 -0
- package/apple/Benchmarks/StringConversionBenchmarks.swift +245 -0
- package/apple/Benchmarks/ValueAccessBenchmarks.swift +132 -0
- package/apple/Package.swift +10 -0
- package/apple/Sources/ExpoModulesJSI/Contexts/HostCallbackContext.swift +28 -0
- package/apple/Sources/ExpoModulesJSI/Contexts/HostFunctionContext.swift +18 -6
- package/apple/Sources/ExpoModulesJSI/Contexts/HostObjectContext.swift +7 -3
- package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +20 -13
- package/apple/Sources/ExpoModulesJSI/Protocols/JavaScriptRepresentable.swift +15 -3
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptPropNameID.swift +11 -7
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +80 -58
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptValuesBuffer.swift +7 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +2 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptBigInt.swift +1 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +18 -8
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +2 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +18 -7
- package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +130 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostFunctionClosure.h +6 -3
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObject.h +2 -1
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObjectCallbacks.h +5 -3
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +14 -3
- package/apple/Tests/JavaScriptArrayBufferTests.swift +2 -2
- package/apple/Tests/JavaScriptNativeStateTests.swift +10 -10
- package/apple/Tests/JavaScriptObjectTests.swift +79 -0
- package/apple/Tests/JavaScriptPropNameIDTests.swift +123 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +29 -0
- package/apple/Tests/JavaScriptTypedArrayTests.swift +1 -1
- package/apple/Tests/JavaScriptUnownedValueTests.swift +9 -0
- package/apple/Tests/JavaScriptValueTests.swift +102 -0
- package/apple/Tests/JavaScriptValuesBufferTests.swift +2 -2
- package/apple/Tests/JavaScriptWeakObjectTests.swift +2 -2
- package/apple/scripts/build-xcframework.sh +10 -0
- package/apple/scripts/generate-modulemap.sh +10 -1
- package/package.json +3 -2
|
@@ -178,16 +178,19 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
178
178
|
getPropertyNames: @escaping @JavaScriptActor () -> [String] = { [] },
|
|
179
179
|
dealloc: @escaping @JavaScriptActor () -> Void = {}
|
|
180
180
|
) -> JavaScriptObject {
|
|
181
|
-
func getter(
|
|
182
|
-
|
|
181
|
+
func getter(
|
|
182
|
+
context: UnsafeMutableRawPointer,
|
|
183
|
+
propertyName: UnsafePointer<CChar>,
|
|
184
|
+
resultPtr: UnsafeMutablePointer<facebook.jsi.Value>
|
|
185
|
+
) {
|
|
183
186
|
let propertyName = String(cString: propertyName)
|
|
187
|
+
nonisolated(unsafe) let resultPtr = resultPtr
|
|
184
188
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return try context.get(propertyName).asJSIValue()
|
|
189
|
+
withGuaranteedContext(context) { (context: HostObjectContext, runtime) in
|
|
190
|
+
resultPtr.pointee = JavaScriptActor.assumeIsolated {
|
|
191
|
+
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
192
|
+
return try context.get(propertyName).asJSIValue()
|
|
193
|
+
}
|
|
191
194
|
}
|
|
192
195
|
}
|
|
193
196
|
}
|
|
@@ -195,34 +198,33 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
195
198
|
func setter(
|
|
196
199
|
context: UnsafeMutableRawPointer, propertyName: UnsafePointer<CChar>, valuePointer: UnsafeMutableRawPointer
|
|
197
200
|
) {
|
|
198
|
-
let context = Unmanaged<HostObjectContext>.fromOpaque(context).takeUnretainedValue()
|
|
199
|
-
|
|
200
|
-
guard let runtime = context.runtime else {
|
|
201
|
-
FatalError.runtimeLost()
|
|
202
|
-
}
|
|
203
|
-
guard let set = context.set else {
|
|
204
|
-
// Unreachable in practice: when the user passed `nil` for `set`, the call site
|
|
205
|
-
// below at `expo.HostObjectCallbacks(...)` also passes `nil` to C++, and
|
|
206
|
-
// `HostObjectCallbacks::set` throws a `jsi::JSError` directly instead of
|
|
207
|
-
// calling back into Swift. Trap loudly so a future C++ refactor can't silently
|
|
208
|
-
// swallow assignments.
|
|
209
|
-
FatalError.readOnlyHostObjectSetterInvoked()
|
|
210
|
-
}
|
|
211
|
-
let value = JavaScriptValue(runtime, valuePointer.assumingMemoryBound(to: facebook.jsi.Value.self).move())
|
|
212
201
|
let propertyName = String(cString: propertyName)
|
|
213
202
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
203
|
+
withGuaranteedContext(context) { (context: HostObjectContext, runtime) in
|
|
204
|
+
guard let set = context.set else {
|
|
205
|
+
// Unreachable in practice: when the user passed `nil` for `set`, the call site
|
|
206
|
+
// below at `expo.HostObjectCallbacks(...)` also passes `nil` to C++, and
|
|
207
|
+
// `HostObjectCallbacks::set` throws a `jsi::JSError` directly instead of
|
|
208
|
+
// calling back into Swift. Trap loudly so a future C++ refactor can't silently
|
|
209
|
+
// swallow assignments.
|
|
210
|
+
FatalError.readOnlyHostObjectSetterInvoked()
|
|
211
|
+
}
|
|
212
|
+
let value = JavaScriptValue(runtime, valuePointer.assumingMemoryBound(to: facebook.jsi.Value.self).move())
|
|
213
|
+
|
|
214
|
+
JavaScriptActor.assumeIsolated {
|
|
215
|
+
forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
216
|
+
try set(propertyName, value)
|
|
217
|
+
}
|
|
217
218
|
}
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
func propertyNamesGetter(context: UnsafeMutableRawPointer) -> expo.HostObjectCallbacks.PropNameIds {
|
|
222
223
|
let context = Unmanaged<HostObjectContext>.fromOpaque(context).takeUnretainedValue()
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
// `IRuntime` is an immortal reference, so reading it through the guaranteed wrapper
|
|
225
|
+
// reference costs no reference counting here either.
|
|
226
|
+
let iRuntime = context.runtime._withUnsafeGuaranteedRef { runtime in
|
|
227
|
+
return runtime.pointee
|
|
226
228
|
}
|
|
227
229
|
// Get property names within the actor isolation, but build the vector outside
|
|
228
230
|
// to avoid returning a non-copyable C++ type through `assumeIsolated`
|
|
@@ -235,7 +237,7 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
235
237
|
vector.reserve(propertyNames.count)
|
|
236
238
|
|
|
237
239
|
for propertyName in propertyNames {
|
|
238
|
-
let propNameId = facebook.jsi.PropNameID.forUtf8(
|
|
240
|
+
let propNameId = facebook.jsi.PropNameID.forUtf8(iRuntime, std.string(propertyName))
|
|
239
241
|
vector.push_back(consuming: propNameId)
|
|
240
242
|
}
|
|
241
243
|
return vector
|
|
@@ -646,6 +648,22 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
646
648
|
return result.is("Promise") ? try await result.getPromise().await() : result
|
|
647
649
|
}
|
|
648
650
|
|
|
651
|
+
// MARK: - Garbage collection
|
|
652
|
+
|
|
653
|
+
/// Requests a full, synchronous garbage collection of the JavaScript heap.
|
|
654
|
+
///
|
|
655
|
+
/// Intended for tests and memory diagnostics. The engine collects on its own, so calling this in
|
|
656
|
+
/// production code usually costs more than it saves.
|
|
657
|
+
///
|
|
658
|
+
/// - Note: This is a no-op on engines whose runtime doesn't implement GC instrumentation. JSI's
|
|
659
|
+
/// default implementation does nothing; Hermes overrides it with a real collection.
|
|
660
|
+
/// - Parameter cause: Reason for the collection, as the engine should report it in its logs.
|
|
661
|
+
/// Defaults to the calling function's name.
|
|
662
|
+
@JavaScriptActor
|
|
663
|
+
public func collectGarbage(cause: String = #function) {
|
|
664
|
+
expo.collectGarbage(pointee, std.string(cause))
|
|
665
|
+
}
|
|
666
|
+
|
|
649
667
|
// MARK: - Equatable
|
|
650
668
|
|
|
651
669
|
public static func == (lhs: JavaScriptRuntime, rhs: JavaScriptRuntime) -> Bool {
|
|
@@ -727,15 +745,12 @@ private func createFunctionClosure(
|
|
|
727
745
|
let context = Unmanaged.passRetained(HostFunctionContext(runtime: runtime, name: name, closure)).toOpaque()
|
|
728
746
|
|
|
729
747
|
func call(
|
|
730
|
-
context: UnsafeMutableRawPointer,
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
FatalError.runtimeLost()
|
|
737
|
-
}
|
|
738
|
-
|
|
748
|
+
context: UnsafeMutableRawPointer,
|
|
749
|
+
thisPtr: UnsafePointer<facebook.jsi.Value>,
|
|
750
|
+
argumentsPtr: UnsafePointer<facebook.jsi.Value>,
|
|
751
|
+
argumentsCount: Int,
|
|
752
|
+
resultPtr: UnsafeMutablePointer<facebook.jsi.Value>
|
|
753
|
+
) {
|
|
739
754
|
// `assumeIsolated` runs `operation` synchronously, in this very scope — it never escapes and never
|
|
740
755
|
// hops threads (see `JavaScriptActor.assumeIsolated`). So rather than materializing the move-only
|
|
741
756
|
// `JavaScriptValuesBuffer` out here and smuggling it across the closure boundary through a
|
|
@@ -747,13 +762,18 @@ private func createFunctionClosure(
|
|
|
747
762
|
// floor.
|
|
748
763
|
nonisolated(unsafe) let thisPtr = thisPtr
|
|
749
764
|
nonisolated(unsafe) let argumentsPtr = argumentsPtr
|
|
765
|
+
nonisolated(unsafe) let resultPtr = resultPtr
|
|
750
766
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
767
|
+
// See `withGuaranteedContext` for why neither the context nor the runtime is retained here, and
|
|
768
|
+
// why the result is written to the caller's slot instead of being returned.
|
|
769
|
+
withGuaranteedContext(context) { (context: HostFunctionContext, runtime) in
|
|
770
|
+
resultPtr.pointee = JavaScriptActor.assumeIsolated {
|
|
771
|
+
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
772
|
+
let this = UnsafeMutablePointer(mutating: thisPtr).move()
|
|
773
|
+
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr, count: argumentsCount)
|
|
774
|
+
let thisValue = JavaScriptValue(runtime, this)
|
|
775
|
+
return try context.call(thisValue, consume arguments).asJSIValue()
|
|
776
|
+
}
|
|
757
777
|
}
|
|
758
778
|
}
|
|
759
779
|
}
|
|
@@ -772,15 +792,12 @@ private func createFunctionClosure(
|
|
|
772
792
|
let context = Unmanaged.passRetained(UnownedThisHostFunctionContext(runtime: runtime, name: name, closure)).toOpaque()
|
|
773
793
|
|
|
774
794
|
func call(
|
|
775
|
-
context: UnsafeMutableRawPointer,
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
FatalError.runtimeLost()
|
|
782
|
-
}
|
|
783
|
-
|
|
795
|
+
context: UnsafeMutableRawPointer,
|
|
796
|
+
thisPtr: UnsafePointer<facebook.jsi.Value>,
|
|
797
|
+
argumentsPtr: UnsafePointer<facebook.jsi.Value>,
|
|
798
|
+
argumentsCount: Int,
|
|
799
|
+
resultPtr: UnsafeMutablePointer<facebook.jsi.Value>
|
|
800
|
+
) {
|
|
784
801
|
// Same call-scoped reasoning as the owning-`this` overload above (see its comment) for why the
|
|
785
802
|
// buffer is built inside the synchronous `assumeIsolated` closure. Here `this` is additionally
|
|
786
803
|
// handed in as a borrowed `JavaScriptUnownedValue` pointing straight at the C++-owned `this` slot:
|
|
@@ -788,12 +805,17 @@ private func createFunctionClosure(
|
|
|
788
805
|
// per-call `weak`-runtime form/destroy and heap object that the owning `this` pays.
|
|
789
806
|
nonisolated(unsafe) let thisPtr = thisPtr
|
|
790
807
|
nonisolated(unsafe) let argumentsPtr = argumentsPtr
|
|
808
|
+
nonisolated(unsafe) let resultPtr = resultPtr
|
|
791
809
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
return
|
|
810
|
+
// See `withGuaranteedContext` for why neither the context nor the runtime is retained here, and
|
|
811
|
+
// why the result is written to the caller's slot instead of being returned.
|
|
812
|
+
withGuaranteedContext(context) { (context: UnownedThisHostFunctionContext, runtime) in
|
|
813
|
+
resultPtr.pointee = JavaScriptActor.assumeIsolated {
|
|
814
|
+
return forwardingSwiftErrorsToJS(runtime: runtime) {
|
|
815
|
+
let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr, count: argumentsCount)
|
|
816
|
+
let thisValue = JavaScriptUnownedValue(runtime.pointee, thisPtr)
|
|
817
|
+
return try context.call(thisValue, consume arguments).asJSIValue()
|
|
818
|
+
}
|
|
797
819
|
}
|
|
798
820
|
}
|
|
799
821
|
}
|
|
@@ -6,7 +6,12 @@ internal import jsi
|
|
|
6
6
|
public struct JavaScriptValuesBuffer: JavaScriptType, ~Copyable {
|
|
7
7
|
// Safe to use unowned — the buffer's lifetime is scoped to a host function call,
|
|
8
8
|
// so the runtime is always alive while the buffer exists.
|
|
9
|
-
|
|
9
|
+
//
|
|
10
|
+
// `unowned(unsafe)` rather than `unowned`: the runtime object carries a refcount side table (other
|
|
11
|
+
// wrappers hold it `weak`), so a safe `unowned` retain/release on it takes the slow side-table path
|
|
12
|
+
// on every buffer construction and destruction. Profiling showed that pair as roughly a third of the
|
|
13
|
+
// native-side cost of a no-op `@JS` host call. `unowned(unsafe)` is a plain pointer store/load.
|
|
14
|
+
internal unowned(unsafe) let runtime: JavaScriptRuntime
|
|
10
15
|
|
|
11
16
|
// The raw `facebook.jsi.IRuntime`, cached alongside the `JavaScriptRuntime` wrapper. `IRuntime` is
|
|
12
17
|
// an immortal reference (`jsi.apinotes`), so reading it costs no ARC, whereas reading `.pointee`
|
|
@@ -65,7 +70,7 @@ public struct JavaScriptValuesBuffer: JavaScriptType, ~Copyable {
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
public subscript(index: Int) -> JavaScriptValue {
|
|
68
|
-
return JavaScriptValue(runtime, bufferPointer[index])
|
|
73
|
+
return JavaScriptValue(runtime, copying: bufferPointer[index])
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
/// Returns a non-owning, non-copyable value borrowing the element at `index` for the zero-copy decode
|
|
@@ -404,7 +404,7 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
|
|
|
404
404
|
guard let runtime else {
|
|
405
405
|
FatalError.runtimeLost()
|
|
406
406
|
}
|
|
407
|
-
let jsiValue = expo.getProperty(runtime.pointee, pointee, .
|
|
407
|
+
let jsiValue = expo.getProperty(runtime.pointee, pointee, key.toJSIPropNameID(in: runtime.pointee))
|
|
408
408
|
return JavaScriptValue(runtime, jsiValue)
|
|
409
409
|
}
|
|
410
410
|
nonmutating set(newValue) {
|
|
@@ -412,7 +412,7 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
|
|
|
412
412
|
FatalError.runtimeLost()
|
|
413
413
|
}
|
|
414
414
|
let jsiValue = newValue.toJSIValue(in: runtime.pointee)
|
|
415
|
-
expo.setProperty(runtime.pointee, pointee, key, jsiValue)
|
|
415
|
+
expo.setProperty(runtime.pointee, pointee, key.toJSIPropNameID(in: runtime.pointee), jsiValue)
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
|
|
@@ -231,8 +231,7 @@ public struct JavaScriptBigInt: JavaScriptType, Sendable, ~Copyable {
|
|
|
231
231
|
guard radix >= 2 && radix <= 36 else {
|
|
232
232
|
throw BigIntConversionError.invalidRadix(radix)
|
|
233
233
|
}
|
|
234
|
-
|
|
235
|
-
return String(jsiString.utf16(runtime.pointee))
|
|
234
|
+
return String(jsiString: pointee.toString(runtime.pointee, Int32(radix)), in: runtime.pointee)
|
|
236
235
|
}
|
|
237
236
|
|
|
238
237
|
// MARK: - JavaScriptType
|
|
@@ -105,7 +105,7 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
105
105
|
guard let runtime else {
|
|
106
106
|
FatalError.runtimeLost()
|
|
107
107
|
}
|
|
108
|
-
return pointee.hasProperty(runtime.pointee, name)
|
|
108
|
+
return pointee.hasProperty(runtime.pointee, name.toJSIPropNameID(in: runtime.pointee))
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/// Returns the property of the object with the given name,
|
|
@@ -114,7 +114,7 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
114
114
|
guard let runtime else {
|
|
115
115
|
FatalError.runtimeLost()
|
|
116
116
|
}
|
|
117
|
-
return JavaScriptValue(runtime, pointee.getProperty(runtime.pointee, name))
|
|
117
|
+
return JavaScriptValue(runtime, pointee.getProperty(runtime.pointee, name.toJSIPropNameID(in: runtime.pointee)))
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/// Returns the property of the object with the given prop name id,
|
|
@@ -163,7 +163,7 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
163
163
|
let count = propertyNames.size(jsiRuntime)
|
|
164
164
|
|
|
165
165
|
return (0..<count).map { i in
|
|
166
|
-
return String(propertyNames.getValueAtIndex(jsiRuntime, i).getString(jsiRuntime)
|
|
166
|
+
return String(jsiString: propertyNames.getValueAtIndex(jsiRuntime, i).getString(jsiRuntime), in: jsiRuntime)
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -230,7 +230,12 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
230
230
|
FatalError.runtimeLost()
|
|
231
231
|
}
|
|
232
232
|
// This specialization is to avoid copying the value; `asValue()` on `JavaScriptValue` needs to do a copy.
|
|
233
|
-
expo.setProperty(
|
|
233
|
+
expo.setProperty(
|
|
234
|
+
runtime.pointee,
|
|
235
|
+
pointee,
|
|
236
|
+
name.toJSIPropNameID(in: runtime.pointee),
|
|
237
|
+
value.toJSIValue(in: runtime.pointee)
|
|
238
|
+
)
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
public func setProperty<T: JavaScriptRepresentable & ~Copyable>(_ name: String, value: consuming T) {
|
|
@@ -238,7 +243,7 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
238
243
|
FatalError.runtimeLost()
|
|
239
244
|
}
|
|
240
245
|
let jsiValue = value.toJavaScriptValue(in: runtime).toJSIValue(in: runtime.pointee)
|
|
241
|
-
expo.setProperty(runtime.pointee, pointee, name, jsiValue)
|
|
246
|
+
expo.setProperty(runtime.pointee, pointee, name.toJSIPropNameID(in: runtime.pointee), jsiValue)
|
|
242
247
|
}
|
|
243
248
|
|
|
244
249
|
internal func setProperty<T: JavaScriptRepresentable>(_ name: String, value: consuming T) where T: JSIRepresentable {
|
|
@@ -246,14 +251,19 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
246
251
|
FatalError.runtimeLost()
|
|
247
252
|
}
|
|
248
253
|
let jsiValue = value.toJSIValue(in: runtime.pointee)
|
|
249
|
-
expo.setProperty(runtime.pointee, pointee, name, jsiValue)
|
|
254
|
+
expo.setProperty(runtime.pointee, pointee, name.toJSIPropNameID(in: runtime.pointee), jsiValue)
|
|
250
255
|
}
|
|
251
256
|
|
|
252
257
|
public func setProperty(_ name: String, _ object: consuming JavaScriptObject) {
|
|
253
258
|
guard let runtime else {
|
|
254
259
|
FatalError.runtimeLost()
|
|
255
260
|
}
|
|
256
|
-
expo.setProperty(
|
|
261
|
+
expo.setProperty(
|
|
262
|
+
runtime.pointee,
|
|
263
|
+
pointee,
|
|
264
|
+
name.toJSIPropNameID(in: runtime.pointee),
|
|
265
|
+
facebook.jsi.Value(runtime.pointee, object.pointee)
|
|
266
|
+
)
|
|
257
267
|
}
|
|
258
268
|
|
|
259
269
|
/// Sets a property to a synchronous host function created from the given closure. The function
|
|
@@ -307,7 +317,7 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
|
|
|
307
317
|
guard let runtime else {
|
|
308
318
|
FatalError.runtimeLost()
|
|
309
319
|
}
|
|
310
|
-
pointee.deleteProperty(runtime.pointee, name)
|
|
320
|
+
pointee.deleteProperty(runtime.pointee, name.toJSIPropNameID(in: runtime.pointee))
|
|
311
321
|
}
|
|
312
322
|
#endif
|
|
313
323
|
|
|
@@ -48,7 +48,7 @@ public struct JavaScriptUnownedValue: ~Copyable {
|
|
|
48
48
|
assert(
|
|
49
49
|
Unmanaged.passUnretained(runtime.pointee).toOpaque() == Unmanaged.passUnretained(self.runtime).toOpaque(),
|
|
50
50
|
"`copied(in:)` must be passed the runtime that owns the borrowed value")
|
|
51
|
-
return JavaScriptValue(runtime, pointer.pointee)
|
|
51
|
+
return JavaScriptValue(runtime, copying: pointer.pointee)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// MARK: - Type checks
|
|
@@ -108,7 +108,7 @@ public struct JavaScriptUnownedValue: ~Copyable {
|
|
|
108
108
|
/// Returns the value as a string, or asserts if not a string.
|
|
109
109
|
public func getString() -> String {
|
|
110
110
|
assert(isString(), "Value is not a string")
|
|
111
|
-
return String(pointer.pointee.getString(runtime)
|
|
111
|
+
return String(jsiString: pointer.pointee.getString(runtime), in: runtime)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/// Returns the value as a ``JavaScriptObject`` *without* materializing an owning ``JavaScriptValue``
|
|
@@ -10,14 +10,26 @@ public final class JavaScriptValue: JavaScriptType, Equatable, Escapable {
|
|
|
10
10
|
internal weak let runtime: JavaScriptRuntime?
|
|
11
11
|
internal let pointee: facebook.jsi.Value
|
|
12
12
|
|
|
13
|
-
///
|
|
13
|
+
/// Takes ownership of the given JSI value. Only runtime-free values (undefined, null, booleans and
|
|
14
|
+
/// numbers) pass `nil` here.
|
|
14
15
|
internal init(_ runtime: JavaScriptRuntime?, _ pointee: consuming facebook.jsi.Value) {
|
|
15
16
|
self.runtime = runtime
|
|
16
17
|
self.pointee = pointee
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
///
|
|
20
|
-
|
|
20
|
+
/// Takes ownership of the given JSI value. Same as the optional-runtime overload, but the exact
|
|
21
|
+
/// parameter type keeps the caller from promoting its runtime to an optional, which costs a
|
|
22
|
+
/// retain/release pair around every call on the hot paths (property reads, function results).
|
|
23
|
+
internal init(_ runtime: JavaScriptRuntime, _ pointee: consuming facebook.jsi.Value) {
|
|
24
|
+
self.runtime = runtime
|
|
25
|
+
self.pointee = pointee
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Copies the given JSI value, which asks the engine to clone the handle for pointer values.
|
|
29
|
+
/// The `copying:` label keeps this initializer from being picked for temporaries: with a plain
|
|
30
|
+
/// second argument, a non-optional runtime used to win overload resolution over the consuming
|
|
31
|
+
/// initializer above, cloning every freshly returned value instead of taking it over.
|
|
32
|
+
internal init(_ runtime: JavaScriptRuntime, copying pointee: borrowing facebook.jsi.Value) {
|
|
21
33
|
self.runtime = runtime
|
|
22
34
|
self.pointee = facebook.jsi.Value(runtime.pointee, pointee)
|
|
23
35
|
}
|
|
@@ -31,8 +43,7 @@ public final class JavaScriptValue: JavaScriptType, Equatable, Escapable {
|
|
|
31
43
|
/// Creates a string JS value.
|
|
32
44
|
public init(_ runtime: JavaScriptRuntime, _ string: String) {
|
|
33
45
|
self.runtime = runtime
|
|
34
|
-
self.pointee =
|
|
35
|
-
runtime.pointee, facebook.jsi.String.createFromUtf8(runtime.pointee, std.string(string)))
|
|
46
|
+
self.pointee = string.toJSIValue(in: runtime.pointee)
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
/// Creates a BigInt JS value from an Int64.
|
|
@@ -251,7 +262,7 @@ public final class JavaScriptValue: JavaScriptType, Equatable, Escapable {
|
|
|
251
262
|
FatalError.runtimeLost()
|
|
252
263
|
}
|
|
253
264
|
assert(isString(), "Value is not a string")
|
|
254
|
-
return String(pointee.getString(jsiRuntime)
|
|
265
|
+
return String(jsiString: pointee.getString(jsiRuntime), in: jsiRuntime)
|
|
255
266
|
}
|
|
256
267
|
|
|
257
268
|
/// Returns the value as a BigInt, or asserts if not a BigInt.
|
|
@@ -418,7 +429,7 @@ public final class JavaScriptValue: JavaScriptType, Equatable, Escapable {
|
|
|
418
429
|
guard let jsiRuntime = runtime?.pointee else {
|
|
419
430
|
FatalError.runtimeLost()
|
|
420
431
|
}
|
|
421
|
-
return String(pointee.toString(jsiRuntime)
|
|
432
|
+
return String(jsiString: pointee.toString(jsiRuntime), in: jsiRuntime)
|
|
422
433
|
}
|
|
423
434
|
|
|
424
435
|
/// Converts the JavaScript value to a JSON string representation.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
internal import ExpoModulesJSI_Cxx
|
|
4
|
+
import Foundation
|
|
5
|
+
internal import jsi
|
|
6
|
+
|
|
7
|
+
extension String {
|
|
8
|
+
/// Builds a Swift `String` from a `jsi::String` by reading the engine's internal representation
|
|
9
|
+
/// through `getStringData`, without materializing a `std::string` first.
|
|
10
|
+
///
|
|
11
|
+
/// Hermes stores strings as either ASCII bytes or UTF-16 code units and hands over whichever it
|
|
12
|
+
/// has, chunk by chunk (only rope strings arrive in several chunks). ASCII bytes are valid UTF-8 and
|
|
13
|
+
/// are copied as-is with no validation. UTF-16 is transcoded to UTF-8 on the Swift side, which is
|
|
14
|
+
/// several times cheaper than letting the engine produce UTF-8 through `utf8()` and validating it
|
|
15
|
+
/// again in Swift. Measured against `utf8()`: 12-byte ASCII unchanged, 4 KB ASCII 3.7x faster,
|
|
16
|
+
/// 4 KB non-ASCII 2.4x faster, 12-byte non-ASCII unchanged.
|
|
17
|
+
internal init(jsiString: borrowing facebook.jsi.String, in runtime: facebook.jsi.IRuntime) {
|
|
18
|
+
var result = ""
|
|
19
|
+
withUnsafeMutablePointer(to: &result) { resultPtr in
|
|
20
|
+
runtime.getStringData(jsiString, UnsafeMutableRawPointer(resultPtr), appendEngineStringChunk)
|
|
21
|
+
}
|
|
22
|
+
self = result
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Builds a `jsi::PropNameID` from the string's UTF-8 bytes. This is how every String-keyed
|
|
26
|
+
/// property API turns its name into a key: JSI's `const char*` overloads treat the bytes as ASCII
|
|
27
|
+
/// and would mangle any non-ASCII name.
|
|
28
|
+
internal func toJSIPropNameID(in runtime: facebook.jsi.IRuntime) -> facebook.jsi.PropNameID {
|
|
29
|
+
// The `(pointer, length)` overload needs the UTF-8 *byte* count, which `withUTF8` provides
|
|
30
|
+
// exactly; `String.count` would be wrong here because it counts grapheme clusters (e.g. `"café"`
|
|
31
|
+
// reports 4 for 5 UTF-8 bytes). `withUTF8` is mutating (it makes a bridged string contiguous
|
|
32
|
+
// first), hence the local copy, and the result leaves the closure through an optional because
|
|
33
|
+
// it has to be a `Copyable` type.
|
|
34
|
+
var string = self
|
|
35
|
+
var propNameID: facebook.jsi.PropNameID? = nil
|
|
36
|
+
string.withUTF8 { utf8 in
|
|
37
|
+
guard let base = utf8.baseAddress else {
|
|
38
|
+
propNameID = facebook.jsi.PropNameID.forAscii(runtime, "", 0)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
propNameID = facebook.jsi.PropNameID.forUtf8(runtime, base, utf8.count)
|
|
42
|
+
}
|
|
43
|
+
return propNameID.take()!
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Callback for `getStringData`. `ctx` points at the Swift `String` being built; each call appends
|
|
48
|
+
/// one chunk of the engine's internal representation to it.
|
|
49
|
+
private func appendEngineStringChunk(ctx: UnsafeMutableRawPointer?, ascii: Bool, data: UnsafeRawPointer?, count: Int) {
|
|
50
|
+
guard count > 0, let data else {
|
|
51
|
+
// An empty chunk adds nothing to the result. Guarding here also keeps the pointer
|
|
52
|
+
// arithmetic below free of optionals on both paths.
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
let out = ctx!.assumingMemoryBound(to: String.self)
|
|
56
|
+
let chunk: String
|
|
57
|
+
if ascii {
|
|
58
|
+
let bytes = UnsafeBufferPointer(start: data.assumingMemoryBound(to: UInt8.self), count: count)
|
|
59
|
+
chunk = String(unsafeUninitializedCapacity: count) { buffer in
|
|
60
|
+
return buffer.initialize(fromContentsOf: bytes)
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
let units = UnsafeBufferPointer(start: data.assumingMemoryBound(to: UInt16.self), count: count)
|
|
64
|
+
if count < 512 {
|
|
65
|
+
// Short strings: transcode straight into the string's UTF-8 storage, sized for the worst case
|
|
66
|
+
// of 3 UTF-8 bytes per UTF-16 code unit (a surrogate pair is 4 bytes for 2 units). This is
|
|
67
|
+
// 1.5-1.6x faster than `String(decoding:as:)` up to a few hundred code units, which carries a
|
|
68
|
+
// fixed setup cost of a few hundred nanoseconds.
|
|
69
|
+
chunk = String(unsafeUninitializedCapacity: count * 3) { buffer in
|
|
70
|
+
return transcodeUTF16(units, into: buffer)
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
// Long strings: the loop and the standard library's bulk decoder run at the same speed here,
|
|
74
|
+
// but the bulk decoder sizes the storage exactly, while the string above keeps its 3x worst-case
|
|
75
|
+
// capacity. That matters once strings are kilobytes rather than bytes.
|
|
76
|
+
chunk = String(decoding: units, as: UTF16.self)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if out.pointee.isEmpty {
|
|
80
|
+
out.pointee = chunk
|
|
81
|
+
} else {
|
|
82
|
+
out.pointee += chunk
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Writes the UTF-8 encoding of `units` into `buffer` and returns the number of bytes written.
|
|
87
|
+
/// Unpaired surrogates become U+FFFD, one per code unit, which is what `String(decoding:as:)` and
|
|
88
|
+
/// `transcode(stoppingOnError: false)` produce. It replaces the standard library's `transcode`, which
|
|
89
|
+
/// calls back once per output byte; this loop writes each scalar's bytes directly.
|
|
90
|
+
/// `buffer` must hold at least 3 bytes per code unit.
|
|
91
|
+
private func transcodeUTF16(
|
|
92
|
+
_ units: UnsafeBufferPointer<UInt16>,
|
|
93
|
+
into buffer: UnsafeMutableBufferPointer<UInt8>
|
|
94
|
+
) -> Int {
|
|
95
|
+
var index = 0
|
|
96
|
+
var written = 0
|
|
97
|
+
while index < units.count {
|
|
98
|
+
let unit = UInt32(units[index])
|
|
99
|
+
index += 1
|
|
100
|
+
let scalar: UInt32
|
|
101
|
+
if unit < 0xD800 || unit > 0xDFFF {
|
|
102
|
+
scalar = unit
|
|
103
|
+
} else if unit < 0xDC00, index < units.count, (0xDC00...0xDFFF).contains(units[index]) {
|
|
104
|
+
scalar = 0x10000 + ((unit - 0xD800) << 10) + (UInt32(units[index]) - 0xDC00)
|
|
105
|
+
index += 1
|
|
106
|
+
} else {
|
|
107
|
+
scalar = 0xFFFD
|
|
108
|
+
}
|
|
109
|
+
if scalar < 0x80 {
|
|
110
|
+
buffer[written] = UInt8(scalar)
|
|
111
|
+
written += 1
|
|
112
|
+
} else if scalar < 0x800 {
|
|
113
|
+
buffer[written] = UInt8(0xC0 | (scalar >> 6))
|
|
114
|
+
buffer[written + 1] = UInt8(0x80 | (scalar & 0x3F))
|
|
115
|
+
written += 2
|
|
116
|
+
} else if scalar < 0x10000 {
|
|
117
|
+
buffer[written] = UInt8(0xE0 | (scalar >> 12))
|
|
118
|
+
buffer[written + 1] = UInt8(0x80 | ((scalar >> 6) & 0x3F))
|
|
119
|
+
buffer[written + 2] = UInt8(0x80 | (scalar & 0x3F))
|
|
120
|
+
written += 3
|
|
121
|
+
} else {
|
|
122
|
+
buffer[written] = UInt8(0xF0 | (scalar >> 18))
|
|
123
|
+
buffer[written + 1] = UInt8(0x80 | ((scalar >> 12) & 0x3F))
|
|
124
|
+
buffer[written + 2] = UInt8(0x80 | ((scalar >> 6) & 0x3F))
|
|
125
|
+
buffer[written + 3] = UInt8(0x80 | (scalar & 0x3F))
|
|
126
|
+
written += 4
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return written
|
|
130
|
+
}
|
|
@@ -12,7 +12,10 @@ namespace expo {
|
|
|
12
12
|
*/
|
|
13
13
|
class HostFunctionClosure final : public RetainedSwiftPointer {
|
|
14
14
|
public:
|
|
15
|
-
|
|
15
|
+
// The result travels through an out-parameter instead of a return value: the Swift side can then
|
|
16
|
+
// write it straight into the caller's slot from inside its guaranteed-reference scope, with no
|
|
17
|
+
// placeholder value to construct and no extra move of the `jsi::Value`.
|
|
18
|
+
using Closure = void(Context context, const facebook::jsi::Value *_Nonnull thisValue, const facebook::jsi::Value *_Nonnull args, size_t count, facebook::jsi::Value *_Nonnull result);
|
|
16
19
|
|
|
17
20
|
explicit HostFunctionClosure(Context context, Closure closure, Deallocator deallocator) : RetainedSwiftPointer(context, deallocator), _closure(closure) {};
|
|
18
21
|
|
|
@@ -23,8 +26,8 @@ public:
|
|
|
23
26
|
/**
|
|
24
27
|
Calls the Swift closure with given `this` value and arguments.
|
|
25
28
|
*/
|
|
26
|
-
inline
|
|
27
|
-
|
|
29
|
+
inline void call(const facebook::jsi::Value &thisValue, const facebook::jsi::Value *_Nonnull args, size_t count, facebook::jsi::Value &result) const {
|
|
30
|
+
_closure(_context, &thisValue, args, count, &result);
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
private:
|
|
@@ -23,7 +23,8 @@ public:
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
inline jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override {
|
|
26
|
-
|
|
26
|
+
jsi::Value result;
|
|
27
|
+
_callbacks.get(name.utf8(runtime).c_str(), result);
|
|
27
28
|
// If the Swift getter stored a pending error, rethrow its JSError directly
|
|
28
29
|
// to preserve all properties (message, code, stack, etc.).
|
|
29
30
|
if (auto *error = CppError::getCurrent()) {
|
|
@@ -16,7 +16,9 @@ class HostObjectCallbacks final {
|
|
|
16
16
|
public:
|
|
17
17
|
using Context = void *_Nonnull;
|
|
18
18
|
using PropNameIds = std::vector<facebook::jsi::PropNameID>;
|
|
19
|
-
|
|
19
|
+
// The getter writes its result through an out-parameter, like `HostFunctionClosure`, so the
|
|
20
|
+
// Swift side can fill the caller's slot from inside its guaranteed-reference scope.
|
|
21
|
+
using Getter = void(Context, const char *_Nonnull name, facebook::jsi::Value *_Nonnull result);
|
|
20
22
|
using Setter = void(Context, const char *_Nonnull name, void *_Nonnull value);
|
|
21
23
|
using PropertyNamesGetter = PropNameIds(Context);
|
|
22
24
|
using Deallocator = void(Context);
|
|
@@ -24,8 +26,8 @@ public:
|
|
|
24
26
|
explicit HostObjectCallbacks(Context context, Getter getter, Setter *_Nullable setter, PropertyNamesGetter propertyNamesGetter, Deallocator deallocator)
|
|
25
27
|
: _context(context), _getter(getter), _setter(setter), _propertyNamesGetter(propertyNamesGetter), _deallocator(deallocator) {}
|
|
26
28
|
|
|
27
|
-
inline
|
|
28
|
-
|
|
29
|
+
inline void get(const char *_Nonnull name, facebook::jsi::Value &result) const {
|
|
30
|
+
_getter(_context, name, &result);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
#include <TargetConditionals.h>
|
|
7
7
|
|
|
8
|
+
// `jsi.h` only forward-declares `jsi::Instrumentation`.
|
|
9
|
+
#include <jsi/instrumentation.h>
|
|
10
|
+
|
|
8
11
|
#include "HostFunctionClosure.h"
|
|
9
12
|
#include "CppError.h"
|
|
10
13
|
#include "IRuntimeCompat.h"
|
|
@@ -36,11 +39,13 @@ inline jsi::Value valueFromFunction(jsi::IRuntime &runtime, const jsi::Function
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
// `jsi::Object::setProperty` is a template function that Swift does not support. We need to provide specialized versions.
|
|
39
|
-
|
|
42
|
+
// They take a `jsi::PropNameID` rather than a `const char *`: JSI treats a `const char *` name as ASCII,
|
|
43
|
+
// which mangles non-ASCII property names coming from Swift strings.
|
|
44
|
+
inline void setProperty(jsi::IRuntime &runtime, const jsi::Object &object, const jsi::PropNameID &name, const jsi::Value &value) {
|
|
40
45
|
object.setProperty(runtime, name, value);
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
inline void setProperty(jsi::IRuntime &runtime, const jsi::Array &array, const
|
|
48
|
+
inline void setProperty(jsi::IRuntime &runtime, const jsi::Array &array, const jsi::PropNameID &name, const jsi::Value &value) {
|
|
44
49
|
array.setProperty(runtime, name, value);
|
|
45
50
|
}
|
|
46
51
|
|
|
@@ -97,7 +102,8 @@ inline std::shared_ptr<const jsi::Buffer> makeSharedStringBuffer(const std::stri
|
|
|
97
102
|
inline jsi::Function createHostFunction(jsi::IRuntime &runtime, const jsi::PropNameID &propName, HostFunctionClosure *closure) {
|
|
98
103
|
auto closurePtr = std::shared_ptr<HostFunctionClosure>(closure);
|
|
99
104
|
return jsi::Function::createFromHostFunction(runtime, propName, 0, [closurePtr](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *_Nonnull args, size_t count) -> jsi::Value {
|
|
100
|
-
|
|
105
|
+
jsi::Value result;
|
|
106
|
+
closurePtr->call(thisValue, args, count, result);
|
|
101
107
|
|
|
102
108
|
// If the Swift closure stored a pending error, rethrow its JSError directly
|
|
103
109
|
// to preserve all properties (message, code, stack, etc.).
|
|
@@ -141,6 +147,11 @@ inline jsi::Value evaluateJavaScript(jsi::IRuntime &runtime, const std::shared_p
|
|
|
141
147
|
});
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
// `jsi::Instrumentation` is not imported into Swift, so reach it from here.
|
|
151
|
+
inline void collectGarbage(jsi::IRuntime &runtime, const std::string &cause) {
|
|
152
|
+
runtime.instrumentation().collectGarbage(cause);
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
inline jsi::Value callFunction(jsi::IRuntime &runtime, const jsi::Function &function, const jsi::Value *_Nullable args, size_t count) {
|
|
145
156
|
return expo::CppError::tryCatch(runtime, [&] {
|
|
146
157
|
return function.call(runtime, args, count);
|