diff --git a/Libraries/LibCore/CMakeLists.txt b/Libraries/LibCore/CMakeLists.txt index 2522634788f..dfb08110aec 100644 --- a/Libraries/LibCore/CMakeLists.txt +++ b/Libraries/LibCore/CMakeLists.txt @@ -114,3 +114,20 @@ endif() if (ANDROID) target_link_libraries(LibCore PRIVATE log) endif() + +if (ENABLE_SWIFT) + set(SWIFT_EXCLUDE_HEADERS "SocketAddressWindows.h") + if(WIN32) + list(APPEND SWIFT_EXCLUDE_HEADERS "EventLoopImplementationUnix.h") + else() + list(APPEND SWIFT_EXCLUDE_HEADERS "EventLoopImplementationWindows.h") + endif() + + generate_clang_module_map(LibCore EXCLUDE_FILES ${SWIFT_EXCLUDE_HEADERS}) + target_sources(LibCore PRIVATE + EventSwift.mm + EventLoopExecutor.swift) + set_source_files_properties(EventSwift.mm PRIVATE PROPERTIES COMPILE_FLAGS -fobjc-arc) + target_link_libraries(LibCore PRIVATE AK) + add_swift_target_properties(LibCore LAGOM_LIBRARIES AK) +endif() diff --git a/Libraries/LibCore/EventLoop.h b/Libraries/LibCore/EventLoop.h index 3de9c91bf54..9240d6da8e6 100644 --- a/Libraries/LibCore/EventLoop.h +++ b/Libraries/LibCore/EventLoop.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,10 @@ class ThreadEventQueue; // - Quit events, i.e. the event loop should exit. // Any event that the event loop needs to wait on or needs to repeatedly handle is stored in a handle, e.g. s_timers. class EventLoop { + AK_MAKE_NONMOVABLE(EventLoop); + AK_MAKE_NONCOPYABLE(EventLoop); + +private: friend struct EventLoopPusher; public: @@ -90,7 +95,7 @@ public: private: NonnullOwnPtr m_impl; -}; +} SWIFT_UNSAFE_REFERENCE; void deferred_invoke(ESCAPING Function); diff --git a/Libraries/LibCore/EventLoopExecutor.swift b/Libraries/LibCore/EventLoopExecutor.swift new file mode 100644 index 00000000000..d678d387371 --- /dev/null +++ b/Libraries/LibCore/EventLoopExecutor.swift @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +import AK +@_exported import CoreCxx + +extension Core.EventLoop: Equatable { + func deferred_invoke(_ task: @escaping () -> Void) { + Core.deferred_invoke_block(self, task) + } + + public static func == (lhs: Core.EventLoop, rhs: Core.EventLoop) -> Bool { + Unmanaged.passUnretained(lhs).toOpaque() == Unmanaged.passUnretained(rhs).toOpaque() + } +} + +public class EventLoopExecutor: SerialExecutor, TaskExecutor, @unchecked Sendable { + nonisolated private let eventLoop: Core.EventLoop + + public init() { + eventLoop = Core.EventLoop.current() + } + + public init(eventLoop: Core.EventLoop) { + self.eventLoop = eventLoop + } + + public nonisolated func enqueue(_ job: consuming ExecutorJob) { + let job = UnownedJob(job) + eventLoop.deferred_invoke { [self, job] in + job.runSynchronously( + isolatedTo: self.asUnownedSerialExecutor(), + taskExecutor: self.asUnownedTaskExecutor()) + } + } + + public func checkIsolated() { + precondition(Core.EventLoop.current() == eventLoop) + } +} + +public protocol EventLoopActor: Actor { + nonisolated var executor: EventLoopExecutor { get } // impl with a let +} + +extension EventLoopActor { + public nonisolated var unownedExecutor: UnownedSerialExecutor { + executor.asUnownedSerialExecutor() + } +} diff --git a/Libraries/LibCore/EventSwift.h b/Libraries/LibCore/EventSwift.h new file mode 100644 index 00000000000..e1635e4e63f --- /dev/null +++ b/Libraries/LibCore/EventSwift.h @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2025, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Core { + +void deferred_invoke_block(EventLoop& event_loop, void (^invokee)(void)); + +} diff --git a/Libraries/LibCore/EventSwift.mm b/Libraries/LibCore/EventSwift.mm new file mode 100644 index 00000000000..871d8bf6bda --- /dev/null +++ b/Libraries/LibCore/EventSwift.mm @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +#if !__has_feature(objc_arc) +# error "This file requires ARC" +#endif + +namespace Core { +void deferred_invoke_block(EventLoop& event_loop, void (^invokee)(void)) +{ + event_loop.deferred_invoke([invokee = move(invokee)] { + invokee(); + }); +} + +} diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index 61834856034..11bdb1b4b0c 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -554,6 +554,24 @@ if (BUILD_TESTING) lagom_test(../../Tests/LibCore/TestLibCoreDateTime.cpp LIBS LibUnicode) + if (ENABLE_SWIFT) + find_package(SwiftTesting REQUIRED) + + add_executable(TestCoreSwift + ../../Tests/LibCore/TestEventLoopActor.swift + ../../Tests/LibCore/TestEventLoop.cpp + ) + + # FIXME: Swift doesn't seem to like object libraries for @main + target_sources(TestCoreSwift PRIVATE ../../Tests/Resources/SwiftTestMain.swift) + + set_target_properties(TestCoreSwift PROPERTIES SUFFIX .swift-testing) + target_include_directories(TestCoreSwift PRIVATE ../../Tests/LibCore) + target_link_libraries(TestCoreSwift PRIVATE AK LibCore SwiftTesting::SwiftTesting) + add_test(NAME TestCoreSwift COMMAND TestCoreSwift) + endif() + + # RegexLibC test POSIX and contains many Serenity extensions # It is therefore not reasonable to run it on Lagom, and we only run the Regex test lagom_test(../../Tests/LibRegex/Regex.cpp LIBS LibRegex WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../Tests/LibRegex) diff --git a/Tests/LibCore/TestEventLoop.cpp b/Tests/LibCore/TestEventLoop.cpp new file mode 100644 index 00000000000..68ad2076825 --- /dev/null +++ b/Tests/LibCore/TestEventLoop.cpp @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "TestEventLoop.h" +#include +#include + +void install_thread_local_event_loop() +{ + thread_local OwnPtr s_thread_local_event_loop = nullptr; + if (!s_thread_local_event_loop) + s_thread_local_event_loop = make(); +} diff --git a/Tests/LibCore/TestEventLoop.h b/Tests/LibCore/TestEventLoop.h new file mode 100644 index 00000000000..793b1a77b02 --- /dev/null +++ b/Tests/LibCore/TestEventLoop.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2025, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +void install_thread_local_event_loop(); diff --git a/Tests/LibCore/TestEventLoopActor.swift b/Tests/LibCore/TestEventLoopActor.swift new file mode 100644 index 00000000000..ac93df0bbb1 --- /dev/null +++ b/Tests/LibCore/TestEventLoopActor.swift @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +import AK +import Core +import CoreTesting +import Testing + +actor TestEventLoopActor: EventLoopActor { + nonisolated public let executor: EventLoopExecutor + + init() { + install_thread_local_event_loop() + executor = EventLoopExecutor() + } + + nonisolated func submit(action: @escaping @Sendable () async -> Void) { + Task(executorPreference: self.executor) { + await action() + } + } +} + +@Suite +struct TestEventLoop { + @Test + func testEventLoopActor() async { + // Creates an executor around EventLoop::current() + let actor = TestEventLoopActor() + + let ev = Core.EventLoop.current() + print("Event loop at \(Unmanaged.passUnretained(ev).toOpaque())") + + let (stream, continuation) = AsyncStream.makeStream() + var iterator = stream.makeAsyncIterator() + + actor.submit { + #expect(ev == Core.EventLoop.current(), "Closure is executed on event loop") + print("Hello from event loop at \(Unmanaged.passUnretained(Core.EventLoop.current()).toOpaque())") + + continuation.yield(42) + } + + actor.submit { + #expect(ev == Core.EventLoop.current(), "Closure is executed on event loop") + Core.EventLoop.current().quit(4) + + continuation.yield(1234) + continuation.finish() + } + + let rc = ev.exec() + #expect(rc == 4) + // Values not available until event loop has processed tasks + #expect(await iterator.next() == 42) + #expect(await iterator.next() == 1234) + + #expect(ev == Core.EventLoop.current(), "Event loop exists until end of function") + } +} diff --git a/Tests/LibCore/module.modulemap b/Tests/LibCore/module.modulemap new file mode 100644 index 00000000000..208cbdc33e2 --- /dev/null +++ b/Tests/LibCore/module.modulemap @@ -0,0 +1,5 @@ +module CoreTesting { + header "TestEventLoop.h" + requires cplusplus + export * +}