LibCore: Add swift bindings for EventLoop as an Executor and Actor

This commit is contained in:
Andrew Kaster 2025-03-09 13:43:51 -06:00 committed by Andrew Kaster
commit c8787e6a9f
Notes: github-actions[bot] 2025-03-16 03:52:13 +00:00
10 changed files with 224 additions and 1 deletions

View file

@ -114,3 +114,20 @@ endif()
if (ANDROID) if (ANDROID)
target_link_libraries(LibCore PRIVATE log) target_link_libraries(LibCore PRIVATE log)
endif() 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()

View file

@ -12,6 +12,7 @@
#include <AK/Function.h> #include <AK/Function.h>
#include <AK/Noncopyable.h> #include <AK/Noncopyable.h>
#include <AK/NonnullOwnPtr.h> #include <AK/NonnullOwnPtr.h>
#include <AK/Swift.h>
#include <AK/Time.h> #include <AK/Time.h>
#include <LibCore/Event.h> #include <LibCore/Event.h>
#include <LibCore/Forward.h> #include <LibCore/Forward.h>
@ -40,6 +41,10 @@ class ThreadEventQueue;
// - Quit events, i.e. the event loop should exit. // - 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. // 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 { class EventLoop {
AK_MAKE_NONMOVABLE(EventLoop);
AK_MAKE_NONCOPYABLE(EventLoop);
private:
friend struct EventLoopPusher; friend struct EventLoopPusher;
public: public:
@ -90,7 +95,7 @@ public:
private: private:
NonnullOwnPtr<EventLoopImplementation> m_impl; NonnullOwnPtr<EventLoopImplementation> m_impl;
}; } SWIFT_UNSAFE_REFERENCE;
void deferred_invoke(ESCAPING Function<void()>); void deferred_invoke(ESCAPING Function<void()>);

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2025, Andrew Kaster <andrew@ladybird.org>
*
* 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()
}
}

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2025, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibCore/Forward.h>
namespace Core {
void deferred_invoke_block(EventLoop& event_loop, void (^invokee)(void));
}

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2025, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/EventLoop.h>
#include <LibCore/EventSwift.h>
#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();
});
}
}

View file

@ -554,6 +554,24 @@ if (BUILD_TESTING)
lagom_test(../../Tests/LibCore/TestLibCoreDateTime.cpp LIBS LibUnicode) 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 <regex.h> and contains many Serenity extensions # RegexLibC test POSIX <regex.h> and contains many Serenity extensions
# It is therefore not reasonable to run it on Lagom, and we only run the Regex test # 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) lagom_test(../../Tests/LibRegex/Regex.cpp LIBS LibRegex WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../Tests/LibRegex)

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2025, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "TestEventLoop.h"
#include <AK/OwnPtr.h>
#include <LibCore/EventLoop.h>
void install_thread_local_event_loop()
{
thread_local OwnPtr<Core::EventLoop> s_thread_local_event_loop = nullptr;
if (!s_thread_local_event_loop)
s_thread_local_event_loop = make<Core::EventLoop>();
}

View file

@ -0,0 +1,9 @@
/*
* Copyright (c) 2025, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
void install_thread_local_event_loop();

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2025, Andrew Kaster <andrew@ladybird.org>
*
* 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<Int>.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")
}
}

View file

@ -0,0 +1,5 @@
module CoreTesting {
header "TestEventLoop.h"
requires cplusplus
export *
}