From e28b75db338ab6d2d3fcc7e7f7a9e64a53c9c1d0 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 21 Jun 2025 19:14:27 -0400 Subject: [PATCH 01/23] Integrate uv_loop with CFRunLoop --- Sources/CNodeAPISupport/include/uv_stubs.h | 41 ++++++++++++++ Sources/NodeAPI/UV.swift | 63 ++++++++++++++++++++++ test/suites/Test/Test.swift | 13 ++++- test/suites/Test/index.js | 6 +++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 Sources/CNodeAPISupport/include/uv_stubs.h create mode 100644 Sources/NodeAPI/UV.swift diff --git a/Sources/CNodeAPISupport/include/uv_stubs.h b/Sources/CNodeAPISupport/include/uv_stubs.h new file mode 100644 index 0000000..da4f5df --- /dev/null +++ b/Sources/CNodeAPISupport/include/uv_stubs.h @@ -0,0 +1,41 @@ +#ifndef uv_stubs_h +#define uv_stubs_h + +#ifdef __APPLE__ + +#include +#include + +static inline void node_swift_fd_set(int fd, struct fd_set *const p) { + FD_SET(fd, p); +} + +typedef enum { + UV_RUN_DEFAULT = 0, + UV_RUN_ONCE, + UV_RUN_NOWAIT +} uv_run_mode; + +typedef enum { + UV_ASYNC = 1, +} uv_handle_type; + +typedef struct uv_loop_s uv_loop_t; +typedef struct uv_async_s uv_async_t; +typedef void (*uv_async_cb)(uv_async_t *handle); + +size_t uv_handle_size(uv_handle_type type); + +uv_loop_t *uv_default_loop(void); +int uv_backend_fd(const uv_loop_t *); +int uv_backend_timeout(const uv_loop_t *); +int uv_run(uv_loop_t *, uv_run_mode mode); + +int uv_async_init(uv_loop_t *loop, + uv_async_t *async, + uv_async_cb async_cb); +int uv_async_send(uv_async_t *async); + +#endif /* __APPLE__ */ + +#endif /* uv_stubs_h */ diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/UV.swift new file mode 100644 index 0000000..2f52408 --- /dev/null +++ b/Sources/NodeAPI/UV.swift @@ -0,0 +1,63 @@ +#if canImport(Darwin) + +import Foundation +import CNodeAPISupport +internal import CNodeAPI + +public enum UV { + public static func setup() { + nonisolated(unsafe) let loop = uv_default_loop() + + // https://github.com/electron/electron/blob/dac5e0cd1a8d31272f428d08289b4b66cb9192fc/shell/common/node_bindings.cc#L962 + let poller = Thread { + nonisolated(unsafe) let loop = loop + + while !Thread.current.isCancelled { + // https://github.com/electron/electron/blob/dac5e0cd1a8d31272f428d08289b4b66cb9192fc/shell/common/node_bindings_mac.cc#L24 + let fd = uv_backend_fd(loop) + let timeoutMS = Int(uv_backend_timeout(loop)) + var readset = fd_set() + node_swift_fd_set(fd, &readset) + var result: Int32 + repeat { + if timeoutMS == -1 { + result = select(fd + 1, &readset, nil, nil, nil) + } else { + var timeout = timeval( + tv_sec: timeoutMS / 1000, + tv_usec: Int32((timeoutMS % 1000) * 1000) + ) + result = select(fd + 1, &readset, nil, nil, &timeout) + } + } while result == -1 && errno == EINTR + + let runResult = DispatchQueue.main.sync { + uv_run(loop, UV_RUN_NOWAIT) + } + if runResult == 0 { break } + } + } + poller.start() + + // TODO: figure out whether this is supported. + // specifically, RunLoop.main.run inside an async block, + // which services DispatchQueue.main.sync { uv_run() }, + // means the above uv_run is called inside the node shell's + // uv_run. The docs say uv_run isn't re-entrant, whoops. + // + // Ideally, we'd 1) perform the above setup and + // 2) replace Node's main uv_run with a RunLoop.main.run() + let uvAsync = OpaquePointer(UnsafeMutableRawPointer.allocate( + // for ABI stability, don't hardcode current uv_async_t size + byteCount: uv_handle_size(UV_ASYNC), + alignment: MemoryLayout.alignment + )) + + uv_async_init(loop, uvAsync) { _ in + RunLoop.main.run() + } + uv_async_send(uvAsync) + } +} + +#endif diff --git a/test/suites/Test/Test.swift b/test/suites/Test/Test.swift index d4355f2..063c6a3 100644 --- a/test/suites/Test/Test.swift +++ b/test/suites/Test/Test.swift @@ -56,6 +56,17 @@ import NodeAPI try FileManager.default.removeItem(at: url) return undefined } + + @NodeMethod + func mainActorMethod() async -> String { + await Task { @MainActor in + await Task.yield() + return "Message from main actor" + }.value + } } -#NodeModule(exports: ["File": File.deferredConstructor]) +#NodeModule { + UV.setup() + return ["File": File.deferredConstructor] +} diff --git a/test/suites/Test/index.js b/test/suites/Test/index.js index f6d2672..013a7c3 100644 --- a/test/suites/Test/index.js +++ b/test/suites/Test/index.js @@ -7,6 +7,12 @@ assert.strictEqual(File.default().filename, "default.txt") const file = new File("test.txt"); assert.strictEqual(file.filename, "test.txt") +(async () => { + console.log("Getting main actor message"); + const msg = await file.mainActorMethod(); + console.log("Main actor message:", msg); +})() + let err = ""; try { file.contents From 65e0a126b1427e9c3f238626c43c08aac0646141 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 21 Jun 2025 23:15:10 -0400 Subject: [PATCH 02/23] Fix test --- test/suites/Test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/suites/Test/index.js b/test/suites/Test/index.js index 013a7c3..ece03cd 100644 --- a/test/suites/Test/index.js +++ b/test/suites/Test/index.js @@ -7,7 +7,7 @@ assert.strictEqual(File.default().filename, "default.txt") const file = new File("test.txt"); assert.strictEqual(file.filename, "test.txt") -(async () => { +;(async () => { console.log("Getting main actor message"); const msg = await file.mainActorMethod(); console.log("Main actor message:", msg); From 86545a5255cc0edd5336427ef351a95c57255ea0 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 21 Jun 2025 23:15:29 -0400 Subject: [PATCH 03/23] Use DispatchSource --- Sources/CNodeAPISupport/include/uv_stubs.h | 5 -- Sources/NodeAPI/UV.swift | 62 ++++++++++++---------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/Sources/CNodeAPISupport/include/uv_stubs.h b/Sources/CNodeAPISupport/include/uv_stubs.h index da4f5df..8409b73 100644 --- a/Sources/CNodeAPISupport/include/uv_stubs.h +++ b/Sources/CNodeAPISupport/include/uv_stubs.h @@ -3,13 +3,8 @@ #ifdef __APPLE__ -#include #include -static inline void node_swift_fd_set(int fd, struct fd_set *const p) { - FD_SET(fd, p); -} - typedef enum { UV_RUN_DEFAULT = 0, UV_RUN_ONCE, diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/UV.swift index 2f52408..6774789 100644 --- a/Sources/NodeAPI/UV.swift +++ b/Sources/NodeAPI/UV.swift @@ -6,38 +6,44 @@ internal import CNodeAPI public enum UV { public static func setup() { - nonisolated(unsafe) let loop = uv_default_loop() + MainActor.assumeIsolated { _setup() } + } + @MainActor private static func _setup() { // https://github.com/electron/electron/blob/dac5e0cd1a8d31272f428d08289b4b66cb9192fc/shell/common/node_bindings.cc#L962 - let poller = Thread { - nonisolated(unsafe) let loop = loop - - while !Thread.current.isCancelled { - // https://github.com/electron/electron/blob/dac5e0cd1a8d31272f428d08289b4b66cb9192fc/shell/common/node_bindings_mac.cc#L24 - let fd = uv_backend_fd(loop) - let timeoutMS = Int(uv_backend_timeout(loop)) - var readset = fd_set() - node_swift_fd_set(fd, &readset) - var result: Int32 - repeat { - if timeoutMS == -1 { - result = select(fd + 1, &readset, nil, nil, nil) - } else { - var timeout = timeval( - tv_sec: timeoutMS / 1000, - tv_usec: Int32((timeoutMS % 1000) * 1000) - ) - result = select(fd + 1, &readset, nil, nil, &timeout) - } - } while result == -1 && errno == EINTR - - let runResult = DispatchQueue.main.sync { - uv_run(loop, UV_RUN_NOWAIT) - } - if runResult == 0 { break } + // https://github.com/electron/electron/blob/dac5e0cd1a8d31272f428d08289b4b66cb9192fc/shell/common/node_bindings_mac.cc#L24 + + let loop = uv_default_loop() + + let fd = uv_backend_fd(loop) + + let reader = DispatchSource.makeReadSource(fileDescriptor: fd, queue: .main) + let timer = DispatchSource.makeTimerSource(queue: .main) + + let wakeUpUV = { + let runResult = uv_run(loop, UV_RUN_NOWAIT) + guard runResult != 0 else { return } + + reader.activate() + + let timeout = Int(uv_backend_timeout(loop)) + if timeout != -1 { + timer.schedule(deadline: .now() + .milliseconds(timeout)) + timer.activate() } } - poller.start() + + reader.setEventHandler { + wakeUpUV() + } + + timer.setEventHandler { + wakeUpUV() + } + + DispatchQueue.main.async { + wakeUpUV() + } // TODO: figure out whether this is supported. // specifically, RunLoop.main.run inside an async block, From 22b47e5e2d09ee35cb628c470f68438cf4d74f88 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 21 Jun 2025 23:24:23 -0400 Subject: [PATCH 04/23] tweaks --- Sources/CNodeAPISupport/include/uv_stubs.h | 1 + Sources/NodeAPI/UV.swift | 1 - test/suites/Test/index.js | 4 ++++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/CNodeAPISupport/include/uv_stubs.h b/Sources/CNodeAPISupport/include/uv_stubs.h index 8409b73..5679ba1 100644 --- a/Sources/CNodeAPISupport/include/uv_stubs.h +++ b/Sources/CNodeAPISupport/include/uv_stubs.h @@ -25,6 +25,7 @@ uv_loop_t *uv_default_loop(void); int uv_backend_fd(const uv_loop_t *); int uv_backend_timeout(const uv_loop_t *); int uv_run(uv_loop_t *, uv_run_mode mode); +int uv_loop_alive(const uv_loop_t *loop); int uv_async_init(uv_loop_t *loop, uv_async_t *async, diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/UV.swift index 6774789..6d94d33 100644 --- a/Sources/NodeAPI/UV.swift +++ b/Sources/NodeAPI/UV.swift @@ -2,7 +2,6 @@ import Foundation import CNodeAPISupport -internal import CNodeAPI public enum UV { public static func setup() { diff --git a/test/suites/Test/index.js b/test/suites/Test/index.js index ece03cd..59576b7 100644 --- a/test/suites/Test/index.js +++ b/test/suites/Test/index.js @@ -7,6 +7,10 @@ assert.strictEqual(File.default().filename, "default.txt") const file = new File("test.txt"); assert.strictEqual(file.filename, "test.txt") +// setInterval(() => { +// console.log("hi") +// }, 1000); + ;(async () => { console.log("Getting main actor message"); const msg = await file.mainActorMethod(); From f406c06aaf1c8c76a1ae0933325f5a94fe32f295 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 21 Jun 2025 23:46:19 -0400 Subject: [PATCH 05/23] Documentation --- Sources/NodeAPI/UV.swift | 51 +++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/UV.swift index 6d94d33..2d29e4e 100644 --- a/Sources/NodeAPI/UV.swift +++ b/Sources/NodeAPI/UV.swift @@ -9,11 +9,22 @@ public enum UV { } @MainActor private static func _setup() { + // By default, node takes over the main thread with an indefinite uv_run(). + // This causes CFRunLoop sources to not be processed (also breaking GCD & MainActor) + // since the CFRunLoop never gets ticked. We instead need to flip things on their + // head: ie use CFRunLoop as the main driver of the process. This is feasible because + // libuv offers an API to "embed" its RunLoop into another. Specifically, it exposes + // a backend file descriptor & timer; we can tell GCD to watch these. Any time they + // trigger, we tick the uv loop once. + + // References: + // https://github.com/TooTallNate/NodObjC/issues/2 // https://github.com/electron/electron/blob/dac5e0cd1a8d31272f428d08289b4b66cb9192fc/shell/common/node_bindings.cc#L962 // https://github.com/electron/electron/blob/dac5e0cd1a8d31272f428d08289b4b66cb9192fc/shell/common/node_bindings_mac.cc#L24 + // https://github.com/indutny/node-cf/blob/de90092bb65bbdb6acbd0b00e18a360028b815f5/src/cf.cc + // [SpinEventLoopInternal]: https://github.com/nodejs/node/blob/11222f1a272b9b2ab000e75cbe3e09942bd2d877/src/api/embed_helpers.cc#L41 let loop = uv_default_loop() - let fd = uv_backend_fd(loop) let reader = DispatchSource.makeReadSource(fileDescriptor: fd, queue: .main) @@ -32,32 +43,30 @@ public enum UV { } } - reader.setEventHandler { - wakeUpUV() - } - - timer.setEventHandler { - wakeUpUV() - } - - DispatchQueue.main.async { - wakeUpUV() - } + reader.setEventHandler { wakeUpUV() } + timer.setEventHandler { wakeUpUV() } + DispatchQueue.main.async { wakeUpUV() } - // TODO: figure out whether this is supported. - // specifically, RunLoop.main.run inside an async block, - // which services DispatchQueue.main.sync { uv_run() }, - // means the above uv_run is called inside the node shell's - // uv_run. The docs say uv_run isn't re-entrant, whoops. - // - // Ideally, we'd 1) perform the above setup and - // 2) replace Node's main uv_run with a RunLoop.main.run() + // Now that we've set up the CF/GCD sources, we need to + // start the CFRunLoop. Ideally, we'd patch the Node.js + // source to 1) perform the above setup and 2) replace + // its uv_run with CFRunLoopRun: insertion point would + // be [SpinEventLoopInternal] linked above. + // However, the hacky alternative (while avoiding the need + // to patch Node) is to kick off the CFRunLoop on the next + // uv tick. This is hacky because we eventually end up with + // a callstack that looks like: + // uv_run -> [process uv_async_t] -> RunLoop.run -> wakeUpUV -> uv_run + // Note that uv_run is called re-entrantly here, which is + // explicitly unsupported per the documentation. This seems + // to work okay based on rudimentary testing but could definitely + // break in the future / under edge cases. + // TODO: figure out whether there's a better solution. let uvAsync = OpaquePointer(UnsafeMutableRawPointer.allocate( // for ABI stability, don't hardcode current uv_async_t size byteCount: uv_handle_size(UV_ASYNC), alignment: MemoryLayout.alignment )) - uv_async_init(loop, uvAsync) { _ in RunLoop.main.run() } From 7c3c65d1f0d9faccd6eb7d2e80c4e4cf3764240f Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 21 Jun 2025 23:47:27 -0400 Subject: [PATCH 06/23] another comment --- Sources/NodeAPI/UV.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/UV.swift index 2d29e4e..c44fbea 100644 --- a/Sources/NodeAPI/UV.swift +++ b/Sources/NodeAPI/UV.swift @@ -45,6 +45,7 @@ public enum UV { reader.setEventHandler { wakeUpUV() } timer.setEventHandler { wakeUpUV() } + // bootstrap DispatchQueue.main.async { wakeUpUV() } // Now that we've set up the CF/GCD sources, we need to From eb5543f438207f4b14b1ef65082b687be9d477c4 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 21 Jun 2025 23:49:09 -0400 Subject: [PATCH 07/23] more docs --- Sources/NodeAPI/UV.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/UV.swift index c44fbea..33b214a 100644 --- a/Sources/NodeAPI/UV.swift +++ b/Sources/NodeAPI/UV.swift @@ -54,10 +54,11 @@ public enum UV { // its uv_run with CFRunLoopRun: insertion point would // be [SpinEventLoopInternal] linked above. // However, the hacky alternative (while avoiding the need - // to patch Node) is to kick off the CFRunLoop on the next + // to patch Node) is to kick off the CFRunLoop inside the next // uv tick. This is hacky because we eventually end up with // a callstack that looks like: - // uv_run -> [process uv_async_t] -> RunLoop.run -> wakeUpUV -> uv_run + // uv_run -> [process uv_async_t] -> RunLoop.run + // -> [some event uv cares about] -> wakeUpUV -> uv_run // Note that uv_run is called re-entrantly here, which is // explicitly unsupported per the documentation. This seems // to work okay based on rudimentary testing but could definitely From 8ba5289cccfd8e46dce79f3485a0363b28ccf8a8 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 00:04:48 -0400 Subject: [PATCH 08/23] Memoize --- Sources/NodeAPI/UV.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/UV.swift index 33b214a..2a874f4 100644 --- a/Sources/NodeAPI/UV.swift +++ b/Sources/NodeAPI/UV.swift @@ -4,8 +4,10 @@ import Foundation import CNodeAPISupport public enum UV { - public static func setup() { - MainActor.assumeIsolated { _setup() } + @MainActor private static let setupOnce: Void = _setup() + + @NodeActor public static func setup() { + MainActor.assumeIsolated { _ = setupOnce } } @MainActor private static func _setup() { @@ -76,4 +78,10 @@ public enum UV { } } +#else + +public enum UV { + @NodeActor public static func setup() {} +} + #endif From 613288de22ab666f5eaa8947c4acc517ed81fdfe Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 00:55:18 -0400 Subject: [PATCH 09/23] Support stopping CFRL --- Sources/CNodeAPISupport/include/uv_stubs.h | 2 + Sources/NodeAPI/UV.swift | 43 ++++++++++++++++++---- test/suites/Test/Test.swift | 6 ++- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/Sources/CNodeAPISupport/include/uv_stubs.h b/Sources/CNodeAPISupport/include/uv_stubs.h index 5679ba1..f723307 100644 --- a/Sources/CNodeAPISupport/include/uv_stubs.h +++ b/Sources/CNodeAPISupport/include/uv_stubs.h @@ -32,6 +32,8 @@ int uv_async_init(uv_loop_t *loop, uv_async_cb async_cb); int uv_async_send(uv_async_t *async); +void uv_close(uv_async_t *handle, void *close_cb); + #endif /* __APPLE__ */ #endif /* uv_stubs_h */ diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/UV.swift index 2a874f4..c6738bc 100644 --- a/Sources/NodeAPI/UV.swift +++ b/Sources/NodeAPI/UV.swift @@ -4,13 +4,33 @@ import Foundation import CNodeAPISupport public enum UV { - @MainActor private static let setupOnce: Void = _setup() + // would ideally be marked @MainActor but we can't prove that + // MainActor == NodeActor, because the runtime notices that the Node actor + // is active when it runs (although this is also on the main thread...), + // causing MainActor.assumeIsolated to abort. + private nonisolated(unsafe) static var cancelHandlers: (() -> Void)? + private nonisolated(unsafe) static var shouldContinue = true - @NodeActor public static func setup() { - MainActor.assumeIsolated { _ = setupOnce } + @NodeActor public static func enable() { + if cancelHandlers == nil { + cancelHandlers = _enable() + } } - @MainActor private static func _setup() { + public static func disable() { + if Thread.isMainThread { + _disable() + } else { + Task { @MainActor in _disable() } + } + } + + private static func _disable() { + cancelHandlers?() + cancelHandlers = nil + } + + private static func _enable() -> (() -> Void) { // By default, node takes over the main thread with an indefinite uv_run(). // This causes CFRunLoop sources to not be processed (also breaking GCD & MainActor) // since the CFRunLoop never gets ticked. We instead need to flip things on their @@ -26,13 +46,15 @@ public enum UV { // https://github.com/indutny/node-cf/blob/de90092bb65bbdb6acbd0b00e18a360028b815f5/src/cf.cc // [SpinEventLoopInternal]: https://github.com/nodejs/node/blob/11222f1a272b9b2ab000e75cbe3e09942bd2d877/src/api/embed_helpers.cc#L41 + UV.shouldContinue = true + let loop = uv_default_loop() let fd = uv_backend_fd(loop) let reader = DispatchSource.makeReadSource(fileDescriptor: fd, queue: .main) let timer = DispatchSource.makeTimerSource(queue: .main) - let wakeUpUV = { + nonisolated(unsafe) let wakeUpUV = { let runResult = uv_run(loop, UV_RUN_NOWAIT) guard runResult != 0 else { return } @@ -53,7 +75,7 @@ public enum UV { // Now that we've set up the CF/GCD sources, we need to // start the CFRunLoop. Ideally, we'd patch the Node.js // source to 1) perform the above setup and 2) replace - // its uv_run with CFRunLoopRun: insertion point would + // its uv_run with RunLoop.run: insertion point would // be [SpinEventLoopInternal] linked above. // However, the hacky alternative (while avoiding the need // to patch Node) is to kick off the CFRunLoop inside the next @@ -72,9 +94,16 @@ public enum UV { alignment: MemoryLayout.alignment )) uv_async_init(loop, uvAsync) { _ in - RunLoop.main.run() + while UV.shouldContinue && RunLoop.main.run(mode: .default, before: .distantFuture) {} } uv_async_send(uvAsync) + + return { + reader.cancel() + timer.cancel() + uv_close(uvAsync, nil) + UV.shouldContinue = false + } } } diff --git a/test/suites/Test/Test.swift b/test/suites/Test/Test.swift index 063c6a3..481e0b6 100644 --- a/test/suites/Test/Test.swift +++ b/test/suites/Test/Test.swift @@ -67,6 +67,10 @@ import NodeAPI } #NodeModule { - UV.setup() + UV.enable() + Task { @NodeActor in + try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) + UV.disable() + } return ["File": File.deferredConstructor] } From c683e69cd76188083f9decfaf88e6d5645aa5837 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 00:57:33 -0400 Subject: [PATCH 10/23] tweaks --- Sources/CNodeAPISupport/include/uv_stubs.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CNodeAPISupport/include/uv_stubs.h b/Sources/CNodeAPISupport/include/uv_stubs.h index f723307..f83fd9c 100644 --- a/Sources/CNodeAPISupport/include/uv_stubs.h +++ b/Sources/CNodeAPISupport/include/uv_stubs.h @@ -15,11 +15,13 @@ typedef enum { UV_ASYNC = 1, } uv_handle_type; +typedef struct uv_handle_s uv_handle_t; typedef struct uv_loop_s uv_loop_t; typedef struct uv_async_s uv_async_t; typedef void (*uv_async_cb)(uv_async_t *handle); size_t uv_handle_size(uv_handle_type type); +void uv_close(uv_handle_t *handle, void *close_cb); uv_loop_t *uv_default_loop(void); int uv_backend_fd(const uv_loop_t *); @@ -32,8 +34,6 @@ int uv_async_init(uv_loop_t *loop, uv_async_cb async_cb); int uv_async_send(uv_async_t *async); -void uv_close(uv_async_t *handle, void *close_cb); - #endif /* __APPLE__ */ #endif /* uv_stubs_h */ From f72ab9e7c29f08b74f6f969d24436cfdeabd95de Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 00:58:44 -0400 Subject: [PATCH 11/23] Fix non-Darwin stubs --- Sources/NodeAPI/UV.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/UV.swift index c6738bc..068e465 100644 --- a/Sources/NodeAPI/UV.swift +++ b/Sources/NodeAPI/UV.swift @@ -110,7 +110,8 @@ public enum UV { #else public enum UV { - @NodeActor public static func setup() {} + @NodeActor public static func enable() {} + public static func disable() } #endif From 68a20c7a727f888f0c0c31bfa3e531d64f9ed51c Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 01:07:40 -0400 Subject: [PATCH 12/23] ref count --- Sources/NodeAPI/UV.swift | 35 ++++++++++++++++++----------------- test/suites/Test/Test.swift | 4 ++-- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/UV.swift index 068e465..2c20c42 100644 --- a/Sources/NodeAPI/UV.swift +++ b/Sources/NodeAPI/UV.swift @@ -9,28 +9,32 @@ public enum UV { // is active when it runs (although this is also on the main thread...), // causing MainActor.assumeIsolated to abort. private nonisolated(unsafe) static var cancelHandlers: (() -> Void)? - private nonisolated(unsafe) static var shouldContinue = true + private nonisolated(unsafe) static var refCount = 0 - @NodeActor public static func enable() { - if cancelHandlers == nil { - cancelHandlers = _enable() + @NodeActor public static func ref() { + refCount += 1 + if refCount == 1 { + cancelHandlers = setUp() } } - public static func disable() { + public static func unref() { if Thread.isMainThread { - _disable() + _unref() } else { - Task { @MainActor in _disable() } + Task { @MainActor in _unref() } } } - private static func _disable() { - cancelHandlers?() - cancelHandlers = nil + private static func _unref() { + refCount -= 1 + if refCount == 0 { + cancelHandlers?() + cancelHandlers = nil + } } - private static func _enable() -> (() -> Void) { + private static func setUp() -> (() -> Void) { // By default, node takes over the main thread with an indefinite uv_run(). // This causes CFRunLoop sources to not be processed (also breaking GCD & MainActor) // since the CFRunLoop never gets ticked. We instead need to flip things on their @@ -46,8 +50,6 @@ public enum UV { // https://github.com/indutny/node-cf/blob/de90092bb65bbdb6acbd0b00e18a360028b815f5/src/cf.cc // [SpinEventLoopInternal]: https://github.com/nodejs/node/blob/11222f1a272b9b2ab000e75cbe3e09942bd2d877/src/api/embed_helpers.cc#L41 - UV.shouldContinue = true - let loop = uv_default_loop() let fd = uv_backend_fd(loop) @@ -94,7 +96,7 @@ public enum UV { alignment: MemoryLayout.alignment )) uv_async_init(loop, uvAsync) { _ in - while UV.shouldContinue && RunLoop.main.run(mode: .default, before: .distantFuture) {} + while UV.refCount > 0 && RunLoop.main.run(mode: .default, before: .distantFuture) {} } uv_async_send(uvAsync) @@ -102,7 +104,6 @@ public enum UV { reader.cancel() timer.cancel() uv_close(uvAsync, nil) - UV.shouldContinue = false } } } @@ -110,8 +111,8 @@ public enum UV { #else public enum UV { - @NodeActor public static func enable() {} - public static func disable() + @NodeActor public static func ref() {} + public static func unref() } #endif diff --git a/test/suites/Test/Test.swift b/test/suites/Test/Test.swift index 481e0b6..27a78de 100644 --- a/test/suites/Test/Test.swift +++ b/test/suites/Test/Test.swift @@ -67,10 +67,10 @@ import NodeAPI } #NodeModule { - UV.enable() + UV.ref() Task { @NodeActor in try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) - UV.disable() + UV.unref() } return ["File": File.deferredConstructor] } From 125bdda89582f9dc22e75e26774ce752c93bcdf8 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 01:09:16 -0400 Subject: [PATCH 13/23] rename --- Sources/NodeAPI/UV.swift | 6 +++--- test/suites/Test/Test.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/UV.swift index 2c20c42..3878566 100644 --- a/Sources/NodeAPI/UV.swift +++ b/Sources/NodeAPI/UV.swift @@ -3,7 +3,7 @@ import Foundation import CNodeAPISupport -public enum UV { +public enum NodeCFRunLoop { // would ideally be marked @MainActor but we can't prove that // MainActor == NodeActor, because the runtime notices that the Node actor // is active when it runs (although this is also on the main thread...), @@ -96,7 +96,7 @@ public enum UV { alignment: MemoryLayout.alignment )) uv_async_init(loop, uvAsync) { _ in - while UV.refCount > 0 && RunLoop.main.run(mode: .default, before: .distantFuture) {} + while NodeCFRunLoop.refCount > 0 && RunLoop.main.run(mode: .default, before: .distantFuture) {} } uv_async_send(uvAsync) @@ -110,7 +110,7 @@ public enum UV { #else -public enum UV { +public enum NodeCFRunLoop { @NodeActor public static func ref() {} public static func unref() } diff --git a/test/suites/Test/Test.swift b/test/suites/Test/Test.swift index 27a78de..7a8a8ea 100644 --- a/test/suites/Test/Test.swift +++ b/test/suites/Test/Test.swift @@ -67,10 +67,10 @@ import NodeAPI } #NodeModule { - UV.ref() + NodeCFRunLoop.ref() Task { @NodeActor in try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) - UV.unref() + NodeCFRunLoop.unref() } return ["File": File.deferredConstructor] } From 8dc90aeb1b0b96b537448b7c8df4c39ef1f97bbf Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 01:10:01 -0400 Subject: [PATCH 14/23] Rename file --- Sources/NodeAPI/{UV.swift => Node+CFRunLoop.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/NodeAPI/{UV.swift => Node+CFRunLoop.swift} (100%) diff --git a/Sources/NodeAPI/UV.swift b/Sources/NodeAPI/Node+CFRunLoop.swift similarity index 100% rename from Sources/NodeAPI/UV.swift rename to Sources/NodeAPI/Node+CFRunLoop.swift From db047448862369dbfe3d8e53bf1683dfa8e9a0cd Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 01:24:13 -0400 Subject: [PATCH 15/23] fix mem leak --- Sources/CNodeAPISupport/include/uv_stubs.h | 4 +++- Sources/NodeAPI/Node+CFRunLoop.swift | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/CNodeAPISupport/include/uv_stubs.h b/Sources/CNodeAPISupport/include/uv_stubs.h index f83fd9c..74f29f0 100644 --- a/Sources/CNodeAPISupport/include/uv_stubs.h +++ b/Sources/CNodeAPISupport/include/uv_stubs.h @@ -18,10 +18,12 @@ typedef enum { typedef struct uv_handle_s uv_handle_t; typedef struct uv_loop_s uv_loop_t; typedef struct uv_async_s uv_async_t; + typedef void (*uv_async_cb)(uv_async_t *handle); +typedef void (*uv_close_cb)(uv_handle_t *handle); size_t uv_handle_size(uv_handle_type type); -void uv_close(uv_handle_t *handle, void *close_cb); +void uv_close(uv_handle_t *handle, uv_close_cb close_cb); uv_loop_t *uv_default_loop(void); int uv_backend_fd(const uv_loop_t *); diff --git a/Sources/NodeAPI/Node+CFRunLoop.swift b/Sources/NodeAPI/Node+CFRunLoop.swift index 3878566..0720839 100644 --- a/Sources/NodeAPI/Node+CFRunLoop.swift +++ b/Sources/NodeAPI/Node+CFRunLoop.swift @@ -103,7 +103,7 @@ public enum NodeCFRunLoop { return { reader.cancel() timer.cancel() - uv_close(uvAsync, nil) + uv_close(uvAsync) { UnsafeMutableRawPointer($0)?.deallocate() } } } } From 67a6c83f66f70bbf49568b623346b9a74f7fa1a4 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 12:52:28 -0400 Subject: [PATCH 16/23] Skip CF integration under Electron --- Sources/NodeAPI/Node+CFRunLoop.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/NodeAPI/Node+CFRunLoop.swift b/Sources/NodeAPI/Node+CFRunLoop.swift index 0720839..ec23ec5 100644 --- a/Sources/NodeAPI/Node+CFRunLoop.swift +++ b/Sources/NodeAPI/Node+CFRunLoop.swift @@ -13,7 +13,10 @@ public enum NodeCFRunLoop { @NodeActor public static func ref() { refCount += 1 - if refCount == 1 { + // check the mode to determine whether the RunLoop is already running. + // this could be the case if we're in an Electron (or other embedder) + // context. only set up our own loop if it's not running. + if refCount == 1 && RunLoop.main.currentMode == nil { cancelHandlers = setUp() } } From 8eefbf3f4de287bdaef38464654aa3bcf4e3b7b8 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 14:31:34 -0400 Subject: [PATCH 17/23] Separate UV target --- Package.swift | 4 ++++ .../Node+CFRunLoop.swift => NodeUV/NodeCFRunLoop.swift} | 0 test/Package.swift | 1 + 3 files changed, 5 insertions(+) rename Sources/{NodeAPI/Node+CFRunLoop.swift => NodeUV/NodeCFRunLoop.swift} (100%) diff --git a/Package.swift b/Package.swift index f064fec..fe47aab 100644 --- a/Package.swift +++ b/Package.swift @@ -57,6 +57,10 @@ let package = Package( name: "NodeAPI", dependencies: ["CNodeAPI", "CNodeAPISupport", "NodeAPIMacros"] ), + .target( + name: "NodeUV", + dependencies: ["CNodeAPISupport"] + ), .target( name: "NodeModuleSupport", dependencies: ["CNodeAPI"] diff --git a/Sources/NodeAPI/Node+CFRunLoop.swift b/Sources/NodeUV/NodeCFRunLoop.swift similarity index 100% rename from Sources/NodeAPI/Node+CFRunLoop.swift rename to Sources/NodeUV/NodeCFRunLoop.swift diff --git a/test/Package.swift b/test/Package.swift index 4f004a2..c4549cb 100644 --- a/test/Package.swift +++ b/test/Package.swift @@ -29,6 +29,7 @@ let package = Package( name: suite, dependencies: [ .product(name: "NodeAPI", package: "node-swift"), + .product(name: "NodeUV", package: "node-swift"), .product(name: "NodeModuleSupport", package: "node-swift"), ], path: "suites/\(suite)", From 62eceb82d5bc79f3752c44eee8d43c30463d666d Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 17:55:55 -0400 Subject: [PATCH 18/23] Fix NodeUV --- Package.swift | 7 ++++++- Sources/CNodeUV/dummy.c | 1 + Sources/{CNodeAPISupport => CNodeUV}/include/uv_stubs.h | 0 Sources/NodeUV/NodeCFRunLoop.swift | 6 +++--- 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 Sources/CNodeUV/dummy.c rename Sources/{CNodeAPISupport => CNodeUV}/include/uv_stubs.h (100%) diff --git a/Package.swift b/Package.swift index fe47aab..db36c82 100644 --- a/Package.swift +++ b/Package.swift @@ -25,6 +25,10 @@ let package = Package( name: "NodeModuleSupport", targets: ["NodeModuleSupport"] ), + .library( + name: "NodeUV", + targets: ["NodeUV"] + ), ], dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax.git", "600.0.0"..<"602.0.0"), @@ -57,9 +61,10 @@ let package = Package( name: "NodeAPI", dependencies: ["CNodeAPI", "CNodeAPISupport", "NodeAPIMacros"] ), + .target(name: "CNodeUV"), .target( name: "NodeUV", - dependencies: ["CNodeAPISupport"] + dependencies: ["CNodeUV"] ), .target( name: "NodeModuleSupport", diff --git a/Sources/CNodeUV/dummy.c b/Sources/CNodeUV/dummy.c new file mode 100644 index 0000000..8b1a393 --- /dev/null +++ b/Sources/CNodeUV/dummy.c @@ -0,0 +1 @@ +// empty diff --git a/Sources/CNodeAPISupport/include/uv_stubs.h b/Sources/CNodeUV/include/uv_stubs.h similarity index 100% rename from Sources/CNodeAPISupport/include/uv_stubs.h rename to Sources/CNodeUV/include/uv_stubs.h diff --git a/Sources/NodeUV/NodeCFRunLoop.swift b/Sources/NodeUV/NodeCFRunLoop.swift index ec23ec5..076cc7c 100644 --- a/Sources/NodeUV/NodeCFRunLoop.swift +++ b/Sources/NodeUV/NodeCFRunLoop.swift @@ -1,7 +1,7 @@ #if canImport(Darwin) import Foundation -import CNodeAPISupport +import CNodeUV public enum NodeCFRunLoop { // would ideally be marked @MainActor but we can't prove that @@ -11,7 +11,7 @@ public enum NodeCFRunLoop { private nonisolated(unsafe) static var cancelHandlers: (() -> Void)? private nonisolated(unsafe) static var refCount = 0 - @NodeActor public static func ref() { + public static func ref() { refCount += 1 // check the mode to determine whether the RunLoop is already running. // this could be the case if we're in an Electron (or other embedder) @@ -114,7 +114,7 @@ public enum NodeCFRunLoop { #else public enum NodeCFRunLoop { - @NodeActor public static func ref() {} + public static func ref() {} public static func unref() } From d8bf635617ede45bd872d2d6efc3ff23f0194657 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sun, 22 Jun 2025 17:56:30 -0400 Subject: [PATCH 19/23] fr this time --- test/suites/Test/Test.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/test/suites/Test/Test.swift b/test/suites/Test/Test.swift index 7a8a8ea..d7149c2 100644 --- a/test/suites/Test/Test.swift +++ b/test/suites/Test/Test.swift @@ -1,5 +1,6 @@ import Foundation import NodeAPI +import NodeUV @NodeClass @NodeActor final class File { static let extraProperties: NodeClassPropertyList = [ From ce0dbd1dca522f1bf3a84ef2238f4dfb753159e3 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 28 Jun 2025 02:45:42 -0400 Subject: [PATCH 20/23] Lightweight GCD integration --- Sources/CNodeUV/include/cf_stubs.h | 20 +++++++++++++ Sources/CNodeUV/include/uv_stubs.h | 14 +++++++++ Sources/NodeUV/NodeCFRunLoop.swift | 46 ++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 Sources/CNodeUV/include/cf_stubs.h diff --git a/Sources/CNodeUV/include/cf_stubs.h b/Sources/CNodeUV/include/cf_stubs.h new file mode 100644 index 0000000..777f2ad --- /dev/null +++ b/Sources/CNodeUV/include/cf_stubs.h @@ -0,0 +1,20 @@ +#ifndef cf_stubs_h +#define cf_stubs_h + +#ifdef __APPLE__ + +#include +#include + +mach_port_t _dispatch_get_main_queue_port_4CF(void); +void _dispatch_main_queue_callback_4CF(void); + +static inline struct kevent64_s node_swift_create_event_descriptor_for_portset(mach_port_t port) { + struct kevent64_s ev; + EV_SET64(&ev, port, EVFILT_MACHPORT, EV_ADD|EV_CLEAR, MACH_RCV_MSG, 0, 0, 0, 0); + return ev; +} + +#endif /* __APPLE__ */ + +#endif /* cf_stubs_h */ diff --git a/Sources/CNodeUV/include/uv_stubs.h b/Sources/CNodeUV/include/uv_stubs.h index 74f29f0..34c881f 100644 --- a/Sources/CNodeUV/include/uv_stubs.h +++ b/Sources/CNodeUV/include/uv_stubs.h @@ -13,13 +13,23 @@ typedef enum { typedef enum { UV_ASYNC = 1, + UV_POLL = 8, } uv_handle_type; +enum uv_poll_event { + UV_READABLE = 1, + UV_WRITABLE = 2, + UV_DISCONNECT = 4, + UV_PRIORITIZED = 8 +}; + typedef struct uv_handle_s uv_handle_t; +typedef struct uv_poll_s uv_poll_t; typedef struct uv_loop_s uv_loop_t; typedef struct uv_async_s uv_async_t; typedef void (*uv_async_cb)(uv_async_t *handle); +typedef void (*uv_poll_cb)(uv_poll_t* handle, int status, int events); typedef void (*uv_close_cb)(uv_handle_t *handle); size_t uv_handle_size(uv_handle_type type); @@ -36,6 +46,10 @@ int uv_async_init(uv_loop_t *loop, uv_async_cb async_cb); int uv_async_send(uv_async_t *async); +int uv_poll_init(uv_loop_t *loop, uv_poll_t *handle, int fd); +int uv_poll_start(uv_poll_t *handle, int events, uv_poll_cb cb); +int uv_poll_stop(uv_poll_t *handle); + #endif /* __APPLE__ */ #endif /* uv_stubs_h */ diff --git a/Sources/NodeUV/NodeCFRunLoop.swift b/Sources/NodeUV/NodeCFRunLoop.swift index 076cc7c..d3f51bd 100644 --- a/Sources/NodeUV/NodeCFRunLoop.swift +++ b/Sources/NodeUV/NodeCFRunLoop.swift @@ -38,6 +38,7 @@ public enum NodeCFRunLoop { } private static func setUp() -> (() -> Void) { + #if false // By default, node takes over the main thread with an indefinite uv_run(). // This causes CFRunLoop sources to not be processed (also breaking GCD & MainActor) // since the CFRunLoop never gets ticked. We instead need to flip things on their @@ -108,6 +109,51 @@ public enum NodeCFRunLoop { timer.cancel() uv_close(uvAsync) { UnsafeMutableRawPointer($0)?.deallocate() } } + #else + // This is a slightly less intrusive approach than the above, + // since it doesn't replace the existing uv loop (but it's less + // powerful): + // + // It's fragile to try extracting the CFRunLoop's entire waitset, + // but if we're okay with only supporting the GCD bits, + // we can extract GCD's mach port with _dispatch_get_main_queue_port_4CF + // and integrate it into uv. Inspiration: + // https://gist.github.com/daurnimator/8cc2ef09ad72a5577b66f34957559e47 + // + // It's theoretically possible to support all of CF this way, but it + // would be fragile. Specifically, we can union the GCD port with + // CFRunLoop->_currentMode->_portSet and create a uv_poll from there + // (like we currently do in this code), and also add a uv_timer_t with + // `CFRunLoopGetNextTimerFireDate`. The issue is that extracting the + // port set is extra fragile due to unstable struct layout. + // + // https://github.com/swiftlang/swift-corelibs-foundation/blob/ae61520/Sources/CoreFoundation/CFRunLoop.c#L3014 + + let port = _dispatch_get_main_queue_port_4CF() + var portset = mach_port_t() + mach_port_allocate(mach_task_self_, MACH_PORT_RIGHT_PORT_SET, &portset) + mach_port_insert_member(mach_task_self_, port, portset) + + let fd = kqueue() + let changelist = [node_swift_create_event_descriptor_for_portset(portset)] + var timeout = timespec(tv_sec: 0, tv_nsec: 0) + kevent64(fd, changelist, numericCast(changelist.count), nil, 0, 0, &timeout) + + let uvPoll = OpaquePointer(UnsafeMutableRawPointer.allocate( + byteCount: uv_handle_size(UV_POLL), + alignment: MemoryLayout.alignment + )) + uv_poll_init(uv_default_loop(), uvPoll, fd) + uv_poll_start(uvPoll, CInt(UV_READABLE.rawValue)) { _, _, _ in + _dispatch_main_queue_callback_4CF() + } + + return { + uv_close(uvPoll) { + UnsafeMutableRawPointer($0)?.deallocate() + } + } + #endif } } From e591cdb22386007129e9ee1cbd7da6065982a399 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 28 Jun 2025 14:52:43 -0400 Subject: [PATCH 21/23] tweaks --- Sources/CNodeUV/include/cf_stubs.h | 26 ++++++++++++++++++------ Sources/NodeUV/NodeCFRunLoop.swift | 32 +++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/Sources/CNodeUV/include/cf_stubs.h b/Sources/CNodeUV/include/cf_stubs.h index 777f2ad..7343297 100644 --- a/Sources/CNodeUV/include/cf_stubs.h +++ b/Sources/CNodeUV/include/cf_stubs.h @@ -1,20 +1,34 @@ #ifndef cf_stubs_h #define cf_stubs_h -#ifdef __APPLE__ +#include + +#if defined(__APPLE__) #include #include -mach_port_t _dispatch_get_main_queue_port_4CF(void); -void _dispatch_main_queue_callback_4CF(void); - -static inline struct kevent64_s node_swift_create_event_descriptor_for_portset(mach_port_t port) { +static inline struct kevent64_s node_swift_create_event_descriptor_for_mach_port(mach_port_t port) { struct kevent64_s ev; EV_SET64(&ev, port, EVFILT_MACHPORT, EV_ADD|EV_CLEAR, MACH_RCV_MSG, 0, 0, 0, 0); return ev; } -#endif /* __APPLE__ */ +typedef mach_port_t dispatch_runloop_handle_t; + +#elif defined(__linux__) +typedef int dispatch_runloop_handle_t; +#elif defined(__unix__) +typedef uint64_t dispatch_runloop_handle_t; +#elif defined(_WIN32) +typedef void *dispatch_runloop_handle_t; +#else +#define NODE_SWIFT_NO_GCD_RUNLOOP +#endif + +#ifndef NODE_SWIFT_NO_GCD_RUNLOOP +dispatch_runloop_handle_t _dispatch_get_main_queue_port_4CF(void); +void _dispatch_main_queue_callback_4CF(void); +#endif #endif /* cf_stubs_h */ diff --git a/Sources/NodeUV/NodeCFRunLoop.swift b/Sources/NodeUV/NodeCFRunLoop.swift index d3f51bd..8ba5cea 100644 --- a/Sources/NodeUV/NodeCFRunLoop.swift +++ b/Sources/NodeUV/NodeCFRunLoop.swift @@ -129,15 +129,7 @@ public enum NodeCFRunLoop { // // https://github.com/swiftlang/swift-corelibs-foundation/blob/ae61520/Sources/CoreFoundation/CFRunLoop.c#L3014 - let port = _dispatch_get_main_queue_port_4CF() - var portset = mach_port_t() - mach_port_allocate(mach_task_self_, MACH_PORT_RIGHT_PORT_SET, &portset) - mach_port_insert_member(mach_task_self_, port, portset) - - let fd = kqueue() - let changelist = [node_swift_create_event_descriptor_for_portset(portset)] - var timeout = timespec(tv_sec: 0, tv_nsec: 0) - kevent64(fd, changelist, numericCast(changelist.count), nil, 0, 0, &timeout) + let fd = getDispatchFileDescriptor() let uvPoll = OpaquePointer(UnsafeMutableRawPointer.allocate( byteCount: uv_handle_size(UV_POLL), @@ -155,6 +147,28 @@ public enum NodeCFRunLoop { } #endif } + + // TODO: error handling + private static func getDispatchFileDescriptor() -> CInt { + let port = _dispatch_get_main_queue_port_4CF() + + #if canImport(Darwin) + var portset = mach_port_t() + mach_port_allocate(mach_task_self_, MACH_PORT_RIGHT_PORT_SET, &portset) + mach_port_insert_member(mach_task_self_, port, portset) + + let fd = kqueue() + let changelist = [node_swift_create_event_descriptor_for_mach_port(portset)] + var timeout = timespec(tv_sec: 0, tv_nsec: 0) + kevent64(fd, changelist, numericCast(changelist.count), nil, 0, 0, &timeout) + + return fd + #elseif os(Linux) + // TODO: drop the top level canImport guard + // also support BSD (and Windows if possible) + return CInt(port) + #endif + } } #else From 8c5c835e35f1834b47e60c5440b7a88be9e5573e Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 28 Jun 2025 14:52:51 -0400 Subject: [PATCH 22/23] xcscheme --- .../xcschemes/node-swift-Package.xcscheme | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/node-swift-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/node-swift-Package.xcscheme index 8011d24..05f2725 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/node-swift-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/node-swift-Package.xcscheme @@ -49,6 +49,20 @@ ReferencedContainer = "container:"> + + + + Date: Fri, 4 Jul 2025 04:14:04 -0400 Subject: [PATCH 23/23] Fix _dispatch_main_queue_callback_4CF sig --- Sources/CNodeUV/include/cf_stubs.h | 2 +- Sources/NodeUV/NodeCFRunLoop.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CNodeUV/include/cf_stubs.h b/Sources/CNodeUV/include/cf_stubs.h index 7343297..a8a302e 100644 --- a/Sources/CNodeUV/include/cf_stubs.h +++ b/Sources/CNodeUV/include/cf_stubs.h @@ -28,7 +28,7 @@ typedef void *dispatch_runloop_handle_t; #ifndef NODE_SWIFT_NO_GCD_RUNLOOP dispatch_runloop_handle_t _dispatch_get_main_queue_port_4CF(void); -void _dispatch_main_queue_callback_4CF(void); +void _dispatch_main_queue_callback_4CF(void *unused); #endif #endif /* cf_stubs_h */ diff --git a/Sources/NodeUV/NodeCFRunLoop.swift b/Sources/NodeUV/NodeCFRunLoop.swift index 8ba5cea..b3ad0b2 100644 --- a/Sources/NodeUV/NodeCFRunLoop.swift +++ b/Sources/NodeUV/NodeCFRunLoop.swift @@ -137,7 +137,7 @@ public enum NodeCFRunLoop { )) uv_poll_init(uv_default_loop(), uvPoll, fd) uv_poll_start(uvPoll, CInt(UV_READABLE.rawValue)) { _, _, _ in - _dispatch_main_queue_callback_4CF() + _dispatch_main_queue_callback_4CF(nil) } return {