From af360f664cbefe52e7cfd6c80de4b19ec87ed245 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 18 Jun 2025 15:43:17 +0100 Subject: [PATCH 01/18] 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 04a14b4946155a2b6bdc310a6711d23fdb8b1e23 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 18 Jun 2025 15:44:36 +0100 Subject: [PATCH 02/18] 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 b5e0a7e313029c444670fdf047bdfb91f12c8d67 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 18 Jun 2025 15:45:29 +0100 Subject: [PATCH 03/18] 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 58341180b04cdad3ad927a0a91378793270f9463 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 18 Jun 2025 15:47:02 +0100 Subject: [PATCH 04/18] 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 2b9f0a889a5d4237c8f768d1b2c7a8d02b54ab89 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 23 Jun 2025 14:46:28 +0100 Subject: [PATCH 05/18] 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 5f11678a92e3a3187109c6ee9280d6cd9276fb54 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 23 Jun 2025 14:47:16 +0100 Subject: [PATCH 06/18] 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 0a1bb920a1c4e0a11db04e86727cf4f4f62cdc37 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 23 Jun 2025 15:58:15 +0100 Subject: [PATCH 07/18] 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 df29b59e4703a9596723b61b3810be93c33a1c6c Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 15:50:52 +0100 Subject: [PATCH 08/18] 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 5a5dce7a2ee313385142e1c911ddef50b7a9703b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 15:51:18 +0100 Subject: [PATCH 09/18] 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 eaff0f17d0504ba391b0ba732f1dc17402d2545e Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 16:02:43 +0100 Subject: [PATCH 10/18] 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 3cde63969b475cb4167f20eeb7c8e0701a71af5f Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 16:11:42 +0100 Subject: [PATCH 11/18] 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 720f3b4f..fff65b12 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 fbbabb40c4149fb752cbbaeeeef266eb7e7166c6 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 16:17:34 +0100 Subject: [PATCH 12/18] 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 5c288dbd35fac409f9dd7ddb2336a70c25149d33 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 25 Jun 2025 16:24:41 +0100 Subject: [PATCH 13/18] 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 } } From acda0306b9561b382df97f06f336ccadc64ca232 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 21 Jul 2025 12:25:33 +0100 Subject: [PATCH 14/18] Fix incorrect subscript offset in `UnsafeGuestBufferPointer` A testsuite-conforming implementation of `poll_oneff` from WebAssembly System Interface (WASI) standard specification (https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#poll_oneoff) should write to the provided buffer of events that occurred before a given clock timeout. WasmKit in its current implementation had an issue with writing to such buffer, since offset for each element was calculated incorrectly in the `WasmTypes/GuestMemory.swift` source file. Implementation of `poll` function in `WASI/Poll.swift` now takes this buffer as an argument, accumulates user data of file descriptor polling subscriptions in `fdUserData`, while storing clock timer user data in `clockUserData`, which allows later writes to the events buffer for conformances with the test suite. Errors thrown by the host system implementation of `poll` are converted to corresponding WASI errors. --- Sources/WASI/Poll.swift | 37 ++++++++++++++++++++++++++--- Sources/WasmTypes/GuestMemory.swift | 3 ++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/Sources/WASI/Poll.swift b/Sources/WASI/Poll.swift index 4f0c142b..1728dd31 100644 --- a/Sources/WASI/Poll.swift +++ b/Sources/WASI/Poll.swift @@ -1,4 +1,5 @@ import SystemPackage +import WasmTypes #if canImport(Darwin) import Darwin @@ -26,27 +27,57 @@ extension FdTable { func poll( subscriptions: some Sequence, + events: UnsafeGuestBufferPointer, _ fdTable: FdTable -) throws { +) throws -> WASIAbi.Size { #if os(Windows) throw WASIAbi.Errno.ENOTSUP #else var pollfds = [pollfd]() + var fdUserData = [WASIAbi.UserData]() var timeoutMilliseconds = UInt.max + var clockUserData: WASIAbi.UserData? for subscription in subscriptions { let union = subscription.union switch union { case .clock(let clock): timeoutMilliseconds = min(timeoutMilliseconds, .init(clock.timeout / 1_000_000)) + clockUserData = subscription.userData case .fdRead(let fd): pollfds.append(.init(fd: try fdTable.fileDescriptor(fd: fd).rawValue, events: .init(POLLIN), revents: 0)) + fdUserData.append(subscription.userData) case .fdWrite(let fd): pollfds.append(.init(fd: try fdTable.fileDescriptor(fd: fd).rawValue, events: .init(POLLOUT), revents: 0)) - + fdUserData.append(subscription.userData) } } - poll(&pollfds, .init(pollfds.count), .init(timeoutMilliseconds)) + let result = poll(&pollfds, .init(pollfds.count), .init(timeoutMilliseconds)) + let err = errno // Preserve `errno` global immediately after `poll` + var updatedEvents: WASIAbi.Size = 0 + if result == 0, let clockUserData { + updatedEvents += 1 + events[0] = .init(userData: clockUserData, error: .SUCCESS, eventType: .clock, fdReadWrite: .init(nBytes: 0, flags: .init(rawValue: 0))) + } else if result > 0 { + for (i, fd) in pollfds.enumerated() { + updatedEvents += 1 + switch fd.revents { + case .init(POLLIN): + events[.init(i)] = .init(userData: fdUserData[i], error: .SUCCESS, eventType: .fdRead, fdReadWrite: .init(nBytes: 0, flags: [])) + case .init(POLLOUT): + events[.init(i)] = .init(userData: fdUserData[i], error: .SUCCESS, eventType: .fdWrite, fdReadWrite: .init(nBytes: 0, flags: [])) + default: throw WASIAbi.Errno.ENOTSUP + } + } + } else { + switch err { + case ENOMEM: throw WASIAbi.Errno.ENOMEM + case EINTR: throw WASIAbi.Errno.EINTR + case EINVAL: throw WASIAbi.Errno.EINVAL + default: throw WASIAbi.Errno.ENOTSUP + } + } + return updatedEvents #endif } diff --git a/Sources/WasmTypes/GuestMemory.swift b/Sources/WasmTypes/GuestMemory.swift index 33364c0b..9f6d7028 100644 --- a/Sources/WasmTypes/GuestMemory.swift +++ b/Sources/WasmTypes/GuestMemory.swift @@ -375,7 +375,8 @@ extension UnsafeGuestBufferPointer: Collection { /// Accesses the pointee at the specified offset from the base address of the buffer. public subscript(position: UInt32) -> Element { - (self.baseAddress + position).pointee + get { (self.baseAddress + position * Element.sizeInGuest).pointee } + nonmutating set { Pointee.writeToGuest(at: self.baseAddress.raw.advanced(by: position * Element.sizeInGuest), value: newValue) } } /// Returns the position immediately after the given index. From 95a0d1b8d9c1603202d8fdb4749cb5ab4af5011b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 21 Jul 2025 12:26:34 +0100 Subject: [PATCH 15/18] Fix `errno` to use `UInt16` per the WASI p1 spec Per the WebAssembly System Interface (WASI) standard specification, size of `errno` variant is 2 bytes, which is equivalent to `UInt16` in Swift. In WasmKit it's currently specified incorrectly to `UInt32`, which leads to bugs when reading and writing `errno` values in guest memory. Corresponding uses of `WASIAbi.Errno` zero-extend value of 2 bytes to 4 bytes when returning 32-bit integers, since WebAssembly doesn't have a separate 16-bit integer type and zero-extension of narrower integers is the expected behavior. Additionally `ClockId` type specifies a fixed number of cases, disallowing unknown raw values, which could be specified in the future. This edge case is exercised in the WASI specification test suite. The change relaxes this restriction, making the implementation compliant with the test. --- Sources/WASI/WASI.swift | 47 +++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/Sources/WASI/WASI.swift b/Sources/WASI/WASI.swift index 320e849b..36a5d349 100644 --- a/Sources/WASI/WASI.swift +++ b/Sources/WASI/WASI.swift @@ -220,7 +220,7 @@ protocol WASI { } enum WASIAbi { - enum Errno: UInt32, Error, GuestPointee { + enum Errno: UInt16, Error, GuestPointee { /// No error occurred. System call completed successfully. case SUCCESS = 0 /// Argument list too long. @@ -579,19 +579,20 @@ enum WASIAbi { } } - enum ClockId: UInt32, GuestPointee { + struct ClockId: Equatable, RawRepresentable, GuestPointee { + let rawValue: UInt32 /// The clock measuring real time. Time value zero corresponds with /// 1970-01-01T00:00:00Z. - case REALTIME = 0 + static let REALTIME = Self(rawValue: 0) /// The store-wide monotonic clock, which is defined as a clock measuring /// real time, whose value cannot be adjusted and which cannot have negative /// clock jumps. The epoch of this clock is undefined. The absolute time /// value of this clock therefore has no meaning. - case MONOTONIC = 1 + static let MONOTONIC = Self(rawValue: 1) /// The CPU-time clock associated with the current process. - case PROCESS_CPUTIME_ID = 2 + static let PROCESS_CPUTIME_ID = Self(rawValue: 2) /// The CPU-time clock associated with the current thread. - case THREAD_CPUTIME_ID = 3 + static let THREAD_CPUTIME_ID = Self(rawValue: 3) } typealias Timestamp = UInt64 @@ -980,7 +981,7 @@ extension WASI { let (name, type) = entry functions[name] = WASIHostFunction(type: type) { _, _ in print("\"\(name)\" not implemented yet") - return [.i32(WASIAbi.Errno.ENOSYS.rawValue)] + return [.i32(.init(WASIAbi.Errno.ENOSYS.rawValue))] } } @@ -1013,7 +1014,7 @@ extension WASI { do { return try implementation(caller, arguments) } catch let errno as WASIAbi.Errno { - return [.i32(errno.rawValue)] + return [.i32(.init(errno.rawValue))] } } } @@ -1026,7 +1027,7 @@ extension WASI { argv: .init(memorySpace: buffer, offset: arguments[0].i32), argvBuffer: .init(memorySpace: buffer, offset: arguments[1].i32) ) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } } @@ -1039,7 +1040,7 @@ extension WASI { argcPointer.pointee = argc let bufferSizePointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) bufferSizePointer.pointee = bufferSize - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } } @@ -1051,7 +1052,7 @@ extension WASI { environ: .init(memorySpace: buffer, offset: arguments[0].i32), environBuffer: .init(memorySpace: buffer, offset: arguments[1].i32) ) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } } @@ -1064,16 +1065,14 @@ extension WASI { environSizePointer.pointee = environSize let bufferSizePointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) bufferSizePointer.pointee = bufferSize - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } } preview1["clock_res_get"] = wasiFunction( type: .init(parameters: [.i32, .i32], results: [.i32]) ) { caller, arguments in - guard let id = WASIAbi.ClockId(rawValue: arguments[0].i32) else { - throw WASIAbi.Errno.EBADF - } + let id = WASIAbi.ClockId(rawValue: arguments[0].i32) let res = try self.clock_res_get(id: id) try withMemoryBuffer(caller: caller) { buffer in let resPointer = UnsafeGuestPointer( @@ -1081,15 +1080,13 @@ extension WASI { ) resPointer.pointee = res } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["clock_time_get"] = wasiFunction( type: .init(parameters: [.i32, .i64, .i32], results: [.i32]) ) { caller, arguments in - guard let id = WASIAbi.ClockId(rawValue: arguments[0].i32) else { - throw WASIAbi.Errno.EBADF - } + let id = WASIAbi.ClockId(rawValue: arguments[0].i32) let time = try self.clock_time_get(id: id, precision: WASIAbi.Timestamp(arguments[1].i64)) try withMemoryBuffer(caller: caller) { buffer in let resPointer = UnsafeGuestPointer( @@ -1097,7 +1094,7 @@ extension WASI { ) resPointer.pointee = time } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_advise"] = wasiFunction( @@ -1112,7 +1109,7 @@ extension WASI { fd: arguments[0].i32, offset: arguments[1].i64, length: arguments[2].i64, advice: advice ) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_allocate"] = wasiFunction( @@ -1121,21 +1118,21 @@ extension WASI { try self.fd_allocate( fd: arguments[0].i32, offset: arguments[1].i64, length: arguments[2].i64 ) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_close"] = wasiFunction( type: .init(parameters: [.i32], results: [.i32]) ) { caller, arguments in try self.fd_close(fd: arguments[0].i32) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_datasync"] = wasiFunction( type: .init(parameters: [.i32], results: [.i32]) ) { caller, arguments in try self.fd_datasync(fd: arguments[0].i32) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_fdstat_get"] = wasiFunction( @@ -1145,7 +1142,7 @@ extension WASI { let stat = try self.fd_fdstat_get(fileDescriptor: arguments[0].i32) let statPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) statPointer.pointee = stat - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } } From b1c8211b8a9abece6d1de1f5d13866b572010f71 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 21 Jul 2025 12:27:34 +0100 Subject: [PATCH 16/18] Fix `errno` backed by `UInt16` With `WASIAbi.Errno` type set to 16-bit (2 bytes) width in WasmKit per WebAssembly System Interface specification (https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#errno), corresponding uses need to be updated to zero-extend result value to 32-bit, which is the standard behavior in absence of a built-in 16-bit integer type in WebAssembly. --- Sources/WASI/WASI.swift | 46 ++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Sources/WASI/WASI.swift b/Sources/WASI/WASI.swift index 36a5d349..5c049451 100644 --- a/Sources/WASI/WASI.swift +++ b/Sources/WASI/WASI.swift @@ -1155,7 +1155,7 @@ extension WASI { try self.fd_fdstat_set_flags( fd: arguments[0].i32, flags: WASIAbi.Fdflags(rawValue: rawFdFlags) ) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_fdstat_set_rights"] = wasiFunction( @@ -1166,7 +1166,7 @@ extension WASI { fsRightsBase: WASIAbi.Rights(rawValue: arguments[1].i64), fsRightsInheriting: WASIAbi.Rights(rawValue: arguments[2].i64) ) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_filestat_get"] = wasiFunction( @@ -1177,14 +1177,14 @@ extension WASI { let filestatPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) filestatPointer.pointee = filestat } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_filestat_set_size"] = wasiFunction( type: .init(parameters: [.i32, .i64], results: [.i32]) ) { caller, arguments in try self.fd_filestat_set_size(fd: arguments[0].i32, size: arguments[1].i64) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_filestat_set_times"] = wasiFunction( @@ -1198,7 +1198,7 @@ extension WASI { atim: arguments[1].i64, mtim: arguments[2].i64, fstFlags: WASIAbi.FstFlags(rawValue: rawFstFlags) ) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_pread"] = wasiFunction( @@ -1216,7 +1216,7 @@ extension WASI { let nreadPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[4].i32) nreadPointer.pointee = nread } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_prestat_get"] = wasiFunction(type: .init(parameters: [.i32, .i32], results: [.i32])) { caller, arguments in let prestat = try self.fd_prestat_get(fd: arguments[0].i32) @@ -1224,7 +1224,7 @@ extension WASI { let prestatPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) prestatPointer.pointee = prestat } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_prestat_dir_name"] = wasiFunction(type: .init(parameters: [.i32, .i32, .i32], results: [.i32])) { caller, arguments in @@ -1235,7 +1235,7 @@ extension WASI { maxPathLength: arguments[2].i32 ) } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_pwrite"] = wasiFunction( @@ -1253,7 +1253,7 @@ extension WASI { let nwrittenPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[4].i32) nwrittenPointer.pointee = nwritten } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_read"] = wasiFunction( @@ -1270,7 +1270,7 @@ extension WASI { let nreadPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[3].i32) nreadPointer.pointee = nread } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_readdir"] = wasiFunction(type: .init(parameters: [.i32, .i32, .i32, .i64, .i32], results: [.i32])) { caller, arguments in @@ -1285,7 +1285,7 @@ extension WASI { ) let nwrittenPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[4].i32) nwrittenPointer.pointee = nwritten - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } } @@ -1293,14 +1293,14 @@ extension WASI { type: .init(parameters: [.i32, .i32], results: [.i32]) ) { caller, arguments in try self.fd_renumber(fd: arguments[0].i32, to: arguments[1].i32) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_seek"] = wasiFunction( type: .init(parameters: [.i32, .i64, .i32, .i32], results: [.i32]) ) { caller, arguments in guard let whence = WASIAbi.Whence(rawValue: UInt8(arguments[2].i32)) else { - return [.i32(WASIAbi.Errno.EINVAL.rawValue)] + return [.i32(.init(WASIAbi.Errno.EINVAL.rawValue))] } let ret = try self.fd_seek( fd: arguments[0].i32, offset: WASIAbi.FileDelta(bitPattern: arguments[1].i64), whence: whence @@ -1309,12 +1309,12 @@ extension WASI { let retPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[3].i32) retPointer.pointee = ret } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_sync"] = wasiFunction(type: .init(parameters: [.i32], results: [.i32])) { caller, arguments in try self.fd_sync(fd: arguments[0].i32) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_tell"] = wasiFunction(type: .init(parameters: [.i32, .i32], results: [.i32])) { caller, arguments in @@ -1323,7 +1323,7 @@ extension WASI { let retPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) retPointer.pointee = ret } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["fd_write"] = wasiFunction( @@ -1339,7 +1339,7 @@ extension WASI { ) let nwrittenPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[3].i32) nwrittenPointer.pointee = nwritten - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } } @@ -1352,7 +1352,7 @@ extension WASI { path: readString(pointer: arguments[1].i32, length: arguments[2].i32, buffer: buffer) ) } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["path_filestat_get"] = wasiFunction( type: .init(parameters: [.i32, .i32, .i32, .i32, .i32], results: [.i32]) @@ -1365,7 +1365,7 @@ extension WASI { let filestatPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[4].i32) filestatPointer.pointee = filestat } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["path_filestat_set_times"] = wasiFunction( @@ -1382,7 +1382,7 @@ extension WASI { fstFlags: WASIAbi.FstFlags(rawValue: rawFstFlags) ) } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["path_link"] = wasiFunction( @@ -1396,7 +1396,7 @@ extension WASI { newPath: readString(pointer: arguments[5].i32, length: arguments[6].i32, buffer: buffer) ) } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["path_open"] = wasiFunction( @@ -1414,7 +1414,7 @@ extension WASI { ) let newFdPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[8].i32) newFdPointer.pointee = newFd - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } } @@ -1433,7 +1433,7 @@ extension WASI { let retPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[5].i32) retPointer.pointee = ret } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["path_remove_directory"] = wasiFunction( From a0594e6f998aa8b27162f8bec9c856daa712092b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 21 Jul 2025 12:28:36 +0100 Subject: [PATCH 17/18] Return number of events in `poll_oneoff` With `WASIAbi.Errno` type set to 16-bit (2 bytes) width in WasmKit per WebAssembly System Interface specification (https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#errno), corresponding uses need to be updated to zero-extend result value to 32-bit, which is the standard behavior in absence of a built-in 16-bit integer type in WebAssembly. Additionally for wasi-testsuite compatibility, WasmKit should return the number of events occurred in the span of `poll_oneoff` call, not the number of subscription that were passed to it. --- Sources/WASI/WASI.swift | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Sources/WASI/WASI.swift b/Sources/WASI/WASI.swift index 5c049451..5064f10e 100644 --- a/Sources/WASI/WASI.swift +++ b/Sources/WASI/WASI.swift @@ -1445,7 +1445,7 @@ extension WASI { path: readString(pointer: arguments[1].i32, length: arguments[2].i32, buffer: buffer) ) } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["path_rename"] = wasiFunction( @@ -1459,7 +1459,7 @@ extension WASI { newPath: readString(pointer: arguments[4].i32, length: arguments[5].i32, buffer: buffer) ) } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["path_symlink"] = wasiFunction( @@ -1472,7 +1472,7 @@ extension WASI { newPath: readString(pointer: arguments[3].i32, length: arguments[4].i32, buffer: buffer) ) } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["path_unlink_file"] = wasiFunction( @@ -1484,7 +1484,7 @@ extension WASI { path: readString(pointer: arguments[1].i32, length: arguments[2].i32, buffer: buffer) ) } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } preview1["proc_exit"] = wasiFunction(type: .init(parameters: [.i32])) { memory, arguments in @@ -1500,7 +1500,7 @@ extension WASI { buffer: UnsafeGuestPointer(memorySpace: buffer, offset: arguments[0].i32), length: arguments[1].i32 ) - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } } @@ -1518,7 +1518,7 @@ extension WASI { raw.withMemoryRebound(to: UInt32.self) { rebound in rebound[0] = size.littleEndian } } - return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + return [.i32(.init(WASIAbi.Errno.SUCCESS.rawValue))] } } @@ -1639,8 +1639,8 @@ public class WASIBridgeToHost: WASI { return WASIAbi.Timestamp(wallClockDuration: try wallClock.resolution()) case .MONOTONIC: return try monotonicClock.resolution() - case .PROCESS_CPUTIME_ID, .THREAD_CPUTIME_ID: - throw WASIAbi.Errno.EBADF + default: + throw WASIAbi.Errno.ENOTSUP } } @@ -1652,8 +1652,8 @@ public class WASIBridgeToHost: WASI { return WASIAbi.Timestamp(wallClockDuration: try wallClock.now()) case .MONOTONIC: return try monotonicClock.now() - case .PROCESS_CPUTIME_ID, .THREAD_CPUTIME_ID: - throw WASIAbi.Errno.EBADF + default: + throw WASIAbi.Errno.ENOTSUP } } @@ -2010,8 +2010,7 @@ public class WASIBridgeToHost: WASI { events: UnsafeGuestBufferPointer ) throws -> WASIAbi.Size { guard !subscriptions.isEmpty else { throw WASIAbi.Errno.EINVAL } - try poll(subscriptions: subscriptions, self.fdTable) - return .init(subscriptions.count) + return try poll(subscriptions: subscriptions, events: events, self.fdTable) } func random_get(buffer: UnsafeGuestPointer, length: WASIAbi.Size) { From b4ebe7626f522c2241559f0a661c9776f6148f42 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 21 Jul 2025 12:45:45 +0100 Subject: [PATCH 18/18] Enable `poll_oneoff_stdio` test on non-Windows platforms --- Sources/WASI/Poll.swift | 2 +- Tests/WASITests/IntegrationTests.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/WASI/Poll.swift b/Sources/WASI/Poll.swift index 1728dd31..d43d116b 100644 --- a/Sources/WASI/Poll.swift +++ b/Sources/WASI/Poll.swift @@ -54,7 +54,7 @@ func poll( } let result = poll(&pollfds, .init(pollfds.count), .init(timeoutMilliseconds)) - let err = errno // Preserve `errno` global immediately after `poll` + let err = errno // Preserve `errno` global immediately after `poll` var updatedEvents: WASIAbi.Size = 0 if result == 0, let clockUserData { updatedEvents += 1 diff --git a/Tests/WASITests/IntegrationTests.swift b/Tests/WASITests/IntegrationTests.swift index 6981fe0d..3b458414 100644 --- a/Tests/WASITests/IntegrationTests.swift +++ b/Tests/WASITests/IntegrationTests.swift @@ -113,7 +113,6 @@ final class IntegrationTests: XCTestCase { "path_rename_dir_trailing_slashes", "path_rename", "pwrite-with-append", - "poll_oneoff_stdio", "overwrite_preopen", "path_filestat", "renumber",