expo-modules-jsi 56.0.4 → 56.0.5
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
CHANGED
|
@@ -43,6 +43,17 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable {
|
|
|
43
43
|
internal let runtimePointee: facebook.jsi.Runtime
|
|
44
44
|
internal let scheduler: expo.RuntimeScheduler
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
Thread ID of the JavaScript thread, captured at construction time. Used by `isOnJavaScriptThread()`
|
|
48
|
+
for a fast integer comparison instead of `Thread.current.name == "..."`.
|
|
49
|
+
Assumes runtime initializers always run on the JS thread.
|
|
50
|
+
*/
|
|
51
|
+
private let jsThreadID: UInt64 = {
|
|
52
|
+
var id: UInt64 = 0
|
|
53
|
+
pthread_threadid_np(nil, &id)
|
|
54
|
+
return id
|
|
55
|
+
}()
|
|
56
|
+
|
|
46
57
|
/**
|
|
47
58
|
Actor for running runtime work.
|
|
48
59
|
*/
|
|
@@ -362,7 +373,7 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable {
|
|
|
362
373
|
let argumentsRef = arguments.copy().ref()
|
|
363
374
|
|
|
364
375
|
// Switch to asynchronous context.
|
|
365
|
-
self.schedule
|
|
376
|
+
self.schedule {
|
|
366
377
|
// Invoke the asynchronous function and resolve/reject the promise.
|
|
367
378
|
do {
|
|
368
379
|
let result = try await function(this, argumentsRef.take())
|
|
@@ -399,11 +410,10 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable {
|
|
|
399
410
|
|
|
400
411
|
public func schedule(
|
|
401
412
|
priority: SchedulerPriority = .normal,
|
|
402
|
-
taskName: String? = "[JS] runtime.schedule (\(#function))",
|
|
403
413
|
@_implicitSelfCapture _ closure: @escaping @JavaScriptActor () async throws -> Void
|
|
404
414
|
) -> Void {
|
|
405
415
|
schedule(priority: priority) {
|
|
406
|
-
Task.immediate_polyfill
|
|
416
|
+
Task.immediate_polyfill {
|
|
407
417
|
try await closure()
|
|
408
418
|
}
|
|
409
419
|
}
|
|
@@ -414,8 +424,14 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable {
|
|
|
414
424
|
Not available in async contexts to prevent blocking the cooperative thread pool.
|
|
415
425
|
*/
|
|
416
426
|
@available(*, noasync)
|
|
427
|
+
@discardableResult
|
|
417
428
|
public func execute<R: Sendable>(@_implicitSelfCapture _ closure: @escaping @JavaScriptActor () throws -> R) throws -> sending R {
|
|
429
|
+
if isOnJavaScriptThread() {
|
|
430
|
+
return try JavaScriptActor.assumeIsolated(closure)
|
|
431
|
+
}
|
|
432
|
+
|
|
418
433
|
var result: Result<R, any Error>!
|
|
434
|
+
nonisolated(unsafe) let callerRunLoop = CFRunLoopGetCurrent()
|
|
419
435
|
|
|
420
436
|
scheduler.scheduleTask(.ImmediatePriority) {
|
|
421
437
|
do {
|
|
@@ -423,12 +439,23 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable {
|
|
|
423
439
|
} catch {
|
|
424
440
|
result = .failure(error)
|
|
425
441
|
}
|
|
442
|
+
// Wake the caller's run loop so its `CFRunLoopRunInMode(...)` returns immediately
|
|
443
|
+
// instead of waiting out the timeout backstop.
|
|
444
|
+
CFRunLoopPerformBlock(callerRunLoop, CFRunLoopMode.commonModes.rawValue) {}
|
|
445
|
+
CFRunLoopWakeUp(callerRunLoop)
|
|
426
446
|
}
|
|
427
447
|
|
|
428
|
-
//
|
|
429
|
-
// this
|
|
448
|
+
// Pump the caller's run loop until the task finishes. As opposed to DispatchSemaphore
|
|
449
|
+
// or DispatchGroup, this lets the run loop continue to process other events in the meantime,
|
|
450
|
+
// and the spin is also faster than a real kernel-mediated context switch when the JS work
|
|
451
|
+
// is short (the common case). The 100ms timeout is a backstop in case the wakeup is missed;
|
|
452
|
+
// the common path is woken by `CFRunLoopWakeUp` from the scheduled block above.
|
|
453
|
+
//
|
|
454
|
+
// `CFRunLoopRunInMode` is the C API rather than `RunLoop.current.run(mode:before:)` to
|
|
455
|
+
// avoid the per-iteration `+[NSRunLoop currentRunLoop]` autorelease push and `Date()`
|
|
456
|
+
// allocation that dominated the caller-thread profile otherwise.
|
|
430
457
|
while result == nil {
|
|
431
|
-
|
|
458
|
+
CFRunLoopRunInMode(.commonModes, 0.1, false)
|
|
432
459
|
}
|
|
433
460
|
return try result.get()
|
|
434
461
|
}
|
|
@@ -438,26 +465,40 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable {
|
|
|
438
465
|
Not available in async contexts to prevent blocking the cooperative thread pool.
|
|
439
466
|
*/
|
|
440
467
|
@available(*, noasync)
|
|
468
|
+
@discardableResult
|
|
441
469
|
public func execute<R: Sendable>(
|
|
442
|
-
taskName: String? = "[JS] runtime.execute (\(#function))",
|
|
443
470
|
@_implicitSelfCapture _ closure: @escaping @JavaScriptActor () async throws -> R
|
|
444
471
|
) throws -> sending R {
|
|
445
472
|
let result = NonisolatedUnsafeVar<Result<R, any Error>>()
|
|
473
|
+
let runInline = isOnJavaScriptThread()
|
|
474
|
+
// Wrapped in `NonisolatedUnsafeVar` instead of `nonisolated(unsafe) let`
|
|
475
|
+
// to work around a Swift 6.2.3 compiler bug.
|
|
476
|
+
let callerRunLoop = NonisolatedUnsafeVar(CFRunLoopGetCurrent())
|
|
446
477
|
|
|
447
|
-
|
|
448
|
-
Task.immediate_polyfill(
|
|
478
|
+
func body() -> Void {
|
|
479
|
+
Task.immediate_polyfill(priority: .high) {
|
|
449
480
|
do {
|
|
450
481
|
result.value = .success(try await closure())
|
|
451
482
|
} catch {
|
|
452
483
|
result.value = .failure(error)
|
|
453
484
|
}
|
|
485
|
+
// Wake the caller's run loop so its `CFRunLoopRunInMode(...)` returns immediately
|
|
486
|
+
// instead of waiting out the timeout backstop.
|
|
487
|
+
CFRunLoopPerformBlock(callerRunLoop.value, CFRunLoopMode.commonModes.rawValue) {}
|
|
488
|
+
CFRunLoopWakeUp(callerRunLoop.value)
|
|
454
489
|
}
|
|
455
490
|
}
|
|
491
|
+
if runInline {
|
|
492
|
+
body()
|
|
493
|
+
} else {
|
|
494
|
+
scheduler.scheduleTask(.ImmediatePriority, body)
|
|
495
|
+
}
|
|
456
496
|
|
|
457
|
-
//
|
|
458
|
-
//
|
|
497
|
+
// Pump the caller's run loop until the task finishes. See the sync overload above for
|
|
498
|
+
// the rationale on `CFRunLoopRunInMode` vs. `RunLoop.current.run(...)` and on pumping
|
|
499
|
+
// the run loop instead of blocking on a semaphore.
|
|
459
500
|
while result.value == nil {
|
|
460
|
-
|
|
501
|
+
CFRunLoopRunInMode(.commonModes, 0.1, false)
|
|
461
502
|
}
|
|
462
503
|
return try result.value.get()
|
|
463
504
|
}
|
|
@@ -465,9 +506,13 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable {
|
|
|
465
506
|
/**
|
|
466
507
|
Asynchronously executes a sync closure on the JavaScript runtime thread, awaiting its completion without blocking.
|
|
467
508
|
*/
|
|
509
|
+
@discardableResult
|
|
468
510
|
public func execute<R: Sendable>(
|
|
469
511
|
@_implicitSelfCapture _ closure: @escaping @JavaScriptActor () throws -> R
|
|
470
512
|
) async throws -> sending R {
|
|
513
|
+
if isOnJavaScriptThread() {
|
|
514
|
+
return try JavaScriptActor.assumeIsolated(closure)
|
|
515
|
+
}
|
|
471
516
|
return try await withUnsafeThrowingContinuation { continuation in
|
|
472
517
|
scheduler.scheduleTask(.ImmediatePriority) {
|
|
473
518
|
do {
|
|
@@ -482,13 +527,16 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable {
|
|
|
482
527
|
/**
|
|
483
528
|
Asynchronously executes an async closure on the JavaScript runtime thread, awaiting its completion without blocking.
|
|
484
529
|
*/
|
|
530
|
+
@discardableResult
|
|
485
531
|
public func execute<R: Sendable>(
|
|
486
|
-
taskName: String? = "[JS] runtime.execute (async \(#function))",
|
|
487
532
|
@_implicitSelfCapture _ closure: @escaping @JavaScriptActor () async throws -> R
|
|
488
533
|
) async throws -> sending R {
|
|
534
|
+
if isOnJavaScriptThread() {
|
|
535
|
+
return try await Task.immediate_polyfill(priority: .high, operation: closure).value
|
|
536
|
+
}
|
|
489
537
|
return try await withUnsafeThrowingContinuation { continuation in
|
|
490
538
|
scheduler.scheduleTask(.ImmediatePriority) {
|
|
491
|
-
Task.immediate_polyfill(
|
|
539
|
+
Task.immediate_polyfill(priority: .high) { @JavaScriptActor in
|
|
492
540
|
do {
|
|
493
541
|
continuation.resume(returning: try await closure())
|
|
494
542
|
} catch {
|
|
@@ -502,14 +550,18 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable {
|
|
|
502
550
|
/**
|
|
503
551
|
Checks whether the function is called on the JavaScript thread.
|
|
504
552
|
*/
|
|
505
|
-
|
|
506
|
-
|
|
553
|
+
@inline(__always)
|
|
554
|
+
public final func isOnJavaScriptThread() -> Bool {
|
|
555
|
+
var current: UInt64 = 0
|
|
556
|
+
pthread_threadid_np(nil, ¤t)
|
|
557
|
+
return current == jsThreadID
|
|
507
558
|
}
|
|
508
559
|
|
|
509
560
|
/**
|
|
510
561
|
Asserts whether we are on the JavaScript thread. Helpful for debugging threading issues.
|
|
511
562
|
*/
|
|
512
|
-
|
|
563
|
+
@inline(__always)
|
|
564
|
+
public final func assertThread(file: String = #file, function: String = #function, line: Int = #line) {
|
|
513
565
|
assert(isOnJavaScriptThread(), "Function '\(function)' is not run on the JavaScript thread (\(file):\(line))")
|
|
514
566
|
}
|
|
515
567
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Testing
|
|
2
2
|
import ExpoModulesJSI
|
|
3
|
+
import Foundation
|
|
3
4
|
|
|
4
5
|
@Suite
|
|
5
6
|
@JavaScriptActor
|
|
@@ -118,6 +119,49 @@ struct JavaScriptRuntimeTests {
|
|
|
118
119
|
#expect(result == 100)
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
// The execute<R> overloads have a same-thread fast path and a cross-thread path that
|
|
123
|
+
// schedules the closure onto the JS thread and pumps the caller's run loop until it
|
|
124
|
+
// completes. The tests above run on `@JavaScriptActor` (the JS thread), so they only
|
|
125
|
+
// exercise the fast path. The next three hop off the JS thread first to cover the
|
|
126
|
+
// cross-thread scheduling + run-loop pump. The sync overloads of `execute` are
|
|
127
|
+
// `@available(*, noasync)`, so the caller must be a real synchronous thread — wrapping
|
|
128
|
+
// in `Task.detached` would stay on the cooperative pool and trip the noasync diagnostic.
|
|
129
|
+
|
|
130
|
+
@Test
|
|
131
|
+
func `execute sync from off-thread caller`() async throws {
|
|
132
|
+
let runtime = self.runtime
|
|
133
|
+
let result = try await onSyncOffThread {
|
|
134
|
+
try runtime.execute { @JavaScriptActor in
|
|
135
|
+
return runtime.global().hasProperty("Object") ? 1 : 0
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
#expect(result == 1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@Test
|
|
142
|
+
func `execute blocking-async from off-thread caller`() async throws {
|
|
143
|
+
let runtime = self.runtime
|
|
144
|
+
let result = try await onSyncOffThread {
|
|
145
|
+
try runtime.execute { @JavaScriptActor () async in
|
|
146
|
+
await Task.yield()
|
|
147
|
+
return runtime.global().hasProperty("Object") ? 1 : 0
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
#expect(result == 1)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Test
|
|
154
|
+
func `execute sync rethrows from off-thread caller`() async throws {
|
|
155
|
+
let runtime = self.runtime
|
|
156
|
+
await #expect(throws: ScriptEvaluationError.self) {
|
|
157
|
+
try await onSyncOffThread {
|
|
158
|
+
try runtime.execute { @JavaScriptActor in
|
|
159
|
+
try runtime.eval("invalid syntax +++")
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
121
165
|
// MARK: - Host objects
|
|
122
166
|
|
|
123
167
|
@Test
|
|
@@ -698,3 +742,19 @@ struct JavaScriptRuntimeTests {
|
|
|
698
742
|
#expect(result.getInt() == 3)
|
|
699
743
|
}
|
|
700
744
|
}
|
|
745
|
+
|
|
746
|
+
/// Runs `body` on a freshly spawned synchronous thread and bridges the result back into the
|
|
747
|
+
/// async test. The thread has a real run loop, which the cross-thread `execute` path pumps.
|
|
748
|
+
private func onSyncOffThread<R: Sendable>(
|
|
749
|
+
_ body: @escaping @Sendable () throws -> R
|
|
750
|
+
) async throws -> R {
|
|
751
|
+
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<R, any Error>) in
|
|
752
|
+
Thread.detachNewThread {
|
|
753
|
+
do {
|
|
754
|
+
continuation.resume(returning: try body())
|
|
755
|
+
} catch {
|
|
756
|
+
continuation.resume(throwing: error)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-jsi",
|
|
3
|
-
"version": "56.0.
|
|
3
|
+
"version": "56.0.5",
|
|
4
4
|
"description": "The JavaScript Interface for Expo Modules",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"sideEffects": [],
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"./apple/scripts/test.sh"
|
|
42
42
|
]
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "f26be3dd9396bf7c399a1d607865d0fabdbc0d64",
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "apple/scripts/build-xcframework.sh",
|
|
47
47
|
"test": "apple/scripts/test.sh"
|