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/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 new file mode 100644 index 00000000..4f0c142b --- /dev/null +++ b/Sources/WASI/Poll.swift @@ -0,0 +1,52 @@ +import SystemPackage + +#if canImport(Darwin) + import Darwin +#elseif canImport(Glibc) + import Glibc +#elseif canImport(Musl) + import Musl +#elseif canImport(Android) + import Android +#elseif os(Windows) + import ucrt +#else + #error("Unsupported Platform") +#endif + +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 { + #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)) + + } + } + + poll(&pollfds, .init(pollfds.count), .init(timeoutMilliseconds)) + #endif +} diff --git a/Sources/WASI/WASI.swift b/Sources/WASI/WASI.swift index 631a3325..320e849b 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. @@ -429,7 +428,158 @@ enum WASIAbi { case END = 2 } - enum ClockId: UInt32 { + struct Clock: Equatable, GuestPointee { + struct Flags: OptionSet, GuestPointee { + 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, GuestPointee { + case clock + case fdRead + case fdWrite + } + + typealias UserData = UInt64 + + struct Subscription: Equatable, GuestPointee { + 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) + } + } + } + + 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 { + 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, GuestPointee { /// The clock measuring real time. Time value zero corresponds with /// 1970-01-01T00:00:00Z. case REALTIME = 0 @@ -817,7 +967,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]), @@ -1358,6 +1507,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) ] @@ -1842,11 +2009,12 @@ 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 + 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/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))! diff --git a/Tests/WASITests/WASITests.swift b/Tests/WASITests/WASITests.swift index 68ef114b..75fc5f0d 100644 --- a/Tests/WASITests/WASITests.swift +++ b/Tests/WASITests/WASITests.swift @@ -1,3 +1,5 @@ +import WasmKit +import WasmTypes import XCTest @testable import WASI @@ -134,4 +136,50 @@ 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 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, writeOffset) + WASIAbi.Subscription.writeToGuest(at: &pointer, value: .init(userData: 2, union: write)) + XCTAssertEqual(pointer.offset, clockOffset) + WASIAbi.Subscription.writeToGuest(at: &pointer, value: .init(userData: 3, union: clock)) + XCTAssertEqual(pointer.offset, eventOffset) + WASIAbi.Event.writeToGuest(at: &pointer, value: event) + 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, writeOffset) + XCTAssertEqual(WASIAbi.Subscription.readFromGuest(&pointer), .init(userData: 2, union: write)) + XCTAssertEqual(pointer.offset, clockOffset) + XCTAssertEqual(WASIAbi.Subscription.readFromGuest(&pointer), .init(userData: 3, union: clock)) + XCTAssertEqual(pointer.offset, eventOffset) + XCTAssertEqual(WASIAbi.Event.readFromGuest(&pointer), event) + XCTAssertEqual(pointer.offset, finalOffset) + + #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 + } }