Skip to content

Add poll_oneoff for POSIX hosts to WASI.swift #187

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down
1 change: 1 addition & 0 deletions Sources/WASI/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ add_wasmkit_library(WASI
FileSystem.swift
GuestMemorySupport.swift
Clock.swift
Poll.swift
RandomBufferGenerator.swift
WASI.swift
)
Expand Down
52 changes: 52 additions & 0 deletions Sources/WASI/Poll.swift
Original file line number Diff line number Diff line change
@@ -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<WASIAbi.Subscription>,
_ 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
}
188 changes: 178 additions & 10 deletions Sources/WASI/WASI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,16 @@ protocol WASI {

/// Concurrently poll for the occurrence of a set of events.
func poll_oneoff(
subscriptions: UnsafeGuestRawPointer,
events: UnsafeGuestRawPointer,
numberOfSubscriptions: WASIAbi.Size
subscriptions: UnsafeGuestBufferPointer<WASIAbi.Subscription>,
events: UnsafeGuestBufferPointer<WASIAbi.Event>
) throws -> WASIAbi.Size

/// Write high-quality random data into a buffer.
func random_get(buffer: UnsafeGuestPointer<UInt8>, length: WASIAbi.Size)
}

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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]),
Expand Down Expand Up @@ -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<WASIAbi.Subscription>(memorySpace: buffer, offset: arguments[0].i32)
let eventsBaseAddress = UnsafeGuestPointer<WASIAbi.Event>(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<UInt32>.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)
]
Expand Down Expand Up @@ -1842,11 +2009,12 @@ public class WASIBridgeToHost: WASI {
}

func poll_oneoff(
subscriptions: UnsafeGuestRawPointer,
events: UnsafeGuestRawPointer,
numberOfSubscriptions: WASIAbi.Size
subscriptions: UnsafeGuestBufferPointer<WASIAbi.Subscription>,
events: UnsafeGuestBufferPointer<WASIAbi.Event>
) 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<UInt8>, length: WASIAbi.Size) {
Expand Down
10 changes: 9 additions & 1 deletion Sources/WasmTypes/GuestMemory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))!
Expand Down
48 changes: 48 additions & 0 deletions Tests/WASITests/WASITests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import WasmKit
import WasmTypes
import XCTest

@testable import WASI
Expand Down Expand Up @@ -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<WASIAbi.Subscription>(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
}
}