Skip to content

Commit 253cfc9

Browse files
Allow async main entry point with JavaScriptEventLoop
This change allows the Swift program to have an async main entry point when the JavaScriptEventLoop is installed as the global executor.
1 parent 68376c5 commit 253cfc9

File tree

8 files changed

+150
-5
lines changed

8 files changed

+150
-5
lines changed

Runtime/src/index.ts

+39
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,27 @@ export class SwiftRuntime {
4545
}
4646
}
4747

48+
main() {
49+
const instance = this.instance;
50+
try {
51+
if (typeof instance.exports.main === "function") {
52+
instance.exports.main();
53+
} else if (
54+
typeof instance.exports.__main_argc_argv === "function"
55+
) {
56+
// Swift 6.0 and later use `__main_argc_argv` instead of `main`.
57+
instance.exports.__main_argc_argv(0, 0);
58+
}
59+
} catch (error) {
60+
if (error instanceof UnsafeEventLoopYield) {
61+
// Ignore the error
62+
return;
63+
}
64+
// Rethrow other errors
65+
throw error;
66+
}
67+
}
68+
4869
private get instance() {
4970
if (!this._instance)
5071
throw new Error("WebAssembly instance is not set yet");
@@ -419,5 +440,23 @@ export class SwiftRuntime {
419440
signed ? BigInt.asIntN(64, value) : BigInt.asUintN(64, value)
420441
);
421442
},
443+
swjs_unsafe_event_loop_yield: () => {
444+
throw new UnsafeEventLoopYield();
445+
},
422446
};
423447
}
448+
449+
/// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue`
450+
/// to JavaScript. This is usually thrown when:
451+
/// - The entry point of the Swift program is `func main() async`
452+
/// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()`
453+
/// - Calling exported `main` or `__main_argc_argv` function from JavaScript
454+
///
455+
/// This exception must be caught by the caller of the exported function and the caller should
456+
/// catch this exception and just ignore it.
457+
///
458+
/// FAQ: Why this error is thrown?
459+
/// This error is thrown to unwind the call stack of the Swift program and return the control to
460+
/// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()`
461+
/// because the event loop expects `exit()` call before the end of the event loop.
462+
class UnsafeEventLoopYield extends Error {}

Runtime/src/js-value.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,13 @@ export const write = (
8282
is_exception: boolean,
8383
memory: Memory
8484
) => {
85-
const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory);
85+
const kind = writeAndReturnKindBits(
86+
value,
87+
payload1_ptr,
88+
payload2_ptr,
89+
is_exception,
90+
memory
91+
);
8692
memory.writeUint32(kind_ptr, kind);
8793
};
8894

Runtime/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export interface ImportedFunctions {
102102
swjs_i64_to_bigint(value: bigint, signed: bool): ref;
103103
swjs_bigint_to_i64(ref: ref, signed: bool): bigint;
104104
swjs_i64_to_bigint_slow(lower: number, upper: number, signed: bool): ref;
105+
swjs_unsafe_event_loop_yield: () => void;
105106
}
106107

107108
export const enum LibraryFeatures {

Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import JavaScriptKit
22
import _CJavaScriptEventLoop
3+
import _CJavaScriptKit
34

45
// NOTE: `@available` annotations are semantically wrong, but they make it easier to develop applications targeting WebAssembly in Xcode.
56

@@ -90,6 +91,14 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
9091
public static func installGlobalExecutor() {
9192
guard !didInstallGlobalExecutor else { return }
9293

94+
#if compiler(>=5.9)
95+
typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) (swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override) -> Void
96+
let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in
97+
_unsafe_event_loop_yield()
98+
}
99+
swift_task_asyncMainDrainQueue_hook = unsafeBitCast(swift_task_asyncMainDrainQueue_hook_impl, to: UnsafeMutableRawPointer?.self)
100+
#endif
101+
93102
typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) -> Void
94103
let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in
95104
JavaScriptEventLoop.shared.unsafeEnqueue(job)

Sources/JavaScriptKit/Runtime/index.js

+38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/JavaScriptKit/Runtime/index.mjs

+38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h

+15-4
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobal_original)(
2727
Job *_Nonnull job);
2828

2929
SWIFT_EXPORT_FROM(swift_Concurrency)
30-
void *_Nullable swift_task_enqueueGlobal_hook;
30+
extern void *_Nullable swift_task_enqueueGlobal_hook;
3131

3232
/// A hook to take over global enqueuing with delay.
3333
typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDelay_original)(
3434
unsigned long long delay, Job *_Nonnull job);
3535
SWIFT_EXPORT_FROM(swift_Concurrency)
36-
void *_Nullable swift_task_enqueueGlobalWithDelay_hook;
36+
extern void *_Nullable swift_task_enqueueGlobalWithDelay_hook;
3737

3838
typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)(
3939
long long sec,
@@ -42,12 +42,23 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)(
4242
long long tnsec,
4343
int clock, Job *_Nonnull job);
4444
SWIFT_EXPORT_FROM(swift_Concurrency)
45-
void *_Nullable swift_task_enqueueGlobalWithDeadline_hook;
45+
extern void *_Nullable swift_task_enqueueGlobalWithDeadline_hook;
4646

4747
/// A hook to take over main executor enqueueing.
4848
typedef SWIFT_CC(swift) void (*swift_task_enqueueMainExecutor_original)(
4949
Job *_Nonnull job);
5050
SWIFT_EXPORT_FROM(swift_Concurrency)
51-
void *_Nullable swift_task_enqueueMainExecutor_hook;
51+
extern void *_Nullable swift_task_enqueueMainExecutor_hook;
52+
53+
/// A hook to override the entrypoint to the main runloop used to drive the
54+
/// concurrency runtime and drain the main queue. This function must not return.
55+
/// Note: If the hook is wrapping the original function and the `compatOverride`
56+
/// is passed in, the `original` function pointer must be passed into the
57+
/// compatibility override function as the original function.
58+
typedef SWIFT_CC(swift) void (*swift_task_asyncMainDrainQueue_original)();
59+
typedef SWIFT_CC(swift) void (*swift_task_asyncMainDrainQueue_override)(
60+
swift_task_asyncMainDrainQueue_original _Nullable original);
61+
SWIFT_EXPORT_FROM(swift_Concurrency)
62+
extern void *_Nullable swift_task_asyncMainDrainQueue_hook;
5263

5364
#endif

Sources/_CJavaScriptKit/include/_CJavaScriptKit.h

+3
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ __attribute__((__import_module__("javascript_kit"),
322322
__import_name__("swjs_release")))
323323
extern void _release(const JavaScriptObjectRef ref);
324324

325+
__attribute__((__import_module__("javascript_kit"),
326+
__import_name__("swjs_unsafe_event_loop_yield")))
327+
extern void _unsafe_event_loop_yield(void);
325328

326329
#endif
327330

0 commit comments

Comments
 (0)