From 6b00f8fc0f3326b07cab0a83a48358d84420b79f Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 18 Jun 2025 15:43:17 +0100 Subject: [PATCH 01/13] Add `Clock` subscription type to `WASIAbi` The change declares a clock subscription type in according with the WebAssembly System Interface standard ABI, which is required for testing clocks and timers in Embedded Swift for WebAssembly. The ABI for this type is specified publicly in corresponding ABI documentation: https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#subscription_clock rdar://149935761 --- Sources/WASI/WASI.swift | 42 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Sources/WASI/WASI.swift b/Sources/WASI/WASI.swift index 631a3325..5aa6c36e 100644 --- a/Sources/WASI/WASI.swift +++ b/Sources/WASI/WASI.swift @@ -429,6 +429,48 @@ enum WASIAbi { case END = 2 } + struct Clock: GuestPointee { + struct Flags: OptionSet, GuestPrimitivePointee { + let rawValue: UInt16 + + static let isAbsoluteTime = Self(rawValue: 1) + } + + let id: ClockId + let timeout: Timestamp + let precision: Timestamp + let flags: Flags + + static let sizeInGuest: UInt32 = 32 + static let alignInGuest: UInt32 = max(ClockId.alignInGuest, Timestamp.alignInGuest, Flags.alignInGuest) + + static func readFromGuest(_ pointer: UnsafeGuestRawPointer) -> Self { + var pointer = pointer + return .init( + id: .readFromGuest(&pointer), + timeout: .readFromGuest(&pointer), + precision: .readFromGuest(&pointer), + flags: .readFromGuest(&pointer) + ) + } + + static func writeToGuest(at pointer: UnsafeGuestRawPointer, value: Self) { + var pointer = pointer + ClockId.writeToGuest(at: &pointer, value: value.id) + Timestamp.writeToGuest(at: &pointer, value: value.timeout) + Timestamp.writeToGuest(at: &pointer, value: value.precision) + Flags.writeToGuest(at: &pointer, value: value.flags) + } + } + + enum EventType: UInt8 { + case clock + case fdRead + case fdWrite + } + + typealias UserData = UInt64 + enum ClockId: UInt32 { /// The clock measuring real time. Time value zero corresponds with /// 1970-01-01T00:00:00Z. From fabd82b34e75bc04d00c765212a0d5f36c6c9f28 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 18 Jun 2025 15:44:36 +0100 Subject: [PATCH 02/13] Add `Subscription` type to `WASIAbi` Testing Swift Concurrency with WebAssembly System Interface requires a missing `subscription` record per the corresponding standard specification: https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#-subscription-record This change adds a subscription union type, with the ABI described in this standard document. --- Sources/WASI/WASI.swift | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/Sources/WASI/WASI.swift b/Sources/WASI/WASI.swift index 5aa6c36e..92acb71c 100644 --- a/Sources/WASI/WASI.swift +++ b/Sources/WASI/WASI.swift @@ -471,6 +471,52 @@ enum WASIAbi { typealias UserData = UInt64 + struct Subscription: Equatable { + enum Union: Equatable, GuestPointee { + case clock(Clock) + case fdRead(Fd) + case fdWrite(Fd) + + static let sizeInGuest: UInt32 = 40 + static let alignInGuest: UInt32 = max(Clock.alignInGuest, Fd.alignInGuest) + + static func readFromGuest(_ pointer: UnsafeGuestRawPointer) -> Self { + var pointer = pointer + let tag = UInt8.readFromGuest(&pointer) + + switch tag { + case 0: + return .clock(.readFromGuest(&pointer)) + + case 1: + return .fdRead(.readFromGuest(&pointer)) + + case 2: + return .fdWrite(.readFromGuest(&pointer)) + + default: + // FIXME: should this throw? + fatalError() + } + } + + static func writeToGuest(at pointer: UnsafeGuestRawPointer, value: Self) { + var pointer = pointer + switch value { + case .clock(let clock): + UInt8.writeToGuest(at: &pointer, value: 0) + Clock.writeToGuest(at: &pointer, value: clock) + case .fdRead(let fd): + UInt8.writeToGuest(at: &pointer, value: 1) + Fd.writeToGuest(at: &pointer, value: fd) + case .fdWrite(let fd): + UInt8.writeToGuest(at: &pointer, value: 2) + Fd.writeToGuest(at: &pointer, value: fd) + } + } + } + } + enum ClockId: UInt32 { /// The clock measuring real time. Time value zero corresponds with /// 1970-01-01T00:00:00Z. From ac56b0da40087c5fe8377c7a8e6bfa6dd1c1455e Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 18 Jun 2025 15:45:29 +0100 Subject: [PATCH 03/13] Add `Event` type to `WASIAbi` To test Swift Concurrency with WebAssembly System Interface, an Event API is missing from WasmKit runtime, as specified in the standard specification https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#-event-record This change adds Event with the ABI as prescribed in the standard, allowing Swift Concurrency tests to utilize timer events. --- Sources/WASI/WASI.swift | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Sources/WASI/WASI.swift b/Sources/WASI/WASI.swift index 92acb71c..1c5ac1ef 100644 --- a/Sources/WASI/WASI.swift +++ b/Sources/WASI/WASI.swift @@ -517,6 +517,53 @@ enum WASIAbi { } } + struct Event: Equatable, GuestPointee { + struct FdReadWrite: Equatable, GuestPointee { + struct Flags: OptionSet, GuestPointee { + let rawValue: UInt16 + static let hangup = Self(rawValue: 1) + } + let nBytes: FileSize + let flags: Flags + static let sizeInGuest: UInt32 = 16 + static let alignInGuest: UInt32 = 8 + + static func readFromGuest(_ pointer: UnsafeGuestRawPointer) -> Self { + var pointer = pointer + return .init(nBytes: FileSize.readFromGuest(&pointer), flags: Flags.readFromGuest(&pointer)) + } + static func writeToGuest(at pointer: UnsafeGuestRawPointer, value: Self) { + var pointer = pointer + FileSize.writeToGuest(at: &pointer, value: value.nBytes) + Flags.writeToGuest(at: &pointer, value: value.flags) + } + } + + let userData: UserData + let error: Errno + let eventType: EventType + let fdReadWrite: FdReadWrite + static let sizeInGuest: UInt32 = 32 + static let alignInGuest: UInt32 = 8 + + static func readFromGuest(_ pointer: UnsafeGuestRawPointer) -> Self { + var pointer = pointer + return .init( + userData: .readFromGuest(&pointer), + error: .readFromGuest(&pointer), + eventType: .readFromGuest(&pointer), + fdReadWrite: .readFromGuest(&pointer) + ) + } + static func writeToGuest(at pointer: UnsafeGuestRawPointer, value: Self) { + var pointer = pointer + UserData.writeToGuest(at: &pointer, value: value.userData) + Errno.writeToGuest(at: &pointer, value: value.error) + EventType.writeToGuest(at: &pointer, value: value.eventType) + FdReadWrite.writeToGuest(at: &pointer, value: value.fdReadWrite) + } + } + enum ClockId: UInt32 { /// The clock measuring real time. Time value zero corresponds with /// 1970-01-01T00:00:00Z. From 36060a0b89a2cc3a28103fb230d5740afa7d281d Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 18 Jun 2025 15:47:02 +0100 Subject: [PATCH 04/13] Add `poll_oneoff` stub with new ABI types Testing Swift Concurrency for WebAssembly requires presence of `epoll_oneoff`, which is absent in WasmKit. This change provides a stub according to the ABI specified in WebAssembly System Interface standard https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#poll_oneoff The stub has no concrete implementation, but provides placeholders that map to clock and file descriptor value that can be polled as specified in the standard. --- Sources/WASI/WASI.swift | 47 +++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/Sources/WASI/WASI.swift b/Sources/WASI/WASI.swift index 1c5ac1ef..e6c1c8e2 100644 --- a/Sources/WASI/WASI.swift +++ b/Sources/WASI/WASI.swift @@ -211,9 +211,8 @@ protocol WASI { /// Concurrently poll for the occurrence of a set of events. func poll_oneoff( - subscriptions: UnsafeGuestRawPointer, - events: UnsafeGuestRawPointer, - numberOfSubscriptions: WASIAbi.Size + subscriptions: UnsafeGuestBufferPointer, + events: UnsafeGuestBufferPointer ) throws -> WASIAbi.Size /// Write high-quality random data into a buffer. @@ -221,7 +220,7 @@ protocol WASI { } enum WASIAbi { - enum Errno: UInt32, Error { + enum Errno: UInt32, Error, GuestPointee { /// No error occurred. System call completed successfully. case SUCCESS = 0 /// Argument list too long. @@ -952,7 +951,6 @@ public struct WASIHostModule { extension WASI { var _hostModules: [String: WASIHostModule] { let unimplementedFunctionTypes: [String: FunctionType] = [ - "poll_oneoff": .init(parameters: [.i32, .i32, .i32, .i32], results: [.i32]), "proc_raise": .init(parameters: [.i32], results: [.i32]), "sched_yield": .init(parameters: [], results: [.i32]), "sock_accept": .init(parameters: [.i32, .i32, .i32], results: [.i32]), @@ -1493,6 +1491,24 @@ extension WASI { } } + preview1["poll_oneoff"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let subscriptionsBaseAddress = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[0].i32) + let eventsBaseAddress = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) + let size = try self.poll_oneoff( + subscriptions: .init(baseAddress: subscriptionsBaseAddress, count: arguments[2].i32), + events: .init(baseAddress: eventsBaseAddress, count: arguments[2].i32) + ) + buffer.withUnsafeMutableBufferPointer(offset: .init(arguments[3].i32), count: MemoryLayout.size) { raw in + raw.withMemoryRebound(to: UInt32.self) { rebound in rebound[0] = size.littleEndian } + } + + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + } + return [ "wasi_snapshot_preview1": WASIHostModule(functions: preview1) ] @@ -1977,11 +1993,24 @@ public class WASIBridgeToHost: WASI { } func poll_oneoff( - subscriptions: UnsafeGuestRawPointer, - events: UnsafeGuestRawPointer, - numberOfSubscriptions: WASIAbi.Size + subscriptions: UnsafeGuestBufferPointer, + events: UnsafeGuestBufferPointer ) throws -> WASIAbi.Size { - throw WASIAbi.Errno.ENOTSUP + for subscription in subscriptions { + switch subscription.union { + case .clock: + throw WASIAbi.Errno.ENOTSUP + + case .fdRead(let fd), .fdWrite(let fd): + guard case let .file(entry) = self.fdTable[fd] else { + throw WASIAbi.Errno.EBADF + + } + throw WASIAbi.Errno.ENOTSUP + } + } + + return 0 } func random_get(buffer: UnsafeGuestPointer, length: WASIAbi.Size) { From 498d0beb37d75dd4772bafe1135336abb90c2e72 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 23 Jun 2025 14:46:28 +0100 Subject: [PATCH 05/13] Add `Equatable` and `GuestPointee` conformances To support Swift Concurrency tested in WebAssembly System Interface environment, `Clock` and `Subscription` types should conform to `GuestPointee` types, which provides size and alignment for correct ABI implementation. This fixes current build issues, where these types didn't have correct size and alignment specified. --- Sources/WASI/WASI.swift | 26 +++++++++++++++++++++----- Sources/WasmTypes/GuestMemory.swift | 10 +++++++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Sources/WASI/WASI.swift b/Sources/WASI/WASI.swift index e6c1c8e2..b8b010bb 100644 --- a/Sources/WASI/WASI.swift +++ b/Sources/WASI/WASI.swift @@ -428,8 +428,8 @@ enum WASIAbi { case END = 2 } - struct Clock: GuestPointee { - struct Flags: OptionSet, GuestPrimitivePointee { + struct Clock: Equatable, GuestPointee { + struct Flags: OptionSet, GuestPointee { let rawValue: UInt16 static let isAbsoluteTime = Self(rawValue: 1) @@ -462,7 +462,7 @@ enum WASIAbi { } } - enum EventType: UInt8 { + enum EventType: UInt8, GuestPointee { case clock case fdRead case fdWrite @@ -470,7 +470,7 @@ enum WASIAbi { typealias UserData = UInt64 - struct Subscription: Equatable { + struct Subscription: Equatable, GuestPointee { enum Union: Equatable, GuestPointee { case clock(Clock) case fdRead(Fd) @@ -514,6 +514,22 @@ enum WASIAbi { } } } + + let userData: UserData + let union: Union + static var sizeInGuest: UInt32 = 48 + static var alignInGuest: UInt32 = max(UserData.alignInGuest, Union.alignInGuest) + + static func readFromGuest(_ pointer: UnsafeGuestRawPointer) -> Self { + var pointer = pointer + return .init(userData: .readFromGuest(&pointer), union: .readFromGuest(&pointer)) + } + + static func writeToGuest(at pointer: UnsafeGuestRawPointer, value: Self) { + var pointer = pointer + UserData.writeToGuest(at: &pointer, value: value.userData) + Union.writeToGuest(at: &pointer, value: value.union) + } } struct Event: Equatable, GuestPointee { @@ -563,7 +579,7 @@ enum WASIAbi { } } - enum ClockId: UInt32 { + enum ClockId: UInt32, GuestPointee { /// The clock measuring real time. Time value zero corresponds with /// 1970-01-01T00:00:00Z. case REALTIME = 0 diff --git a/Sources/WasmTypes/GuestMemory.swift b/Sources/WasmTypes/GuestMemory.swift index b524ef89..33364c0b 100644 --- a/Sources/WasmTypes/GuestMemory.swift +++ b/Sources/WasmTypes/GuestMemory.swift @@ -38,7 +38,15 @@ extension GuestPrimitivePointee { } /// Auto implementation of ``GuestPointee`` for ``RawRepresentable`` types -extension GuestPrimitivePointee where Self: RawRepresentable, Self.RawValue: GuestPointee { +extension GuestPointee where Self: RawRepresentable, Self.RawValue: GuestPointee { + public static var sizeInGuest: UInt32 { + RawValue.sizeInGuest + } + + public static var alignInGuest: UInt32 { + RawValue.alignInGuest + } + /// Reads a value of RawValue type and constructs a value of Self type public static func readFromGuest(_ pointer: UnsafeGuestRawPointer) -> Self { Self(rawValue: .readFromGuest(pointer))! From c5b4506cfc578bc827f9d2855bcd6dc67eac62ad Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 23 Jun 2025 14:47:16 +0100 Subject: [PATCH 06/13] Add tests for new ABI types memory layout Types like Clock, Subscription, and Event that cover WebAssembly System Interface should have their size, alignment, and load/store functionality tested for conformance with the ABI specified in the standard https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#-subscription_clock-record This change adds corresponding tests that cover this. --- Tests/WASITests/WASITests.swift | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Tests/WASITests/WASITests.swift b/Tests/WASITests/WASITests.swift index 68ef114b..c154da54 100644 --- a/Tests/WASITests/WASITests.swift +++ b/Tests/WASITests/WASITests.swift @@ -1,5 +1,7 @@ import XCTest +import WasmKit +import WasmTypes @testable import WASI final class WASITests: XCTestCase { @@ -134,4 +136,37 @@ final class WASITests: XCTestCase { XCTAssertEqual(error, .ELOOP) } } + + func testWASIAbi() throws { + let engine = Engine() + let store = Store(engine: engine) + let memory = try Memory(store: store, type: .init(min: 1)) + + // Test union size and alignment end-to-end + let start = UnsafeGuestRawPointer(memorySpace: memory, offset: 0) + var pointer = start + let read = WASIAbi.Subscription.Union.fdRead(.init(0)) + let write = WASIAbi.Subscription.Union.fdWrite(.init(0)) + let clock = WASIAbi.Subscription.Union.clock(.init(id: .REALTIME, timeout: 42, precision: 0, flags: [])) + let event = WASIAbi.Event(userData: 3, error: .EIO, eventType: .fdRead, fdReadWrite: .init(nBytes: 37, flags: [.hangup])) + WASIAbi.Subscription.writeToGuest(at: &pointer, value: .init(userData: 1, union: read)) + XCTAssertEqual(pointer.offset, 48) + WASIAbi.Subscription.writeToGuest(at: &pointer, value: .init(userData: 2, union: write)) + XCTAssertEqual(pointer.offset, 48 * 2) + WASIAbi.Subscription.writeToGuest(at: &pointer, value: .init(userData: 3, union: clock)) + XCTAssertEqual(pointer.offset, 48 * 3) + WASIAbi.Event.writeToGuest(at: &pointer, value: event) + XCTAssertEqual(pointer.offset, 48 * 3 + 32) + + // Test that reading back yields same result + pointer = start + XCTAssertEqual(WASIAbi.Subscription.readFromGuest(&pointer), .init(userData: 1, union: read)) + XCTAssertEqual(pointer.offset, 48) + XCTAssertEqual(WASIAbi.Subscription.readFromGuest(&pointer), .init(userData: 2, union: write)) + XCTAssertEqual(pointer.offset, 48 * 2) + XCTAssertEqual(WASIAbi.Subscription.readFromGuest(&pointer), .init(userData: 3, union: clock)) + XCTAssertEqual(pointer.offset, 48 * 3) + XCTAssertEqual(WASIAbi.Event.readFromGuest(&pointer), event) + XCTAssertEqual(pointer.offset, 48 * 3 + 32) + } } From f9d647b5574cb2f18b0f966336dc6468366478f8 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 23 Jun 2025 15:58:15 +0100 Subject: [PATCH 07/13] Fix formatting in `WASITests.swift` --- Tests/WASITests/WASITests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/WASITests/WASITests.swift b/Tests/WASITests/WASITests.swift index c154da54..4f0f9c00 100644 --- a/Tests/WASITests/WASITests.swift +++ b/Tests/WASITests/WASITests.swift @@ -1,7 +1,7 @@ -import XCTest - import WasmKit import WasmTypes +import XCTest + @testable import WASI final class WASITests: XCTestCase { From 63574ab65588040603821810cab8686a247118d6 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 15:50:52 +0100 Subject: [PATCH 08/13] Add `Poll.swift` Implementation of `poll_oneoff` in WebAssembly System Interface standard (https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#poll_oneoff) can be approximated in operating systems implementing POSIX standard with `poll` (https://www.unix.com/man_page/posix/2/ppoll). This change adds such approximation, converting clock subscriptions using nanoseconds to milliseconds, while mapping file descriptor read and write subscriptions to `POLLIN` and `POLLOUT` events in POSIX. --- Sources/WASI/Poll.swift | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Sources/WASI/Poll.swift diff --git a/Sources/WASI/Poll.swift b/Sources/WASI/Poll.swift new file mode 100644 index 00000000..829f9650 --- /dev/null +++ b/Sources/WASI/Poll.swift @@ -0,0 +1,46 @@ +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(Android) +import Android +#else +#error("Unsupported Platform") +#endif + +import SystemPackage + +extension FdTable { + func fileDescriptor(fd: WASIAbi.Fd) throws -> FileDescriptor { + guard case let .file(entry) = self[fd], let fd = (entry as? FdWASIEntry)?.fd else { + throw WASIAbi.Errno.EBADF + } + + return fd + } +} + +func poll( + subscriptions: some Sequence, + _ fdTable: FdTable +) throws { + var pollfds = [pollfd]() + var timeoutMilliseconds = UInt.max + + for subscription in subscriptions { + let union = subscription.union + switch union { + case .clock(let clock): + timeoutMilliseconds = min(timeoutMilliseconds, .init(clock.timeout / 1_000_000)) + case .fdRead(let fd): + pollfds.append(.init(fd: try fdTable.fileDescriptor(fd: fd).rawValue, events: .init(POLLIN), revents: 0)) + case .fdWrite(let fd): + pollfds.append(.init(fd: try fdTable.fileDescriptor(fd: fd).rawValue, events: .init(POLLOUT), revents: 0)) + + } + } + + poll(&pollfds, .init(pollfds.count), .init(timeoutMilliseconds)) +} From 9edb55fcd8a317e251874c507aeefcbe1da26dfa Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 15:51:18 +0100 Subject: [PATCH 09/13] Add a test for `poll_oneoff` Implementation for POSIX platforms of `poll_oneoff` syscall from WebAssembly System Interface standard should have corresponding tests in WasmKit. This change wires it up via `WASIBridgeToHost` for testability, which then allows exercising clock subscription events in a simple test. Hardcoded test constants for pointer offsets are also generalized to make the test more readable. --- Sources/WASI/WASI.swift | 18 +++--------------- Tests/WASITests/WASITests.swift | 27 ++++++++++++++++++--------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/Sources/WASI/WASI.swift b/Sources/WASI/WASI.swift index b8b010bb..320e849b 100644 --- a/Sources/WASI/WASI.swift +++ b/Sources/WASI/WASI.swift @@ -2012,21 +2012,9 @@ public class WASIBridgeToHost: WASI { subscriptions: UnsafeGuestBufferPointer, events: UnsafeGuestBufferPointer ) throws -> WASIAbi.Size { - for subscription in subscriptions { - switch subscription.union { - case .clock: - throw WASIAbi.Errno.ENOTSUP - - case .fdRead(let fd), .fdWrite(let fd): - guard case let .file(entry) = self.fdTable[fd] else { - throw WASIAbi.Errno.EBADF - - } - throw WASIAbi.Errno.ENOTSUP - } - } - - return 0 + guard !subscriptions.isEmpty else { throw WASIAbi.Errno.EINVAL } + try poll(subscriptions: subscriptions, self.fdTable) + return .init(subscriptions.count) } func random_get(buffer: UnsafeGuestPointer, length: WASIAbi.Size) { diff --git a/Tests/WASITests/WASITests.swift b/Tests/WASITests/WASITests.swift index 4f0f9c00..e1d61641 100644 --- a/Tests/WASITests/WASITests.swift +++ b/Tests/WASITests/WASITests.swift @@ -147,26 +147,35 @@ final class WASITests: XCTestCase { var pointer = start let read = WASIAbi.Subscription.Union.fdRead(.init(0)) let write = WASIAbi.Subscription.Union.fdWrite(.init(0)) - let clock = WASIAbi.Subscription.Union.clock(.init(id: .REALTIME, timeout: 42, precision: 0, flags: [])) + let writeOffset = WASIAbi.Subscription.sizeInGuest + let timeout: WASIAbi.Timestamp = 100_000_000 + let clock = WASIAbi.Subscription.Union.clock(.init(id: .REALTIME, timeout: timeout, precision: 0, flags: [])) + let clockOffset = writeOffset + WASIAbi.Subscription.sizeInGuest let event = WASIAbi.Event(userData: 3, error: .EIO, eventType: .fdRead, fdReadWrite: .init(nBytes: 37, flags: [.hangup])) + let eventOffset = clockOffset + WASIAbi.Subscription.sizeInGuest + let finalOffset = eventOffset + WASIAbi.Event.sizeInGuest WASIAbi.Subscription.writeToGuest(at: &pointer, value: .init(userData: 1, union: read)) - XCTAssertEqual(pointer.offset, 48) + XCTAssertEqual(pointer.offset, writeOffset) WASIAbi.Subscription.writeToGuest(at: &pointer, value: .init(userData: 2, union: write)) - XCTAssertEqual(pointer.offset, 48 * 2) + XCTAssertEqual(pointer.offset, clockOffset) WASIAbi.Subscription.writeToGuest(at: &pointer, value: .init(userData: 3, union: clock)) - XCTAssertEqual(pointer.offset, 48 * 3) + XCTAssertEqual(pointer.offset, eventOffset) WASIAbi.Event.writeToGuest(at: &pointer, value: event) - XCTAssertEqual(pointer.offset, 48 * 3 + 32) + XCTAssertEqual(pointer.offset, finalOffset) // Test that reading back yields same result pointer = start XCTAssertEqual(WASIAbi.Subscription.readFromGuest(&pointer), .init(userData: 1, union: read)) - XCTAssertEqual(pointer.offset, 48) + XCTAssertEqual(pointer.offset, writeOffset) XCTAssertEqual(WASIAbi.Subscription.readFromGuest(&pointer), .init(userData: 2, union: write)) - XCTAssertEqual(pointer.offset, 48 * 2) + XCTAssertEqual(pointer.offset, clockOffset) XCTAssertEqual(WASIAbi.Subscription.readFromGuest(&pointer), .init(userData: 3, union: clock)) - XCTAssertEqual(pointer.offset, 48 * 3) + XCTAssertEqual(pointer.offset, eventOffset) XCTAssertEqual(WASIAbi.Event.readFromGuest(&pointer), event) - XCTAssertEqual(pointer.offset, 48 * 3 + 32) + XCTAssertEqual(pointer.offset, finalOffset) + XCTAssertTrue(try ContinuousClock().measure { + let clockPointer = UnsafeGuestBufferPointer(baseAddress: .init(memorySpace: memory, offset: clockOffset), count: 1) + XCTAssertEqual(try WASIBridgeToHost().poll_oneoff(subscriptions: clockPointer, events: .init(baseAddress: .init(memorySpace: memory, offset: finalOffset), count: 1)), 1) + } > .nanoseconds(timeout)) } } From a78d370233d1bca8e5a330428285d9fecbbc6619 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 16:02:43 +0100 Subject: [PATCH 10/13] Fix formatting --- Sources/WASI/CMakeLists.txt | 1 + Sources/WASI/Poll.swift | 16 ++++++++-------- Tests/WASITests/WASITests.swift | 9 +++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Sources/WASI/CMakeLists.txt b/Sources/WASI/CMakeLists.txt index 74144a9f..105a53ce 100644 --- a/Sources/WASI/CMakeLists.txt +++ b/Sources/WASI/CMakeLists.txt @@ -9,6 +9,7 @@ add_wasmkit_library(WASI FileSystem.swift GuestMemorySupport.swift Clock.swift + Poll.swift RandomBufferGenerator.swift WASI.swift ) diff --git a/Sources/WASI/Poll.swift b/Sources/WASI/Poll.swift index 829f9650..0efe10b3 100644 --- a/Sources/WASI/Poll.swift +++ b/Sources/WASI/Poll.swift @@ -1,20 +1,20 @@ +import SystemPackage + #if canImport(Darwin) -import Darwin + import Darwin #elseif canImport(Glibc) -import Glibc + import Glibc #elseif canImport(Musl) -import Musl + import Musl #elseif canImport(Android) -import Android + import Android #else -#error("Unsupported Platform") + #error("Unsupported Platform") #endif -import SystemPackage - extension FdTable { func fileDescriptor(fd: WASIAbi.Fd) throws -> FileDescriptor { - guard case let .file(entry) = self[fd], let fd = (entry as? FdWASIEntry)?.fd else { + guard case let .file(entry) = self[fd], let fd = (entry as? FdWASIEntry)?.fd else { throw WASIAbi.Errno.EBADF } diff --git a/Tests/WASITests/WASITests.swift b/Tests/WASITests/WASITests.swift index e1d61641..6cedfc00 100644 --- a/Tests/WASITests/WASITests.swift +++ b/Tests/WASITests/WASITests.swift @@ -173,9 +173,10 @@ final class WASITests: XCTestCase { XCTAssertEqual(pointer.offset, eventOffset) XCTAssertEqual(WASIAbi.Event.readFromGuest(&pointer), event) XCTAssertEqual(pointer.offset, finalOffset) - XCTAssertTrue(try ContinuousClock().measure { - let clockPointer = UnsafeGuestBufferPointer(baseAddress: .init(memorySpace: memory, offset: clockOffset), count: 1) - XCTAssertEqual(try WASIBridgeToHost().poll_oneoff(subscriptions: clockPointer, events: .init(baseAddress: .init(memorySpace: memory, offset: finalOffset), count: 1)), 1) - } > .nanoseconds(timeout)) + XCTAssertTrue( + try ContinuousClock().measure { + let clockPointer = UnsafeGuestBufferPointer(baseAddress: .init(memorySpace: memory, offset: clockOffset), count: 1) + XCTAssertEqual(try WASIBridgeToHost().poll_oneoff(subscriptions: clockPointer, events: .init(baseAddress: .init(memorySpace: memory, offset: finalOffset), count: 1)), 1) + } > .nanoseconds(timeout)) } } From 63c75bb4e6c76c3ff72aa1921b99de457ead70b9 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 16:11:42 +0100 Subject: [PATCH 11/13] Bump macOS version requirement, mark Windows unsupported --- Package.swift | 2 +- Sources/WASI/Poll.swift | 30 +++++++++++++++++------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Package.swift b/Package.swift index a188d6dc..0200d3ee 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let DarwinPlatforms: [Platform] let package = Package( name: "WasmKit", - platforms: [.macOS(.v10_13), .iOS(.v12)], + platforms: [.macOS(.v13), .iOS(.v16)], products: [ .executable(name: "wasmkit-cli", targets: ["CLI"]), .library(name: "WasmKit", targets: ["WasmKit"]), diff --git a/Sources/WASI/Poll.swift b/Sources/WASI/Poll.swift index 0efe10b3..64de579a 100644 --- a/Sources/WASI/Poll.swift +++ b/Sources/WASI/Poll.swift @@ -26,21 +26,25 @@ func poll( subscriptions: some Sequence, _ fdTable: FdTable ) throws { - var pollfds = [pollfd]() - var timeoutMilliseconds = UInt.max + #if os(Windows) + throw WASIAbi.Errno.ENOTSUP + #else + var pollfds = [pollfd]() + var timeoutMilliseconds = UInt.max - for subscription in subscriptions { - let union = subscription.union - switch union { - case .clock(let clock): - timeoutMilliseconds = min(timeoutMilliseconds, .init(clock.timeout / 1_000_000)) - case .fdRead(let fd): - pollfds.append(.init(fd: try fdTable.fileDescriptor(fd: fd).rawValue, events: .init(POLLIN), revents: 0)) - case .fdWrite(let fd): - pollfds.append(.init(fd: try fdTable.fileDescriptor(fd: fd).rawValue, events: .init(POLLOUT), revents: 0)) + for subscription in subscriptions { + let union = subscription.union + switch union { + case .clock(let clock): + timeoutMilliseconds = min(timeoutMilliseconds, .init(clock.timeout / 1_000_000)) + case .fdRead(let fd): + pollfds.append(.init(fd: try fdTable.fileDescriptor(fd: fd).rawValue, events: .init(POLLIN), revents: 0)) + case .fdWrite(let fd): + pollfds.append(.init(fd: try fdTable.fileDescriptor(fd: fd).rawValue, events: .init(POLLOUT), revents: 0)) + } } - } - poll(&pollfds, .init(pollfds.count), .init(timeoutMilliseconds)) + poll(&pollfds, .init(pollfds.count), .init(timeoutMilliseconds)) + #endif } From 68212d4fb094074f816c798a096075c9b3ae04d9 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 16:17:34 +0100 Subject: [PATCH 12/13] Fix missing Windows `import ucrt` --- Sources/WASI/Poll.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/WASI/Poll.swift b/Sources/WASI/Poll.swift index 64de579a..4f0c142b 100644 --- a/Sources/WASI/Poll.swift +++ b/Sources/WASI/Poll.swift @@ -8,6 +8,8 @@ import SystemPackage import Musl #elseif canImport(Android) import Android +#elseif os(Windows) + import ucrt #else #error("Unsupported Platform") #endif From 9e8306e9d4dee48c3c4cce62142046dd7428d178 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 16:24:41 +0100 Subject: [PATCH 13/13] Exclude unsupported test on Windows --- Tests/WASITests/WASITests.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Tests/WASITests/WASITests.swift b/Tests/WASITests/WASITests.swift index 6cedfc00..75fc5f0d 100644 --- a/Tests/WASITests/WASITests.swift +++ b/Tests/WASITests/WASITests.swift @@ -173,10 +173,13 @@ final class WASITests: XCTestCase { XCTAssertEqual(pointer.offset, eventOffset) XCTAssertEqual(WASIAbi.Event.readFromGuest(&pointer), event) XCTAssertEqual(pointer.offset, finalOffset) - XCTAssertTrue( - try ContinuousClock().measure { - let clockPointer = UnsafeGuestBufferPointer(baseAddress: .init(memorySpace: memory, offset: clockOffset), count: 1) - XCTAssertEqual(try WASIBridgeToHost().poll_oneoff(subscriptions: clockPointer, events: .init(baseAddress: .init(memorySpace: memory, offset: finalOffset), count: 1)), 1) - } > .nanoseconds(timeout)) + + #if !os(Windows) + XCTAssertTrue( + try ContinuousClock().measure { + let clockPointer = UnsafeGuestBufferPointer(baseAddress: .init(memorySpace: memory, offset: clockOffset), count: 1) + XCTAssertEqual(try WASIBridgeToHost().poll_oneoff(subscriptions: clockPointer, events: .init(baseAddress: .init(memorySpace: memory, offset: finalOffset), count: 1)), 1) + } > .nanoseconds(timeout)) + #endif } }