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:">
+
+
+
+
+
+#if defined(__APPLE__)
+
+#include
+#include
+
+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;
+}
+
+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 *unused);
+#endif
+
+#endif /* cf_stubs_h */
diff --git a/Sources/CNodeUV/include/uv_stubs.h b/Sources/CNodeUV/include/uv_stubs.h
new file mode 100644
index 0000000..34c881f
--- /dev/null
+++ b/Sources/CNodeUV/include/uv_stubs.h
@@ -0,0 +1,55 @@
+#ifndef uv_stubs_h
+#define uv_stubs_h
+
+#ifdef __APPLE__
+
+#include
+
+typedef enum {
+ UV_RUN_DEFAULT = 0,
+ UV_RUN_ONCE,
+ UV_RUN_NOWAIT
+} uv_run_mode;
+
+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);
+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 *);
+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,
+ 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
new file mode 100644
index 0000000..b3ad0b2
--- /dev/null
+++ b/Sources/NodeUV/NodeCFRunLoop.swift
@@ -0,0 +1,181 @@
+#if canImport(Darwin)
+
+import Foundation
+import CNodeUV
+
+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...),
+ // causing MainActor.assumeIsolated to abort.
+ private nonisolated(unsafe) static var cancelHandlers: (() -> Void)?
+ private nonisolated(unsafe) static var refCount = 0
+
+ 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)
+ // context. only set up our own loop if it's not running.
+ if refCount == 1 && RunLoop.main.currentMode == nil {
+ cancelHandlers = setUp()
+ }
+ }
+
+ public static func unref() {
+ if Thread.isMainThread {
+ _unref()
+ } else {
+ Task { @MainActor in _unref() }
+ }
+ }
+
+ private static func _unref() {
+ refCount -= 1
+ if refCount == 0 {
+ cancelHandlers?()
+ cancelHandlers = nil
+ }
+ }
+
+ 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
+ // 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)
+ let timer = DispatchSource.makeTimerSource(queue: .main)
+
+ nonisolated(unsafe) 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()
+ }
+ }
+
+ reader.setEventHandler { wakeUpUV() }
+ timer.setEventHandler { wakeUpUV() }
+ // bootstrap
+ DispatchQueue.main.async { wakeUpUV() }
+
+ // 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 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
+ // uv tick. This is hacky because we eventually end up with
+ // a callstack that looks like:
+ // 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
+ // 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
+ while NodeCFRunLoop.refCount > 0 && RunLoop.main.run(mode: .default, before: .distantFuture) {}
+ }
+ uv_async_send(uvAsync)
+
+ return {
+ reader.cancel()
+ 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 fd = getDispatchFileDescriptor()
+
+ 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(nil)
+ }
+
+ return {
+ uv_close(uvPoll) {
+ UnsafeMutableRawPointer($0)?.deallocate()
+ }
+ }
+ #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
+
+public enum NodeCFRunLoop {
+ public static func ref() {}
+ public static func unref()
+}
+
+#endif
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)",
diff --git a/test/suites/Test/Test.swift b/test/suites/Test/Test.swift
index d4355f2..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 = [
@@ -56,6 +57,21 @@ 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 {
+ NodeCFRunLoop.ref()
+ Task { @NodeActor in
+ try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
+ NodeCFRunLoop.unref()
+ }
+ return ["File": File.deferredConstructor]
+}
diff --git a/test/suites/Test/index.js b/test/suites/Test/index.js
index f6d2672..59576b7 100644
--- a/test/suites/Test/index.js
+++ b/test/suites/Test/index.js
@@ -7,6 +7,16 @@ 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();
+ console.log("Main actor message:", msg);
+})()
+
let err = "";
try {
file.contents