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