diff --git a/loader/goroot.go b/loader/goroot.go index a79af485a2..0aab0a0e13 100644 --- a/loader/goroot.go +++ b/loader/goroot.go @@ -246,6 +246,7 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool { "internal/futex/": false, "internal/fuzz/": false, "internal/itoa/": false, + "internal/poll/": false, "internal/reflectlite/": false, "internal/gclayout": false, "internal/task/": false, diff --git a/src/internal/poll/export_test_wasip1.go b/src/internal/poll/export_test_wasip1.go new file mode 100644 index 0000000000..4a08c01577 --- /dev/null +++ b/src/internal/poll/export_test_wasip1.go @@ -0,0 +1,45 @@ +//go:build wasip1 + +// Internal-test helpers exposed via go:linkname so user code can drive +// the deadline-aware Read/Write loop without becoming a stdlib package. +// Not part of the public API; the names are intentionally awkward to +// signal "for tests only". + +package poll + +import ( + "syscall" + "time" +) + +// pollTestReadWithDeadline opens a pollable FD wrapper for sysfd, sets +// a read deadline d into the future, calls Read once, and returns +// (n, err). Caller is responsible for closing sysfd. +// +//go:linkname pollTestReadWithDeadline +func pollTestReadWithDeadline(sysfd int, d time.Duration, p []byte) (int, error) { + fd := &FD{Sysfd: sysfd, IsStream: true} + // Best-effort init; ignore error so a caller using a not-fcntl-able FD + // (stdin under wazero, etc.) still gets to test the deadline path on + // whatever park behaviour the runtime gives. + _ = fd.Init("test", true) + if err := fd.SetReadDeadline(time.Now().Add(d)); err != nil { + return 0, err + } + return fd.Read(p) +} + +// pollTestSetNonblock toggles O_NONBLOCK on a raw sysfd. Useful in +// tests when the caller wants to ensure the FD is in nonblocking mode +// before calling pollTestReadWithDeadline (Init is best-effort and may +// silently skip). +// +//go:linkname pollTestSetNonblock +func pollTestSetNonblock(sysfd int) error { + flags, err := syscall.Fcntl(sysfd, syscall.F_GETFL, 0) + if err != nil { + return err + } + _, err = syscall.Fcntl(sysfd, syscall.F_SETFL, flags|syscall.O_NONBLOCK) + return err +} diff --git a/src/internal/poll/fd_wasip1.go b/src/internal/poll/fd_wasip1.go new file mode 100644 index 0000000000..bfb0d4d007 --- /dev/null +++ b/src/internal/poll/fd_wasip1.go @@ -0,0 +1,549 @@ +//go:build wasip1 + +// Package poll is a minimal subset of upstream Go's internal/poll, scoped +// to what is needed to back a wasip1 net implementation on top of +// TinyGo's cooperative-scheduler netpoll integration. +// +// On wasip1 the cooperative scheduler integrates poll_oneoff with FD +// waiters (see runtime/netpoll_wasip1.go and syscall/syscall_libc_wasip1.go). +// This package wraps the syscall layer to: +// +// - own the O_NONBLOCK policy decision (set on Init for pollable FDs), +// unblocking the EAGAIN→park retry loop that syscall.Read already has; +// - provide a Go-shaped FD type that net.* can use without reaching +// into syscall directly; +// - thread per-FD read/write deadlines through a runtime helper that +// lets a time.AfterFunc callback wake the parked goroutine; +// - dispatch socket FDs through wasi sock_recv / sock_send / sock_accept +// / sock_shutdown so net.Conn/net.Listener (via upstream Go's +// net/file_wasip1.go) work end-to-end. +package poll + +import ( + "errors" + "internal/task" + "syscall" + "time" + "unsafe" +) + +// ErrFileClosing is returned when a Read or Write is started on a closed FD. +var ErrFileClosing = errors.New("use of closed file") + +// ErrNetClosing is returned for network operations on a closed FD. +var ErrNetClosing = errors.New("use of closed network connection") + +// ErrDeadlineExceeded is returned by Read/Write when a deadline expired. +// Matches the error returned by os.IsTimeout-style helpers. +var ErrDeadlineExceeded = errors.New("i/o timeout") + +// ErrNoDeadline is returned if SetDeadline is called on an FD whose +// underlying type does not support deadlines. +var ErrNoDeadline = errors.New("file type does not support deadline") + +// pollMode constants must mirror runtime/netpoll_wasip1.go's pollRead/ +// pollWrite values. +const ( + pollModeRead uint8 = 1 + pollModeWrite uint8 = 2 +) + +//go:linkname runtime_netpoll_addwait runtime.runtime_netpoll_addwait +func runtime_netpoll_addwait(fd uint32, mode uint8) uintptr + +//go:linkname runtime_netpoll_done runtime.runtime_netpoll_done +func runtime_netpoll_done(pd uintptr) + +//go:linkname runtime_netpoll_pdfired runtime.runtime_netpoll_pdfired +func runtime_netpoll_pdfired(pd uintptr) bool + +//go:linkname runtime_netpoll_wake runtime.runtime_netpoll_wake +func runtime_netpoll_wake(pd uintptr) + +//go:linkname fd_fdstat_get_type syscall.fd_fdstat_get_type +func fd_fdstat_get_type(fd int) (syscall.Filetype, error) + +// wasiIovec / wasiCiovec mirror wasi-snapshot-preview1's iovec / ciovec +// records: a buffer pointer plus a 32-bit length. Marshalled inline with +// each fd_read / fd_write / sock_recv / sock_send call. +type wasiIovec struct { + buf *byte + bufLen uint32 +} + +// fd_read / fd_write are bound directly here rather than going through +// wasi-libc so the deadline-aware Read/Write loop can observe EAGAIN +// cleanly. Outside package runtime, TinyGo's wasmimport binding +// restricts us to unsafe.Pointer for struct args and uint32 for the +// errno result; the wasi spec uses *iovec and uint16 respectively but +// the wire layout is identical. +// +//go:wasmimport wasi_snapshot_preview1 fd_read +func wasi_fd_read(fd int32, iovs unsafe.Pointer, iovsLen uint32, nread unsafe.Pointer) uint32 + +//go:wasmimport wasi_snapshot_preview1 fd_write +func wasi_fd_write(fd int32, iovs unsafe.Pointer, iovsLen uint32, nwritten unsafe.Pointer) uint32 + +// sock_recv / sock_send are the socket-data wasi syscalls used when the +// FD's Filetype indicates a socket. We bind them here for the same +// reason as fd_read / fd_write: the deadline-aware loop wants direct +// access to the EAGAIN signal without libc's translation layer. +// +//go:wasmimport wasi_snapshot_preview1 sock_recv +func wasi_sock_recv(fd int32, riData unsafe.Pointer, riDataLen uint32, riFlags uint32, roDatalen unsafe.Pointer, roFlags unsafe.Pointer) uint32 + +//go:wasmimport wasi_snapshot_preview1 sock_send +func wasi_sock_send(fd int32, siData unsafe.Pointer, siDataLen uint32, siFlags uint32, soDatalen unsafe.Pointer) uint32 + +// SysFile carries per-FD bookkeeping that upstream Go's poll.FD uses to +// share an underlying syscall FD between an os.File and a net.Conn (see +// Copy below). RefCountPtr / RefCount handle the shared-ownership case; +// Filetype caches the wasi filetype so socket-vs-file dispatch in Read / +// Write is a single integer compare on the hot path. +// +// wasip1 is single-threaded, so the refcount is a plain int — no atomics +// needed. Match upstream's field naming for source-level compatibility +// with code that constructs FDs via struct literal (e.g. upstream's +// net/fd_fake.go). +type SysFile struct { + RefCountPtr *int32 + RefCount int32 + Filetype uint32 +} + +// init lazily allocates the refcount the first time it's needed (i.e. +// the first Init or Copy on this FD). A zero SysFile starts at refcount +// 1 — the FD's sole owner is the caller. +func (s *SysFile) init() { + if s.RefCountPtr == nil { + s.RefCount = 1 + s.RefCountPtr = &s.RefCount + } +} + +// ref increments the shared refcount and returns a SysFile that points +// at the same counter. Used by FD.Copy. +func (s *SysFile) ref() SysFile { + s.init() + *s.RefCountPtr++ + return SysFile{RefCountPtr: s.RefCountPtr, Filetype: s.Filetype} +} + +// destroy decrements the refcount and reports whether the underlying +// syscall FD should now be closed (i.e. this was the last owner). +func (s *SysFile) destroy() bool { + if s.RefCountPtr == nil { + return true + } + *s.RefCountPtr-- + return *s.RefCountPtr <= 0 +} + +// FD is the wasip1 file/socket descriptor wrapped with the bookkeeping +// that net and os rely on. It owns the lifecycle of the underlying +// syscall FD (modulo the Copy / refcount handoff between os.File and +// net.Conn). +// +// The struct mirrors upstream Go's internal/poll.FD field naming +// (Sysfd, IsStream, ZeroReadIsEOF, SysFile) so that upstream's +// net/file_wasip1.go and net/fd_fake.go can construct one via struct +// literal without modification. +type FD struct { + Sysfd int + SysFile SysFile + IsStream bool + ZeroReadIsEOF bool + closed bool + + // Per-FD deadlines, zero means "no deadline". Subsequent Read/Write + // calls observe whatever the current value is at call time; an + // in-flight call uses the deadline it captured at its start. + rDeadline time.Time + wDeadline time.Time +} + +// Init readies the FD for use. When pollable is true (i.e. the FD might +// block — sockets, pipes, FIFOs), Init sets O_NONBLOCK so that +// Read/Write enter the EAGAIN→park retry loop instead of blocking the +// entire wasm module. +// +// Init also caches the wasi filetype so the Read/Write hot path can +// dispatch socket vs file with a single integer compare. +// +// The net argument is currently ignored but kept for parity with +// upstream Go. +func (fd *FD) Init(net string, pollable bool) error { + _ = net + fd.SysFile.init() + if ft, err := fd_fdstat_get_type(fd.Sysfd); err == nil { + fd.SysFile.Filetype = uint32(ft) + } + if !pollable { + return nil + } + flags, err := syscall.Fcntl(fd.Sysfd, syscall.F_GETFL, 0) + if err != nil { + return err + } + if flags&syscall.O_NONBLOCK != 0 { + return nil + } + if _, err := syscall.Fcntl(fd.Sysfd, syscall.F_SETFL, flags|syscall.O_NONBLOCK); err != nil { + return err + } + return nil +} + +// Copy returns a duplicate FD that shares the underlying Sysfd through +// the SysFile refcount. The original and the copy can independently +// call Close — only the last one actually issues the syscall. Used by +// upstream net/file_wasip1.go to hand a socket FD off from an os.File +// to a net.Listener / net.Conn. +func (fd *FD) Copy() FD { + return FD{ + Sysfd: fd.Sysfd, + SysFile: fd.SysFile.ref(), + IsStream: fd.IsStream, + ZeroReadIsEOF: fd.ZeroReadIsEOF, + } +} + +// Close marks the FD closed. The underlying syscall FD is only released +// when the refcount drops to zero — earlier Close calls (e.g. on the +// os.File side after a successful net.FileConn handoff) just decrement. +func (fd *FD) Close() error { + if fd.closed { + return ErrFileClosing + } + fd.closed = true + if !fd.SysFile.destroy() { + return nil + } + return syscall.Close(fd.Sysfd) +} + +// Exist reports whether fd points to an actual FD wrapper (non-nil). +// Callers in package os hold a *FD via a per-target alias and need to +// nil-check without depending on the concrete type being a pointer. +func (fd *FD) Exist() bool { return fd != nil } + +// CloseFunc is the hook upstream net's poll package exposes so tests +// can intercept Close. We just point at syscall.Close. +var CloseFunc func(int) error = syscall.Close + +// String is an internal string definition for methods/functions that +// shouldn't be used outside the stdlib. Upstream net references it +// from rawconn.go to mark methods as not-for-external-use. +type String string + +// RawControl / RawRead / RawWrite back syscall.RawConn's three callback +// methods. They invoke f with the underlying FD; the bool return of +// RawRead / RawWrite controls retry-on-EAGAIN, which we implement by +// parking the goroutine on the netpoll registry and retrying until f +// returns true (the same loop upstream uses). +func (fd *FD) RawControl(f func(uintptr)) error { + if fd.closed { + return ErrFileClosing + } + f(uintptr(fd.Sysfd)) + return nil +} + +func (fd *FD) RawRead(f func(uintptr) bool) error { + if fd.closed { + return ErrFileClosing + } + for { + if f(uintptr(fd.Sysfd)) { + return nil + } + wait(fd.Sysfd, pollModeRead) + } +} + +func (fd *FD) RawWrite(f func(uintptr) bool) error { + if fd.closed { + return ErrFileClosing + } + for { + if f(uintptr(fd.Sysfd)) { + return nil + } + wait(fd.Sysfd, pollModeWrite) + } +} + +// Shutdown calls wasi sock_shutdown. how is one of syscall.SHUT_RD, +// SHUT_WR, SHUT_RDWR. +func (fd *FD) Shutdown(how int) error { + return syscall.Shutdown(fd.Sysfd, how) +} + +// Accept loops over wasi sock_accept, parking the goroutine on EAGAIN +// (waiting for a new connection) through the netpoll registry. Returns +// (newfd, sockaddr=nil, errcall, error) — sockaddr is always nil because +// wasi sock_accept doesn't return one. +func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) { + deadline := fd.rDeadline + for { + if !deadline.IsZero() && !time.Now().Before(deadline) { + return -1, nil, "accept", ErrDeadlineExceeded + } + nfd, _, err := syscall.Accept(fd.Sysfd) + if err == nil { + return nfd, nil, "", nil + } + if err == syscall.EINTR { + continue + } + if err != syscall.EAGAIN { + return -1, nil, "accept", err + } + if deadline.IsZero() { + wait(fd.Sysfd, pollModeRead) + } else { + if perr := fd.parkUntil(pollModeRead, deadline); perr != nil { + return -1, nil, "accept", perr + } + } + } +} + +// isSocket reports whether the cached Filetype is a stream or datagram +// socket. False if Init was never called (Filetype stays 0 = UNKNOWN). +func (fd *FD) isSocket() bool { + ft := syscall.Filetype(fd.SysFile.Filetype) + return ft == syscall.FILETYPE_SOCKET_STREAM || ft == syscall.FILETYPE_SOCKET_DGRAM +} + +// Read reads from the FD into p. Sockets dispatch to sock_recv (which +// honours fd.rDeadline directly); regular files/pipes go through +// syscall.Read for the no-deadline fast path or readWithDeadline when +// a deadline is set. +func (fd *FD) Read(p []byte) (int, error) { + if fd.closed { + return 0, ErrFileClosing + } + if len(p) == 0 { + return 0, nil + } + if fd.isSocket() { + return fd.sockRecv(p) + } + if fd.rDeadline.IsZero() { + return syscall.Read(fd.Sysfd, p) + } + return fd.readWithDeadline(p) +} + +// Write writes p to the FD. Sockets dispatch to sock_send. Regular +// files / pipes go through syscall.Write or writeWithDeadline. +func (fd *FD) Write(p []byte) (int, error) { + if fd.closed { + return 0, ErrFileClosing + } + if fd.isSocket() { + return fd.sockSend(p) + } + if fd.wDeadline.IsZero() { + return syscall.Write(fd.Sysfd, p) + } + return fd.writeWithDeadline(p) +} + +// Pread reads from the FD at the given offset. Always file semantics — +// sockets aren't seekable so this never goes through sock_recv. +func (fd *FD) Pread(p []byte, off int64) (int, error) { + if fd.closed { + return 0, ErrFileClosing + } + return syscall.Pread(fd.Sysfd, p, off) +} + +// Pwrite writes to the FD at the given offset. +func (fd *FD) Pwrite(p []byte, off int64) (int, error) { + if fd.closed { + return 0, ErrFileClosing + } + return syscall.Pwrite(fd.Sysfd, p, off) +} + +// SetDeadline sets both the read and write deadlines. +func (fd *FD) SetDeadline(t time.Time) error { + fd.rDeadline = t + fd.wDeadline = t + return nil +} + +// SetReadDeadline sets the deadline for future Read calls. A zero t +// clears the deadline. +func (fd *FD) SetReadDeadline(t time.Time) error { + fd.rDeadline = t + return nil +} + +// SetWriteDeadline sets the deadline for future Write calls. +func (fd *FD) SetWriteDeadline(t time.Time) error { + fd.wDeadline = t + return nil +} + +// readWithDeadline implements the EAGAIN→park retry loop with deadline +// cancellation for non-socket FDs. The deadline is captured at function +// entry; later SetReadDeadline calls don't affect this in-flight Read. +func (fd *FD) readWithDeadline(p []byte) (int, error) { + deadline := fd.rDeadline + iov := wasiIovec{buf: &p[0], bufLen: uint32(len(p))} + for { + if !time.Now().Before(deadline) { + return 0, ErrDeadlineExceeded + } + var n uint32 + errno := wasi_fd_read(int32(fd.Sysfd), unsafe.Pointer(&iov), 1, unsafe.Pointer(&n)) + switch errno { + case 0: + return int(n), nil + case wasiErrnoIntr: + continue + case wasiErrnoAgain: + if err := fd.parkUntil(pollModeRead, deadline); err != nil { + return 0, err + } + default: + return 0, syscall.Errno(errno) + } + } +} + +func (fd *FD) writeWithDeadline(p []byte) (int, error) { + deadline := fd.wDeadline + var nn int + for { + if !time.Now().Before(deadline) { + return nn, ErrDeadlineExceeded + } + if nn == len(p) { + return nn, nil + } + buf := p[nn:] + iov := wasiIovec{buf: &buf[0], bufLen: uint32(len(buf))} + var n uint32 + errno := wasi_fd_write(int32(fd.Sysfd), unsafe.Pointer(&iov), 1, unsafe.Pointer(&n)) + switch errno { + case 0: + nn += int(n) + case wasiErrnoIntr: + // retry + case wasiErrnoAgain: + if err := fd.parkUntil(pollModeWrite, deadline); err != nil { + return nn, err + } + default: + return nn, syscall.Errno(errno) + } + } +} + +// sockRecv is the socket sibling of readWithDeadline / syscall.Read. +// Always issues sock_recv, regardless of deadline state, so callers +// that don't want fd_read on a socket get a guaranteed sock_recv path. +func (fd *FD) sockRecv(p []byte) (int, error) { + deadline := fd.rDeadline + iov := wasiIovec{buf: &p[0], bufLen: uint32(len(p))} + for { + if !deadline.IsZero() && !time.Now().Before(deadline) { + return 0, ErrDeadlineExceeded + } + var n uint32 + var roFlags uint32 + errno := wasi_sock_recv(int32(fd.Sysfd), unsafe.Pointer(&iov), 1, 0, unsafe.Pointer(&n), unsafe.Pointer(&roFlags)) + switch errno { + case 0: + return int(n), nil + case wasiErrnoIntr: + continue + case wasiErrnoAgain: + if deadline.IsZero() { + wait(fd.Sysfd, pollModeRead) + } else if err := fd.parkUntil(pollModeRead, deadline); err != nil { + return 0, err + } + default: + return 0, syscall.Errno(errno) + } + } +} + +func (fd *FD) sockSend(p []byte) (int, error) { + deadline := fd.wDeadline + var nn int + for { + if !deadline.IsZero() && !time.Now().Before(deadline) { + return nn, ErrDeadlineExceeded + } + if nn == len(p) { + return nn, nil + } + buf := p[nn:] + iov := wasiIovec{buf: &buf[0], bufLen: uint32(len(buf))} + var n uint32 + errno := wasi_sock_send(int32(fd.Sysfd), unsafe.Pointer(&iov), 1, 0, unsafe.Pointer(&n)) + switch errno { + case 0: + nn += int(n) + case wasiErrnoIntr: + // retry + case wasiErrnoAgain: + if deadline.IsZero() { + wait(fd.Sysfd, pollModeWrite) + } else if err := fd.parkUntil(pollModeWrite, deadline); err != nil { + return nn, err + } + default: + return nn, syscall.Errno(errno) + } + } +} + +// wasi snapshot preview1 errno values, kept here so the read/write loops +// can compare without dragging in the full syscall errno table for a +// switch on three constants. These match the wasi spec exactly; see also +// syscall_libc_wasi.go in package syscall. +const ( + wasiErrnoAgain uint32 = 6 + wasiErrnoIntr uint32 = 27 +) + +// wait parks the current goroutine until the FD becomes ready in the +// given direction. No deadline; mirrors the helper of the same name in +// package syscall (intentionally duplicated rather than linknamed +// across the package boundary — see project memory on shim avoidance). +func wait(fd int, mode uint8) { + pd := runtime_netpoll_addwait(uint32(fd), mode) + task.Pause() + runtime_netpoll_done(pd) +} + +// parkUntil parks the current goroutine on (fd, mode) with a deadline. +// Returns nil if the FD became ready or the timer fired (caller's loop +// re-checks the deadline at top); ErrDeadlineExceeded if the deadline +// was already in the past. +// +// Race handling: the deadline timer's callback and pollIO's event walk +// can both target the same pollDesc. The pd.fired flag guards against +// double-pushing the task to the run queue; whichever arrives second +// is a no-op. +func (fd *FD) parkUntil(mode uint8, deadline time.Time) error { + d := time.Until(deadline) + if d <= 0 { + return ErrDeadlineExceeded + } + pd := runtime_netpoll_addwait(uint32(fd.Sysfd), mode) + timer := time.AfterFunc(d, func() { + runtime_netpoll_wake(pd) + }) + task.Pause() + timer.Stop() + runtime_netpoll_done(pd) + return nil +} diff --git a/src/os/file_unix.go b/src/os/file_unix.go index 17d26e166b..997894eaad 100644 --- a/src/os/file_unix.go +++ b/src/os/file_unix.go @@ -37,9 +37,19 @@ func rename(oldname, newname string) error { // can overwrite this data, which could cause the finalizer // to close the wrong file descriptor. type file struct { - handle FileHandle - name string - dirinfo *dirInfo // nil unless directory being read + handle FileHandle + name string + dirinfo *dirInfo // nil unless directory being read + + // pfd is set on wasip1 by (*File).PollFD to a *poll.FD that wraps + // the underlying syscall FD. When set, Close routes through it so + // the refcount semantics shared with net.FileListener / net.FileConn + // are honoured. On non-wasip1 builds pfd is a literal empty struct + // (see pollfd_other.go) and stays at zero bytes — provided it's not + // the last field of this struct, which is why it lives here above + // appendMode rather than at the end. + pfd pollFD + appendMode bool } @@ -48,6 +58,9 @@ func (f *file) close() (err error) { f.dirinfo.close() f.dirinfo = nil } + if f.pfd.Exist() { + return f.pfd.Close() + } return f.handle.Close() } diff --git a/src/os/file_wasip1.go b/src/os/file_wasip1.go new file mode 100644 index 0000000000..d8f680ca5a --- /dev/null +++ b/src/os/file_wasip1.go @@ -0,0 +1,38 @@ +//go:build wasip1 + +package os + +import "internal/poll" + +// PollFD returns the *poll.FD wrapping this file's underlying syscall +// FD. The first call lazily allocates and caches the *poll.FD on the +// File; subsequent calls return the same pointer so that refcount +// semantics shared with net.FileListener / net.FileConn (via +// poll.FD.Copy) work correctly: +// +// - net.FileListener(f) calls f.PollFD().Copy(); the Copy increments +// the refcount via the cached *poll.FD's SysFile. +// - f.Close() routes through the cached *poll.FD's Close (see +// file_unix.go's file.close), which decrements the refcount and +// only releases the syscall FD when the count reaches zero. +// - The eventual Listener.Close / Conn.Close decrements the refcount +// from the other side. +// +// PollFD is intended for use by upstream Go's net/file_wasip1.go (which +// reaches it via a //go:linkname-style type assertion in this package). +func (f *File) PollFD() *poll.FD { + if f.handle == nil { + return nil + } + if f.pfd != nil { + return f.pfd + } + pfd := &poll.FD{ + Sysfd: int(f.handle.(interface{ Fd() uintptr }).Fd()), + IsStream: true, + } + pfd.SysFile.RefCount = 1 + pfd.SysFile.RefCountPtr = &pfd.SysFile.RefCount + f.pfd = pfd + return pfd +} diff --git a/src/os/pollfd_other.go b/src/os/pollfd_other.go new file mode 100644 index 0000000000..18fa26f564 --- /dev/null +++ b/src/os/pollfd_other.go @@ -0,0 +1,12 @@ +//go:build !wasip1 + +package os + +// pollFD is a literal empty struct on non-wasip1 targets. As long as the +// field is not the last in its containing struct, Go gives a zero-sized +// non-trailing field a true zero byte layout — file then occupies exactly +// the same space as it would without the field at all. +type pollFD struct{} + +func (pollFD) Close() error { return nil } +func (pollFD) Exist() bool { return false } diff --git a/src/os/pollfd_wasip1.go b/src/os/pollfd_wasip1.go new file mode 100644 index 0000000000..34626acc6f --- /dev/null +++ b/src/os/pollfd_wasip1.go @@ -0,0 +1,12 @@ +//go:build wasip1 + +package os + +import "internal/poll" + +// pollFD on wasip1 is the *poll.FD that backs net.FileListener / +// net.FileConn handoffs. The alias makes file.pfd directly typed as +// *poll.FD so PollFD reads/writes need no type conversion. The Exist +// method on *poll.FD (defined in internal/poll) absorbs the nil-check +// that file.close needs on the shared code path. +type pollFD = *poll.FD diff --git a/src/runtime/netpoll_wasip1.go b/src/runtime/netpoll_wasip1.go new file mode 100644 index 0000000000..a5b4cc66c9 --- /dev/null +++ b/src/runtime/netpoll_wasip1.go @@ -0,0 +1,230 @@ +//go:build wasip1 && (scheduler.tasks || scheduler.asyncify) + +package runtime + +import ( + "internal/task" + "unsafe" +) + +// pollMode identifies the I/O direction a goroutine is waiting on. +// Zero is intentionally invalid so an uninitialized pollDesc cannot +// silently look like a read waiter. +type pollMode uint8 + +const ( + pollRead pollMode = 1 + pollWrite pollMode = 2 +) + +// pollDesc tracks one parked goroutine waiting for an FD to become ready. +// It is created by netpollAddWait, kept alive by activePolls, and freed +// (eventually GC'd) once unlinked. +type pollDesc struct { + fd uint32 + mode pollMode + fired bool // set by pollIO when the wait is satisfied; netpollDone uses this for idempotency + task *task.Task + bnxt *pollDesc // chain in activePolls +} + +var ( + // activePolls is the singly-linked list of all currently-parked FD + // waiters. wasip1 is single-threaded — every mutation happens from + // the running goroutine or the scheduler loop, never both. + activePolls *pollDesc + pollCount int + + // Scratch buffers for poll_oneoff. Grown on demand, never shrunk — + // the working set settles on a stable max. + pollSubs []__wasi_subscription_t + pollEvents []__wasi_event_t +) + +// netpollAddWait registers the calling goroutine's interest in fd / mode +// and returns a descriptor identifying the wait. The caller must: +// +// 1. call task.Pause() to suspend until the FD is ready (or the task is +// woken for some other reason — timer, manual scheduleTask), and +// 2. call netpollDone(pd) after Pause returns to deregister. +// +// Multiple waiters on the same (fd, mode) pair are supported; each gets +// its own pollDesc and its own subscription in the next poll_oneoff call. +func netpollAddWait(fd uint32, mode pollMode) *pollDesc { + pd := &pollDesc{ + fd: fd, + mode: mode, + task: task.Current(), + bnxt: activePolls, + } + activePolls = pd + pollCount++ + return pd +} + +// netpollDone removes pd from activePolls if it is still registered. +// Idempotent — if pollIO has already woken the waiter, pd.fired is true +// and this is a no-op. +func netpollDone(pd *pollDesc) { + if pd.fired { + return + } + pp := &activePolls + for *pp != nil { + if *pp == pd { + *pp = pd.bnxt + pd.bnxt = nil + pollCount-- + return + } + pp = &(*pp).bnxt + } +} + +// pollIO is the cooperative scheduler's blocking wait on wasip1. It +// invokes poll_oneoff with one subscription per pollDesc currently in +// activePolls, plus optionally a clock subscription. +// +// timeoutNs > 0 : add a clock subscription with this nanosecond timeout. +// timeoutNs == 0 : non-blocking poll, no clock sub. (Forward-looking; the +// v1 scheduler does not invoke this path.) +// timeoutNs < 0 : block until any FD is ready, no clock sub. Caller must +// ensure pollCount > 0 — calling poll_oneoff with zero +// subscriptions returns EINVAL. +// +// Tasks whose subscriptions fire are pushed onto runqueue. The caller +// (the scheduler) re-walks the sleep / timer queues on its next loop +// iteration to handle clock fires. +func pollIO(timeoutNs int64) { + addClock := timeoutNs > 0 + nsubs := pollCount + if addClock { + nsubs++ + } + if nsubs == 0 { + // Caller is responsible for not invoking pollIO with nothing to + // wait on; bail out rather than calling poll_oneoff with zero + // subscriptions. + return + } + if cap(pollSubs) < nsubs { + pollSubs = make([]__wasi_subscription_t, nsubs) + pollEvents = make([]__wasi_event_t, nsubs) + } else { + pollSubs = pollSubs[:nsubs] + pollEvents = pollEvents[:nsubs] + } + + i := 0 + for pd := activePolls; pd != nil; pd = pd.bnxt { + var et __wasi_eventtype_t + if pd.mode == pollRead { + et = __wasi_eventtype_t_fd_read + } else { + et = __wasi_eventtype_t_fd_write + } + pollSubs[i].userData = uint64(uintptr(unsafe.Pointer(pd))) + pollSubs[i].u.setFDReadWrite(et, pd.fd) + i++ + } + + if addClock { + pollSubs[i].userData = 0 + pollSubs[i].u.setClock(0, uint64(timeoutNs), timePrecisionNanoseconds, 0) + i++ + } + + var nevents uint32 + poll_oneoff(&pollSubs[0], &pollEvents[0], uint32(nsubs), &nevents) + + for k := uint32(0); k < nevents; k++ { + ev := &pollEvents[k] + if ev.userData == 0 { + continue + } + pd := (*pollDesc)(unsafe.Pointer(uintptr(ev.userData))) + if pd.fired { + continue + } + pd.fired = true + pp := &activePolls + for *pp != nil { + if *pp == pd { + *pp = pd.bnxt + pd.bnxt = nil + pollCount-- + break + } + pp = &(*pp).bnxt + } + runqueue.Push(pd.task) + } +} + +// runtime_netpoll_addwait is the linkname target used by package syscall +// (and any future package using //go:linkname into runtime) to register +// a wait on an FD without sharing the runtime's pollDesc / pollMode +// types. The returned uintptr is an opaque pollDesc pointer; callers +// must pass it back to runtime_netpoll_done. +// +// mode must be one of pollRead (1) or pollWrite (2). +// +//go:linkname runtime_netpoll_addwait +func runtime_netpoll_addwait(fd uint32, mode uint8) uintptr { + return uintptr(unsafe.Pointer(netpollAddWait(fd, pollMode(mode)))) +} + +// runtime_netpoll_done is the linkname target used by package syscall to +// release a pollDesc previously returned by runtime_netpoll_addwait. +// Idempotent; safe to call whether or not pollIO has already woken the +// waiter. +// +//go:linkname runtime_netpoll_done +func runtime_netpoll_done(pd uintptr) { + if pd == 0 { + return + } + netpollDone((*pollDesc)(unsafe.Pointer(pd))) +} + +// runtime_netpoll_pdfired reports whether the given pollDesc has already +// been woken (either by a poll_oneoff event or by a manual wake). Used +// by deadline-driven cancellation paths to avoid double-waking a task. +// +//go:linkname runtime_netpoll_pdfired +func runtime_netpoll_pdfired(pd uintptr) bool { + if pd == 0 { + return true + } + return (*pollDesc)(unsafe.Pointer(pd)).fired +} + +// runtime_netpoll_wake wakes the task parked on pd from outside the +// poll_oneoff event loop — for example, from a deadline timer's +// callback. Idempotent: a second call (or a race with pollIO firing +// the same pd) is a no-op thanks to the pd.fired flag. +// +// wasip1 is single-threaded so we don't need atomic ops here. +// +//go:linkname runtime_netpoll_wake +func runtime_netpoll_wake(pd uintptr) { + if pd == 0 { + return + } + p := (*pollDesc)(unsafe.Pointer(pd)) + if p.fired { + return + } + p.fired = true + pp := &activePolls + for *pp != nil { + if *pp == p { + *pp = p.bnxt + p.bnxt = nil + pollCount-- + break + } + pp = &(*pp).bnxt + } + runqueue.Push(p.task) +} diff --git a/src/runtime/runtime_wasip1.go b/src/runtime/runtime_wasip1.go index d680fad17e..3cc911ec46 100644 --- a/src/runtime/runtime_wasip1.go +++ b/src/runtime/runtime_wasip1.go @@ -78,11 +78,6 @@ var ( sleepTicksNEvents uint32 ) -func sleepTicks(d timeUnit) { - sleepTicksSubscription.u.u.timeout = uint64(d) - poll_oneoff(&sleepTicksSubscription, &sleepTicksResult, 1, &sleepTicksNEvents) -} - func ticks() timeUnit { var nano uint64 clock_time_get(0, timePrecisionNanoseconds, &nano) @@ -106,9 +101,9 @@ func poll_oneoff(in *__wasi_subscription_t, out *__wasi_event_t, nsubscriptions type __wasi_eventtype_t = uint8 const ( - __wasi_eventtype_t_clock __wasi_eventtype_t = 0 - // TODO: __wasi_eventtype_t_fd_read __wasi_eventtype_t = 1 - // TODO: __wasi_eventtype_t_fd_write __wasi_eventtype_t = 2 + __wasi_eventtype_t_clock __wasi_eventtype_t = iota + __wasi_eventtype_t_fd_read + __wasi_eventtype_t_fd_write ) type ( @@ -118,10 +113,12 @@ type ( u __wasi_subscription_u_t } + // The union payload is sized by the largest variant (clock, 32 bytes after + // the tag and its 7-byte alignment pad). FD read/write subscriptions reuse + // the same memory via setFDReadWrite. __wasi_subscription_u_t struct { tag __wasi_eventtype_t - // TODO: support fd_read/fd_write event u __wasi_subscription_clock_t } @@ -134,6 +131,28 @@ type ( } ) +// __wasi_subscription_fd_readwrite_t is the FD variant of the subscription +// union payload. It overlays the first 4 bytes of the clock variant. +type __wasi_subscription_fd_readwrite_t struct { + fd uint32 +} + +func (s *__wasi_subscription_u_t) setClock(id uint32, timeoutNs, precision uint64, flags uint16) { + s.tag = __wasi_eventtype_t_clock + s.u = __wasi_subscription_clock_t{ + id: id, + timeout: timeoutNs, + precision: precision, + flags: flags, + } +} + +func (s *__wasi_subscription_u_t) setFDReadWrite(eventType __wasi_eventtype_t, fd uint32) { + s.tag = eventType + s.u = __wasi_subscription_clock_t{} + (*__wasi_subscription_fd_readwrite_t)(unsafe.Pointer(&s.u)).fd = fd +} + type ( // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#-event-record __wasi_event_t struct { @@ -141,11 +160,18 @@ type ( errno uint16 eventType __wasi_eventtype_t - // only used for fd_read or fd_write events - // TODO: support fd_read/fd_write event - _ struct { + // fdReadWrite is populated by poll_oneoff for fd_read / fd_write events. + // For clock events the field is zero. Reading nBytes/flags after a + // clock event is meaningless but not unsafe. + fdReadWrite struct { nBytes uint64 flags uint16 } } ) + +// Compile-time size assertions for the wasip1 ABI. If these fail to compile +// the struct layout drifted from the spec and poll_oneoff would corrupt +// memory. +var _ [0]byte = [48 - unsafe.Sizeof(__wasi_subscription_t{})]byte{} +var _ [0]byte = [32 - unsafe.Sizeof(__wasi_event_t{})]byte{} diff --git a/src/runtime/scheduler_idle_wasip1.go b/src/runtime/scheduler_idle_wasip1.go new file mode 100644 index 0000000000..5cb913f97c --- /dev/null +++ b/src/runtime/scheduler_idle_wasip1.go @@ -0,0 +1,32 @@ +//go:build wasip1 && (scheduler.tasks || scheduler.asyncify) + +package runtime + +// sleepTicks is the cooperative scheduler's "wait until the next deadline" +// primitive on wasip1. It is only called by the scheduler when the run queue +// is empty and there's a sleeping task or pending timer due in d ticks. +// +// If any FD waiters are registered via netpollAddWait, this routes through +// pollIO so the same poll_oneoff call observes both the clock subscription +// and the FD subscriptions. With no FD waiters it falls back to the cheap +// single-clock-subscription path. +func sleepTicks(d timeUnit) { + if pollCount > 0 { + pollIO(ticksToNanoseconds(d)) + return + } + sleepTicksSubscription.u.u.timeout = uint64(d) + poll_oneoff(&sleepTicksSubscription, &sleepTicksResult, 1, &sleepTicksNEvents) +} + +// waitForEvents is the cooperative scheduler's "wait until something external +// happens" primitive. It is only called when both the run queue and the +// timer/sleep queues are empty. With no FD waiters this is a genuine +// deadlock; with FD waiters we block until any of them is ready. +func waitForEvents() { + if pollCount > 0 { + pollIO(-1) + return + } + runtimePanic("deadlocked: no event source") +} diff --git a/src/runtime/scheduler_idle_wasip1_none.go b/src/runtime/scheduler_idle_wasip1_none.go new file mode 100644 index 0000000000..7bc933b0e2 --- /dev/null +++ b/src/runtime/scheduler_idle_wasip1_none.go @@ -0,0 +1,19 @@ +//go:build wasip1 && !scheduler.tasks && !scheduler.asyncify + +package runtime + +// sleepTicks blocks the current execution context for d ticks. This is the +// fallback used when no cooperative scheduler is configured (-scheduler=none +// or -scheduler=threads on wasip1) and it has no FD-polling integration — +// see scheduler_idle_wasip1.go for the cooperative variant. +func sleepTicks(d timeUnit) { + sleepTicksSubscription.u.u.timeout = uint64(d) + poll_oneoff(&sleepTicksSubscription, &sleepTicksResult, 1, &sleepTicksNEvents) +} + +// waitForEvents is only meaningful when there's an event source available. +// Without the cooperative scheduler running poll_oneoff on FDs, wasip1 has +// nothing to wake on, so this is a hard deadlock. +func waitForEvents() { + runtimePanic("deadlocked: no event source") +} diff --git a/src/runtime/wait_other.go b/src/runtime/wait_other.go index f1487e3969..ebac63d743 100644 --- a/src/runtime/wait_other.go +++ b/src/runtime/wait_other.go @@ -1,4 +1,4 @@ -//go:build !tinygo.riscv && !cortexm && !(linux && !baremetal && !tinygo.wasm && !nintendoswitch) && !darwin +//go:build !tinygo.riscv && !cortexm && !(linux && !baremetal && !tinygo.wasm && !nintendoswitch) && !darwin && !wasip1 package runtime diff --git a/src/syscall/net_wasip1.go b/src/syscall/net_wasip1.go new file mode 100644 index 0000000000..6829337aad --- /dev/null +++ b/src/syscall/net_wasip1.go @@ -0,0 +1,163 @@ +//go:build wasip1 + +package syscall + +import "unsafe" + +// Sockaddr is the wasip1 socket-address sentinel. wasip1 socket syscalls +// don't surface peer addresses (sock_accept doesn't return one), so any +// Sockaddr-typed return is always nil. Defined as `any` to match +// upstream Go's net_fake.go. +type Sockaddr = any + +// Concrete sockaddr types exist so upstream Go's net package compiles; +// none of them are ever populated (Accept always returns nil). +type SockaddrInet4 struct { + Port int + Addr [4]byte +} + +type SockaddrInet6 struct { + Port int + ZoneId uint32 + Addr [16]byte +} + +type SockaddrUnix struct { + Name string +} + +// Address-family / socket-type / protocol constants. AF_INET / AF_INET6 +// are already defined in syscall.go (Linux values, 0x2 / 0xa); we add +// the rest here. wasip1's host never reads these — they exist so +// upstream Go's net builds. +const ( + AF_UNSPEC = 0 + AF_UNIX = 1 +) + +const ( + SOCK_STREAM = 1 + iota + SOCK_DGRAM + SOCK_RAW + SOCK_SEQPACKET +) + +const ( + IPPROTO_IP = 0 + IPPROTO_IPV4 = 4 + IPPROTO_IPV6 = 0x29 + IPPROTO_TCP = 6 + IPPROTO_UDP = 0x11 +) + +const SOMAXCONN = 0x80 + +// Socket-option / fcntl constants used by upstream net but unsupported +// on wasip1; they exist so the build compiles. +const ( + IPV6_V6ONLY = 1 + SO_ERROR = 2 +) + +const F_DUPFD_CLOEXEC = 1 + +// RLIMIT_NOFILE is referenced by net's rlimit_unix.go. Rlimit / +// Setrlimit are defined in syscall.go; we add the missing constant and +// a Getrlimit stub here. +const RLIMIT_NOFILE = 0 + +func Getrlimit(which int, lim *Rlimit) error { return ENOSYS } + +const ( + SHUT_RD = 0x1 + SHUT_WR = 0x2 + SHUT_RDWR = SHUT_RD | SHUT_WR +) + +// sock_recv ri_flags / sock_send si_flags. Currently only the receive +// flags have public counterparts in wasi-libc; we expose them for +// callers that want MSG_PEEK-style behaviour. internal/poll's hot-path +// Read/Write pass 0. +const ( + MSG_PEEK = 0x1 + MSG_WAITALL = 0x2 +) + +// wasi flag types. fdflags is shared with syscall_libc_wasi.go's O_* +// constants (e.g. O_NONBLOCK = __WASI_FDFLAGS_NONBLOCK = 4). +type ( + fdflags = uint16 + sdflags = uint32 + riflags = uint16 + roflags = uint16 + siflags = uint16 +) + +//go:wasmimport wasi_snapshot_preview1 sock_accept +//go:noescape +func sock_accept(fd int32, flags fdflags, newfd unsafe.Pointer) uint32 + +//go:wasmimport wasi_snapshot_preview1 sock_shutdown +//go:noescape +func sock_shutdown(fd int32, flags sdflags) uint32 + +// Accept wraps wasi sock_accept. The returned Sockaddr is always nil +// because wasi preview1 doesn't surface the peer address. The accepted +// FD inherits the listener's flags, including O_NONBLOCK — pass +// __WASI_FDFLAGS_NONBLOCK explicitly so we don't depend on inheritance +// semantics that vary between hosts. +func Accept(fd int) (int, Sockaddr, error) { + var newfd int32 + errno := sock_accept(int32(fd), __WASI_FDFLAGS_NONBLOCK, unsafe.Pointer(&newfd)) + if errno != 0 { + return -1, nil, Errno(errno) + } + return int(newfd), nil, nil +} + +// Shutdown wraps wasi sock_shutdown. how is one of SHUT_RD, SHUT_WR, +// SHUT_RDWR. +func Shutdown(fd int, how int) error { + if errno := sock_shutdown(int32(fd), sdflags(how)); errno != 0 { + return Errno(errno) + } + return nil +} + +// The remaining socket-related entry points exist as stubs because +// upstream Go's net package references them on the wasip1 build path, +// even though the FileConn / FileListener flow we care about doesn't +// reach them. Each one returns ENOSYS so callers see a clean error. + +func Socket(proto, sotype, unused int) (int, error) { return -1, ENOSYS } + +func Bind(fd int, sa Sockaddr) error { return ENOSYS } + +func Listen(fd int, backlog int) error { return ENOSYS } + +func Connect(fd int, sa Sockaddr) error { return ENOSYS } + +func Recvfrom(fd int, p []byte, flags int) (int, Sockaddr, error) { + return 0, nil, ENOSYS +} + +func Sendto(fd int, p []byte, flags int, to Sockaddr) error { return ENOSYS } + +func Recvmsg(fd int, p, oob []byte, flags int) (n, oobn, recvflags int, from Sockaddr, err error) { + return 0, 0, 0, nil, ENOSYS +} + +func SendmsgN(fd int, p, oob []byte, to Sockaddr, flags int) (int, error) { + return 0, ENOSYS +} + +func GetsockoptInt(fd, level, opt int) (int, error) { return 0, ENOSYS } + +func SetsockoptInt(fd, level, opt int, value int) error { return ENOSYS } + +func SetReadDeadline(fd int, t int64) error { return ENOSYS } + +func SetWriteDeadline(fd int, t int64) error { return ENOSYS } + +func StopIO(fd int) error { return ENOSYS } diff --git a/src/syscall/syscall_fcntl_wasip1.go b/src/syscall/syscall_fcntl_wasip1.go new file mode 100644 index 0000000000..4158994671 --- /dev/null +++ b/src/syscall/syscall_fcntl_wasip1.go @@ -0,0 +1,86 @@ +//go:build wasip1 + +package syscall + +import "unsafe" + +// __wasi_fdstat_t mirrors the wasip1 fdstat record. Per the spec +// (https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#-fdstat-record): +// +// size: 24, align: 8 +// fs_filetype: u8 at offset 0 +// fs_flags: u16 at offset 2 +// fs_rights_base: u64 at offset 8 +// fs_rights_inheriting: u64 at offset 16 +type __wasi_fdstat_t struct { + fsFiletype uint8 + _ uint8 + fsFlags uint16 + _ [4]byte + fsRightsBase uint64 + fsRightsInheriting uint64 +} + +var _ [0]byte = [24 - unsafe.Sizeof(__wasi_fdstat_t{})]byte{} + +//go:wasmimport wasi_snapshot_preview1 fd_fdstat_get +func fd_fdstat_get(fd int32, out *__wasi_fdstat_t) uint16 + +//go:wasmimport wasi_snapshot_preview1 fd_fdstat_set_flags +func fd_fdstat_set_flags(fd int32, flags uint16) uint16 + +// Fcntl is a minimal subset of POSIX fcntl backed by wasip1's fd_fdstat +// primitives. Only F_GETFL and F_SETFL are supported on wasip1 (these are +// the only commands TinyGo's runtime needs for setting O_NONBLOCK). The +// libc fcntl path can't be used because wasi-libc's fcntl is variadic and +// the Go wasmimport binding has no way to express that. +func Fcntl(fd int, cmd int, arg int) (val int, err error) { + switch cmd { + case F_GETFL: + var st __wasi_fdstat_t + if errno := fd_fdstat_get(int32(fd), &st); errno != 0 { + err = Errno(errno) + return + } + return int(st.fsFlags), nil + case F_SETFL: + if errno := fd_fdstat_set_flags(int32(fd), uint16(arg)); errno != 0 { + err = Errno(errno) + return + } + return 0, nil + default: + err = ENOSYS + return + } +} + +// Filetype is the wasi filetype tag returned by fd_fdstat_get for any +// open file descriptor. Used by upstream net/file_wasip1.go to decide +// whether a pre-opened FD should be wrapped as net.Listener (stream +// socket) or net.Conn (stream / dgram socket). +type Filetype = uint8 + +const ( + FILETYPE_UNKNOWN Filetype = 0 + FILETYPE_BLOCK_DEVICE Filetype = 1 + FILETYPE_CHARACTER_DEVICE Filetype = 2 + FILETYPE_DIRECTORY Filetype = 3 + FILETYPE_REGULAR_FILE Filetype = 4 + FILETYPE_SOCKET_DGRAM Filetype = 5 + FILETYPE_SOCKET_STREAM Filetype = 6 + FILETYPE_SYMBOLIC_LINK Filetype = 7 +) + +// fd_fdstat_get_type returns the wasi filetype of fd. Used by upstream +// Go's net/file_wasip1.go via //go:linkname syscall.fd_fdstat_get_type +// to detect socket FDs handed in by the host runtime. +// +//go:linkname fd_fdstat_get_type +func fd_fdstat_get_type(fd int) (Filetype, error) { + var st __wasi_fdstat_t + if errno := fd_fdstat_get(int32(fd), &st); errno != 0 { + return 0, Errno(errno) + } + return st.fsFiletype, nil +} diff --git a/src/syscall/syscall_libc.go b/src/syscall/syscall_libc.go index 2e8f5e3112..c166bba15a 100644 --- a/src/syscall/syscall_libc.go +++ b/src/syscall/syscall_libc.go @@ -27,41 +27,10 @@ func Dup(fd int) (fd2 int, err error) { return } -func Write(fd int, p []byte) (n int, err error) { - buf, count := splitSlice(p) - n = libc_write(int32(fd), buf, uint(count)) - if n < 0 { - err = getErrno() - } - return -} - -func Read(fd int, p []byte) (n int, err error) { - buf, count := splitSlice(p) - n = libc_read(int32(fd), buf, uint(count)) - if n < 0 { - err = getErrno() - } - return -} - -func Pread(fd int, p []byte, offset int64) (n int, err error) { - buf, count := splitSlice(p) - n = libc_pread(int32(fd), buf, uint(count), offset) - if n < 0 { - err = getErrno() - } - return -} - -func Pwrite(fd int, p []byte, offset int64) (n int, err error) { - buf, count := splitSlice(p) - n = libc_pwrite(int32(fd), buf, uint(count), offset) - if n < 0 { - err = getErrno() - } - return -} +// Read, Write, Pread, Pwrite are defined per-build-target so that the +// wasip1 cooperative-scheduler build can wrap the libc syscalls with a +// park-on-EAGAIN loop. See syscall_libc_default.go and +// syscall_libc_wasip1.go. func Seek(fd int, offset int64, whence int) (newoffset int64, err error) { newoffset = libc_lseek(int32(fd), offset, whence) diff --git a/src/syscall/syscall_libc_default.go b/src/syscall/syscall_libc_default.go new file mode 100644 index 0000000000..ad04b7dee0 --- /dev/null +++ b/src/syscall/syscall_libc_default.go @@ -0,0 +1,48 @@ +//go:build js || nintendoswitch || wasip2 || (wasip1 && !scheduler.tasks && !scheduler.asyncify) + +package syscall + +// These are the default Read/Write/Pread/Pwrite implementations for +// libc-backed wasm targets that do NOT have the cooperative scheduler +// + wasip1 netpoll integration. They are simple pass-throughs to the +// underlying libc syscalls and block the entire wasm module if the FD +// is in blocking mode. +// +// The wasip1 + cooperative-scheduler build replaces these with versions +// that park the goroutine on EAGAIN; see syscall_libc_wasip1.go. + +func Write(fd int, p []byte) (n int, err error) { + buf, count := splitSlice(p) + n = libc_write(int32(fd), buf, uint(count)) + if n < 0 { + err = getErrno() + } + return +} + +func Read(fd int, p []byte) (n int, err error) { + buf, count := splitSlice(p) + n = libc_read(int32(fd), buf, uint(count)) + if n < 0 { + err = getErrno() + } + return +} + +func Pread(fd int, p []byte, offset int64) (n int, err error) { + buf, count := splitSlice(p) + n = libc_pread(int32(fd), buf, uint(count), offset) + if n < 0 { + err = getErrno() + } + return +} + +func Pwrite(fd int, p []byte, offset int64) (n int, err error) { + buf, count := splitSlice(p) + n = libc_pwrite(int32(fd), buf, uint(count), offset) + if n < 0 { + err = getErrno() + } + return +} diff --git a/src/syscall/syscall_libc_wasip1.go b/src/syscall/syscall_libc_wasip1.go new file mode 100644 index 0000000000..fea0da9750 --- /dev/null +++ b/src/syscall/syscall_libc_wasip1.go @@ -0,0 +1,110 @@ +//go:build wasip1 && (scheduler.tasks || scheduler.asyncify) + +package syscall + +import ( + "internal/task" + _ "unsafe" // for go:linkname +) + +// pollMode constants must mirror runtime/netpoll_wasip1.go's pollRead/ +// pollWrite. Keep the two definitions in sync. +const ( + pollModeRead uint8 = 1 + pollModeWrite uint8 = 2 +) + +//go:linkname runtime_netpoll_addwait runtime.runtime_netpoll_addwait +func runtime_netpoll_addwait(fd uint32, mode uint8) uintptr + +//go:linkname runtime_netpoll_done runtime.runtime_netpoll_done +func runtime_netpoll_done(pd uintptr) + +// readWritePark is the shared park-on-EAGAIN body for Read, Write, Pread, +// Pwrite. The do() callback performs the underlying libc syscall and +// returns its result; on EAGAIN we register an FD wait, suspend the +// goroutine until the cooperative scheduler's pollIO wakes us, then +// retry. EINTR retries immediately without parking. +func Write(fd int, p []byte) (n int, err error) { + buf, count := splitSlice(p) + for { + n = libc_write(int32(fd), buf, uint(count)) + if n >= 0 { + return + } + switch e := getErrno(); e { + case EAGAIN: + wait(fd, pollModeWrite) + case EINTR: + // retry + default: + err = e + return + } + } +} + +func Read(fd int, p []byte) (n int, err error) { + buf, count := splitSlice(p) + for { + n = libc_read(int32(fd), buf, uint(count)) + if n >= 0 { + return + } + switch e := getErrno(); e { + case EAGAIN: + wait(fd, pollModeRead) + case EINTR: + // retry + default: + err = e + return + } + } +} + +func Pread(fd int, p []byte, offset int64) (n int, err error) { + buf, count := splitSlice(p) + for { + n = libc_pread(int32(fd), buf, uint(count), offset) + if n >= 0 { + return + } + switch e := getErrno(); e { + case EAGAIN: + wait(fd, pollModeRead) + case EINTR: + // retry + default: + err = e + return + } + } +} + +func Pwrite(fd int, p []byte, offset int64) (n int, err error) { + buf, count := splitSlice(p) + for { + n = libc_pwrite(int32(fd), buf, uint(count), offset) + if n >= 0 { + return + } + switch e := getErrno(); e { + case EAGAIN: + wait(fd, pollModeWrite) + case EINTR: + // retry + default: + err = e + return + } + } +} + +// wait parks the current goroutine until the given FD is ready for the +// requested I/O direction, then deregisters it from the poll registry. +func wait(fd int, mode uint8) { + pd := runtime_netpoll_addwait(uint32(fd), mode) + task.Pause() + runtime_netpoll_done(pd) +}