diff --git a/loader/goroot.go b/loader/goroot.go index df4ae40cfe..221e0fe2d8 100644 --- a/loader/goroot.go +++ b/loader/goroot.go @@ -16,6 +16,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "go/build" "io" "io/fs" "os" @@ -48,7 +49,7 @@ func GetCachedGoroot(config *compileopts.Config) (string, error) { overrides := pathsToOverride(config.GoMinorVersion, needsSyscallPackage(config.BuildTags())) // Resolve the merge links within the goroot. - merge, err := listGorootMergeLinks(goroot, tinygoroot, overrides) + merge, err := listGorootMergeLinks(goroot, tinygoroot, overrides, config) if err != nil { return "", err } @@ -143,10 +144,22 @@ func GetCachedGoroot(config *compileopts.Config) (string, error) { } // listGorootMergeLinks searches goroot and tinygoroot for all symlinks that must be created within the merged goroot. -func listGorootMergeLinks(goroot, tinygoroot string, overrides map[string]bool) (map[string]string, error) { +func listGorootMergeLinks(goroot, tinygoroot string, overrides map[string]bool, config *compileopts.Config) (map[string]string, error) { goSrc := filepath.Join(goroot, "src") tinygoSrc := filepath.Join(tinygoroot, "src") merges := make(map[string]string) + + // buildContext is used to evaluate //go:build constraints on TinyGo + // source files. A TinyGo file that doesn't match the current target + // (e.g. a *_wasip2.go file in a wasip1 build) must not be treated as + // "TinyGo owns this directory" — otherwise wasip1 builds would lose + // upstream Go files at the same level. + bctx := build.Default + bctx.GOOS = config.GOOS() + bctx.GOARCH = config.GOARCH() + bctx.BuildTags = config.BuildTags() + bctx.Compiler = "gc" + for dir, merge := range overrides { if !merge { // Use the TinyGo version. @@ -168,6 +181,14 @@ func listGorootMergeLinks(goroot, tinygoroot string, overrides map[string]bool) // Link this file. name := e.Name() + // Only count this file as a TinyGo override of the directory + // when its build tags match the current target. Files with a + // non-matching //go:build are invisible to this build anyway + // (the compiler would skip them), so they shouldn't cause + // upstream files to be dropped from the merge. + if matched, _ := bctx.MatchFile(tinygoDir, name); !matched { + continue + } merges[filepath.Join("src", dir, name)] = filepath.Join(tinygoDir, name) hasTinyGoFiles = true @@ -246,6 +267,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/errors_wasip.go b/src/internal/poll/errors_wasip.go new file mode 100644 index 0000000000..e7c389fbc3 --- /dev/null +++ b/src/internal/poll/errors_wasip.go @@ -0,0 +1,24 @@ +//go:build wasip1 || wasip2 + +// Shared error sentinels for the wasip1 and wasip2 internal/poll +// implementations. The error values are part of the public API surface +// upstream net relies on; sharing them keeps fd_wasip1.go and +// fd_wasip2.go free of redundant declarations. + +package poll + +import "errors" + +// 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") 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..31a93cf52e --- /dev/null +++ b/src/internal/poll/fd_wasip1.go @@ -0,0 +1,534 @@ +//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 ( + "internal/task" + "syscall" + "time" + "unsafe" +) + +// 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/internal/poll/fd_wasip2.go b/src/internal/poll/fd_wasip2.go new file mode 100644 index 0000000000..630b3d4265 --- /dev/null +++ b/src/internal/poll/fd_wasip2.go @@ -0,0 +1,436 @@ +//go:build wasip2 + +// fd_wasip2.go is the wasip2 sibling of fd_wasip1.go: it backs net.TCPListener +// and net.TCPConn with the runtime's pollable-keyed netpoll registry (see +// runtime/netpoll_wasip2.go). The Sysfd of fd_wasip1.go is replaced with a +// triple of wasi resource handles: the tcp-socket itself plus, for an +// accepted/connected connection, the (input-stream, output-stream) pair. + +package poll + +import ( + "errors" + "internal/cm" + "internal/task" + "internal/wasi/io/v0.2.0/poll" + wasistreams "internal/wasi/io/v0.2.0/streams" + "internal/wasi/sockets/v0.2.0/instance-network" + wasinet "internal/wasi/sockets/v0.2.0/network" + wasitcp "internal/wasi/sockets/v0.2.0/tcp" + wasitcpcreate "internal/wasi/sockets/v0.2.0/tcp-create-socket" + "time" + "unsafe" +) + +// ErrFileClosingWasip2 distinguishes the wasip2 close error; reusing the +// wasip1 ErrFileClosing keeps callers in upstream net happy. + +//go:linkname runtime_netpoll_addpollable_wasip2 runtime.runtime_netpoll_addpollable_wasip2 +func runtime_netpoll_addpollable_wasip2(pollable uint32) uintptr + +//go:linkname runtime_netpoll_done_wasip2 runtime.runtime_netpoll_done_wasip2 +func runtime_netpoll_done_wasip2(pd uintptr) + +//go:linkname runtime_netpoll_pdfired_wasip2 runtime.runtime_netpoll_pdfired_wasip2 +func runtime_netpoll_pdfired_wasip2(pd uintptr) bool + +//go:linkname runtime_netpoll_wake_wasip2 runtime.runtime_netpoll_wake_wasip2 +func runtime_netpoll_wake_wasip2(pd uintptr) + +// network is the lazy-initialised wasi:sockets/instance-network handle. +// All TCP operations need a network; instance-network() returns the host's +// default network. We hold a single handle for the program's lifetime. +var ( + wasip2Network wasinet.Network + wasip2NetworkInit bool +) + +func wasip2GetNetwork() wasinet.Network { + if !wasip2NetworkInit { + wasip2Network = instancenetwork.InstanceNetwork() + wasip2NetworkInit = true + } + return wasip2Network +} + +// WasipNFD (named to avoid colliding with the wasip1 FD type that ships in +// the same package via fd_wasip1.go) is the wasip2 file descriptor wrapper. +// Each FD is either a listener (input/output zero-valued) or a connection +// (all three valid). +// +// Public field naming mirrors fd_wasip1.go where it makes sense; callers in +// src/net/*_wasip2.go construct it via the open / dial / listen helpers +// below rather than struct literal. +type WasipNFD struct { + socket wasitcp.TCPSocket + input wasistreams.InputStream + output wasistreams.OutputStream + isListener bool + closed bool + + rDeadline time.Time + wDeadline time.Time +} + +// errorCodeToError maps a wasi network ErrorCode into a Go error. +func errorCodeToError(c wasinet.ErrorCode) error { + if c == wasinet.ErrorCodeWouldBlock { + return errWasip2WouldBlock + } + return errors.New("wasip2 network: " + c.String()) +} + +var errWasip2WouldBlock = errors.New("would block") + +// DialTCPWasip2 creates a TCP socket, starts a connect to remote, parks +// until finish-connect succeeds, and returns the resulting connection FD. +func DialTCPWasip2(remoteIPv4 [4]byte, remotePort uint16) (*WasipNFD, error) { + sockRes := wasitcpcreate.CreateTCPSocket(wasinet.IPAddressFamilyIPv4) + if sockRes.IsErr() { + return nil, errorCodeToError(*sockRes.Err()) + } + sock := *sockRes.OK() + + addr := wasinet.IPSocketAddressIPv4(wasinet.IPv4SocketAddress{ + Port: remotePort, + Address: remoteIPv4, + }) + + startRes := sock.StartConnect(wasip2GetNetwork(), addr) + if startRes.IsErr() { + sock.ResourceDrop() + return nil, errorCodeToError(*startRes.Err()) + } + + for { + finRes := sock.FinishConnect() + if !finRes.IsErr() { + tup := finRes.OK() + return &WasipNFD{ + socket: sock, + input: tup.F0, + output: tup.F1, + }, nil + } + ec := *finRes.Err() + if ec != wasinet.ErrorCodeWouldBlock { + sock.ResourceDrop() + return nil, errorCodeToError(ec) + } + // park on the socket's pollable until connect completes + waitOnPollable(sock.Subscribe()) + } +} + +// ListenTCPWasip2 creates a TCP socket, binds it to localIPv4:localPort, +// puts it in listening mode, and returns the listener FD. +func ListenTCPWasip2(localIPv4 [4]byte, localPort uint16) (*WasipNFD, error) { + sockRes := wasitcpcreate.CreateTCPSocket(wasinet.IPAddressFamilyIPv4) + if sockRes.IsErr() { + return nil, errorCodeToError(*sockRes.Err()) + } + sock := *sockRes.OK() + + addr := wasinet.IPSocketAddressIPv4(wasinet.IPv4SocketAddress{ + Port: localPort, + Address: localIPv4, + }) + + if r := sock.StartBind(wasip2GetNetwork(), addr); r.IsErr() { + sock.ResourceDrop() + return nil, errorCodeToError(*r.Err()) + } + for { + r := sock.FinishBind() + if !r.IsErr() { + break + } + ec := *r.Err() + if ec != wasinet.ErrorCodeWouldBlock { + sock.ResourceDrop() + return nil, errorCodeToError(ec) + } + waitOnPollable(sock.Subscribe()) + } + + if r := sock.StartListen(); r.IsErr() { + sock.ResourceDrop() + return nil, errorCodeToError(*r.Err()) + } + for { + r := sock.FinishListen() + if !r.IsErr() { + break + } + ec := *r.Err() + if ec != wasinet.ErrorCodeWouldBlock { + sock.ResourceDrop() + return nil, errorCodeToError(ec) + } + waitOnPollable(sock.Subscribe()) + } + + return &WasipNFD{socket: sock, isListener: true}, nil +} + +// LocalAddr returns the bound local address of this FD (listener or +// connection). Returns nil if the wasi runtime doesn't surface it. +func (fd *WasipNFD) LocalAddr() (ip [4]byte, port uint16, ok bool) { + r := fd.socket.LocalAddress() + if r.IsErr() { + return ip, 0, false + } + addr := r.OK() + if v4 := addr.IPv4(); v4 != nil { + return v4.Address, v4.Port, true + } + return ip, 0, false +} + +// Accept blocks until an incoming connection is available, then returns +// the connection FD. Honours fd.rDeadline. +func (fd *WasipNFD) Accept() (*WasipNFD, error) { + if fd.closed { + return nil, ErrFileClosing + } + deadline := fd.rDeadline + for { + if !deadline.IsZero() && !time.Now().Before(deadline) { + return nil, ErrDeadlineExceeded + } + r := fd.socket.Accept() + if !r.IsErr() { + tup := r.OK() + return &WasipNFD{ + socket: tup.F0, + input: tup.F1, + output: tup.F2, + }, nil + } + ec := *r.Err() + if ec != wasinet.ErrorCodeWouldBlock { + return nil, errorCodeToError(ec) + } + if deadline.IsZero() { + waitOnPollable(fd.socket.Subscribe()) + } else { + if err := waitOnPollableUntil(fd.socket.Subscribe(), deadline); err != nil { + return nil, err + } + } + } +} + +// Read reads from the connection's input stream. Returns (0, nil) on EOF. +func (fd *WasipNFD) Read(p []byte) (int, error) { + if fd.closed { + return 0, ErrFileClosing + } + if len(p) == 0 { + return 0, nil + } + if fd.isListener { + return 0, errors.New("read on listener FD") + } + deadline := fd.rDeadline + for { + if !deadline.IsZero() && !time.Now().Before(deadline) { + return 0, ErrDeadlineExceeded + } + r := fd.input.Read(uint64(len(p))) + if r.IsErr() { + se := r.Err() + if se.Closed() { + return 0, nil // EOF + } + return 0, errors.New("wasip2 stream read failed") + } + data := r.OK().Slice() + if len(data) > 0 { + n := copy(p, data) + return n, nil + } + // No data available — park on the input stream's pollable. + if deadline.IsZero() { + waitOnPollable(fd.input.Subscribe()) + } else { + if err := waitOnPollableUntil(fd.input.Subscribe(), deadline); err != nil { + return 0, err + } + } + } +} + +// Write writes p to the connection's output stream. Loops until all of p +// is written or an error occurs. Honours fd.wDeadline. +func (fd *WasipNFD) Write(p []byte) (int, error) { + if fd.closed { + return 0, ErrFileClosing + } + if fd.isListener { + return 0, errors.New("write on listener FD") + } + deadline := fd.wDeadline + var nn int + for nn < len(p) { + if !deadline.IsZero() && !time.Now().Before(deadline) { + return nn, ErrDeadlineExceeded + } + cw := fd.output.CheckWrite() + if cw.IsErr() { + se := cw.Err() + if se.Closed() { + return nn, errors.New("wasip2 stream closed") + } + return nn, errors.New("wasip2 stream write check failed") + } + canWrite := uint64(*cw.OK()) + if canWrite == 0 { + if deadline.IsZero() { + waitOnPollable(fd.output.Subscribe()) + } else { + if err := waitOnPollableUntil(fd.output.Subscribe(), deadline); err != nil { + return nn, err + } + } + continue + } + chunk := uint64(len(p) - nn) + if chunk > canWrite { + chunk = canWrite + } + wr := fd.output.Write(cm.ToList(p[nn : nn+int(chunk)])) + if wr.IsErr() { + se := wr.Err() + if se.Closed() { + return nn, errors.New("wasip2 stream closed") + } + return nn, errors.New("wasip2 stream write failed") + } + nn += int(chunk) + } + return nn, nil +} + +// Close drops all wasi resources held by the FD. Idempotent in the sense +// that a second call returns ErrFileClosing without re-dropping (resource +// drop is once-only in the component model). +func (fd *WasipNFD) Close() error { + if fd.closed { + return ErrFileClosing + } + fd.closed = true + // Drop streams first (they reference the socket). + var zeroIn wasistreams.InputStream + if fd.input != zeroIn { + fd.input.ResourceDrop() + } + var zeroOut wasistreams.OutputStream + if fd.output != zeroOut { + fd.output.ResourceDrop() + } + fd.socket.ResourceDrop() + return nil +} + +func (fd *WasipNFD) SetDeadline(t time.Time) error { + fd.rDeadline = t + fd.wDeadline = t + return nil +} + +func (fd *WasipNFD) SetReadDeadline(t time.Time) error { + fd.rDeadline = t + return nil +} + +func (fd *WasipNFD) SetWriteDeadline(t time.Time) error { + fd.wDeadline = t + return nil +} + +// waitOnPollable transfers ownership of the pollable to the runtime +// registry, parks the current goroutine, and returns when the runtime +// drops the pollable + wakes us. +func waitOnPollable(p poll.Pollable) { + handle := cm.Reinterpret[uint32](p) + pd := runtime_netpoll_addpollable_wasip2(handle) + task.Pause() + runtime_netpoll_done_wasip2(pd) +} + +// waitOnPollableUntil parks on the pollable but arms a time.AfterFunc that +// wakes the task if the deadline expires first. Mirrors the wasip1 parkUntil +// pattern from fd_wasip1.go. +func waitOnPollableUntil(p poll.Pollable, deadline time.Time) error { + d := time.Until(deadline) + if d <= 0 { + // Don't even register: drop the pollable and report timeout. + p.ResourceDrop() + return ErrDeadlineExceeded + } + handle := cm.Reinterpret[uint32](p) + pd := runtime_netpoll_addpollable_wasip2(handle) + timer := time.AfterFunc(d, func() { + runtime_netpoll_wake_wasip2(pd) + }) + task.Pause() + timer.Stop() + runtime_netpoll_done_wasip2(pd) + return nil +} + +// Linkname-friendly wrappers around the WasipNFD methods. They use +// uintptr for the FD pointer so callers can hold the FD via a raw +// handle without needing the WasipNFD type in scope (the type itself +// can't easily be linknamed). Used by tests / future net package code. +// +//go:linkname Wasip2TCPListen +func Wasip2TCPListen(ipv4 [4]byte, port uint16) (uintptr, error) { + fd, err := ListenTCPWasip2(ipv4, port) + if err != nil { + return 0, err + } + return uintptr(unsafe.Pointer(fd)), nil +} + +//go:linkname Wasip2TCPDial +func Wasip2TCPDial(ipv4 [4]byte, port uint16) (uintptr, error) { + fd, err := DialTCPWasip2(ipv4, port) + if err != nil { + return 0, err + } + return uintptr(unsafe.Pointer(fd)), nil +} + +//go:linkname Wasip2TCPAccept +func Wasip2TCPAccept(listener uintptr) (uintptr, error) { + fd := (*WasipNFD)(unsafe.Pointer(listener)) + accepted, err := fd.Accept() + if err != nil { + return 0, err + } + return uintptr(unsafe.Pointer(accepted)), nil +} + +//go:linkname Wasip2TCPRead +func Wasip2TCPRead(conn uintptr, p []byte) (int, error) { + fd := (*WasipNFD)(unsafe.Pointer(conn)) + return fd.Read(p) +} + +//go:linkname Wasip2TCPWrite +func Wasip2TCPWrite(conn uintptr, p []byte) (int, error) { + fd := (*WasipNFD)(unsafe.Pointer(conn)) + return fd.Write(p) +} + +//go:linkname Wasip2TCPClose +func Wasip2TCPClose(fd uintptr) error { + return (*WasipNFD)(unsafe.Pointer(fd)).Close() +} + +//go:linkname Wasip2TCPSetDeadline +func Wasip2TCPSetDeadline(fd uintptr, t time.Time) error { + return (*WasipNFD)(unsafe.Pointer(fd)).SetDeadline(t) +} 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/poll_link_wasip2.go b/src/os/poll_link_wasip2.go new file mode 100644 index 0000000000..7b2ba2e170 --- /dev/null +++ b/src/os/poll_link_wasip2.go @@ -0,0 +1,11 @@ +//go:build wasip2 + +package os + +import ( + // Pulls internal/poll into the build for wasip2 so its TCP/pollable + // surface (WasipNFD, DialTCPWasip2, ListenTCPWasip2) is linkable from + // user code via //go:linkname. Once wasip2 net.Listen/Dial land in + // the stdlib this blank import will be replaced by a real consumer. + _ "internal/poll" +) 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/netpoll_wasip2.go b/src/runtime/netpoll_wasip2.go new file mode 100644 index 0000000000..ad33243757 --- /dev/null +++ b/src/runtime/netpoll_wasip2.go @@ -0,0 +1,267 @@ +//go:build wasip2 && (scheduler.tasks || scheduler.asyncify) + +package runtime + +import ( + "internal/cm" + "internal/task" + monotonicclock "internal/wasi/clocks/v0.2.0/monotonic-clock" + "internal/wasi/io/v0.2.0/poll" + "unsafe" +) + +// pollMode is unused on wasip2 (each pollable encodes its own direction at +// subscribe time — InputStream.Subscribe vs OutputStream.Subscribe vs +// TcpSocket.Subscribe). Kept for API parity with the wasip1 netpoll +// callers that pass a mode constant. +type pollMode uint8 + +const ( + pollRead pollMode = 1 + pollWrite pollMode = 2 +) + +// pollDesc tracks one parked goroutine waiting for a wasi pollable to +// become ready. It owns the pollable handle and is responsible for +// dropping it (either inside pollIO when the pollable fires, or via +// netpollDone for an unfired desc). +type pollDesc struct { + pollable uint32 // poll.Pollable resource handle (cm.Resource = uint32) + fired bool + task *task.Task + bnxt *pollDesc +} + +var ( + activePolls *pollDesc + pollCount int + + // Scratch buffers for the next Poll() call. Grown on demand, never + // shrunk — the working set settles on a stable max. + pollList []poll.Pollable // pollables passed to Poll + pollDescList []*pollDesc // parallel scratch: same indices as pollList; nil entry == clock pollable + pollResult cm.List[uint32] // index list returned by Poll +) + +// netpollAddPollable registers the calling goroutine's interest in a +// pollable and returns a descriptor identifying the wait. The caller +// transfers ownership of the pollable to the runtime: pollIO drops it +// when it fires; netpollDone drops it when the caller gives up. +// +// The caller must: +// +// 1. call task.Pause() to suspend until the pollable is ready (or the +// task is woken for some other reason — timer, manual scheduleTask), +// and +// 2. call netpollDone(pd) after Pause returns. +func netpollAddPollable(p uint32) *pollDesc { + pd := &pollDesc{ + pollable: p, + task: task.Current(), + bnxt: activePolls, + } + activePolls = pd + pollCount++ + return pd +} + +// netpollDone deregisters pd. If the pollable hasn't fired yet, its +// resource is dropped. Idempotent. +func netpollDone(pd *pollDesc) { + if pd.fired { + // pollIO already dropped the pollable and unlinked the desc. + return + } + pp := &activePolls + for *pp != nil { + if *pp == pd { + *pp = pd.bnxt + pd.bnxt = nil + pollCount-- + pd.fired = true + (poll.Pollable)(cm.Reinterpret[poll.Pollable](pd.pollable)).ResourceDrop() + return + } + pp = &(*pp).bnxt + } +} + +// pollIO is the cooperative scheduler's blocking wait on wasip2. It +// invokes wasi:io/poll.Poll with one pollable per active pollDesc, plus +// optionally a clock pollable. +// +// timeoutNs > 0 : subscribe a fresh monotonic-clock pollable with this +// duration; include it in the Poll list. +// timeoutNs == 0 : non-blocking poll — use Pollable.Ready() on each +// registered pollable. Wake any that are ready; +// return without calling Poll. +// timeoutNs < 0 : block until any FD pollable fires. Caller must ensure +// pollCount > 0 — Poll with zero pollables would +// block forever with no way out. +func pollIO(timeoutNs int64) { + addClock := timeoutNs > 0 + if !addClock && pollCount == 0 { + // Non-blocking poll with nothing to check, or block-forever with + // nothing to block on — caller should have caught this. + return + } + + if timeoutNs == 0 { + // Non-blocking fast path: Ready() each registered pollable and + // wake any that are already ready. + pp := &activePolls + for *pp != nil { + pd := *pp + pollable := cm.Reinterpret[poll.Pollable](pd.pollable) + if pollable.Ready() { + *pp = pd.bnxt + pd.bnxt = nil + pollCount-- + pd.fired = true + pollable.ResourceDrop() + runqueue.Push(pd.task) + continue + } + pp = &(*pp).bnxt + } + return + } + + // Build pollList in this order: [clock?, active pollables...]. The + // pollDescList parallel slice maps each index back to its pd (nil for + // the clock). + n := pollCount + if addClock { + n++ + } + if cap(pollList) < n { + pollList = make([]poll.Pollable, n) + pollDescList = make([]*pollDesc, n) + } else { + pollList = pollList[:n] + pollDescList = pollDescList[:n] + } + + i := 0 + var clockPollable poll.Pollable + if addClock { + clockPollable = monotonicclock.SubscribeDuration(monotonicclock.Duration(timeoutNs)) + pollList[i] = clockPollable + pollDescList[i] = nil + i++ + } + for pd := activePolls; pd != nil; pd = pd.bnxt { + pollList[i] = cm.Reinterpret[poll.Pollable](pd.pollable) + pollDescList[i] = pd + i++ + } + + pollResult = poll.Poll(cm.ToList(pollList)) + + // Walk the returned indices. Any pd that fired is unlinked + woken; + // its pollable is dropped. The clock pollable is always dropped at + // the end of this call, fired or not. + for _, idx := range pollResult.Slice() { + if int(idx) >= len(pollDescList) { + continue // defensive + } + pd := pollDescList[idx] + if pd == nil { + // Clock pollable fired — drop happens unconditionally below. + continue + } + if pd.fired { + continue + } + pd.fired = true + // Unlink from activePolls. + pp := &activePolls + for *pp != nil { + if *pp == pd { + *pp = pd.bnxt + pd.bnxt = nil + pollCount-- + break + } + pp = &(*pp).bnxt + } + cm.Reinterpret[poll.Pollable](pd.pollable).ResourceDrop() + runqueue.Push(pd.task) + } + if addClock { + // Drop the clock pollable whether or not it fired — it was + // freshly subscribed for this call only. + clockPollable.ResourceDrop() + } + + // Clear pointers in the scratch slices so we don't pin pollDescs in + // memory between calls. + for i := range pollDescList { + pollDescList[i] = nil + } +} + +// runtime_netpoll_addpollable_wasip2 is the linkname target used by +// internal/poll and other stdlib callers that hold a poll.Pollable handle +// (as a raw uint32) and want to park the current goroutine until it +// becomes ready. Returns an opaque uintptr (the pollDesc pointer); +// pass it back to runtime_netpoll_done_wasip2. +// +//go:linkname runtime_netpoll_addpollable_wasip2 +func runtime_netpoll_addpollable_wasip2(pollable uint32) uintptr { + return uintptr(unsafe.Pointer(netpollAddPollable(pollable))) +} + +// runtime_netpoll_done_wasip2 releases a pollDesc previously returned by +// runtime_netpoll_addpollable_wasip2. Idempotent. +// +//go:linkname runtime_netpoll_done_wasip2 +func runtime_netpoll_done_wasip2(pd uintptr) { + if pd == 0 { + return + } + netpollDone((*pollDesc)(unsafe.Pointer(pd))) +} + +// runtime_netpoll_pdfired_wasip2 reports whether the given pollDesc has +// already been woken. Used by deadline-driven cancellation paths to +// avoid double-waking a task. +// +//go:linkname runtime_netpoll_pdfired_wasip2 +func runtime_netpoll_pdfired_wasip2(pd uintptr) bool { + if pd == 0 { + return true + } + return (*pollDesc)(unsafe.Pointer(pd)).fired +} + +// runtime_netpoll_wake_wasip2 wakes the task parked on pd from outside +// the Poll 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. +// +// wasip2 is single-threaded so we don't need atomic ops here. +// +//go:linkname runtime_netpoll_wake_wasip2 +func runtime_netpoll_wake_wasip2(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 + } + cm.Reinterpret[poll.Pollable](p.pollable).ResourceDrop() + 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/runtime_wasip2.go b/src/runtime/runtime_wasip2.go index 46ce3d853b..de18ad6f45 100644 --- a/src/runtime/runtime_wasip2.go +++ b/src/runtime/runtime_wasip2.go @@ -42,11 +42,6 @@ func nanosecondsToTicks(ns int64) timeUnit { return timeUnit(ns) } -func sleepTicks(d timeUnit) { - p := monotonicclock.SubscribeDuration(monotonicclock.Duration(d)) - p.Block() -} - func ticks() timeUnit { return timeUnit(monotonicclock.Now()) } 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/scheduler_idle_wasip2.go b/src/runtime/scheduler_idle_wasip2.go new file mode 100644 index 0000000000..9ce6d737c2 --- /dev/null +++ b/src/runtime/scheduler_idle_wasip2.go @@ -0,0 +1,37 @@ +//go:build wasip2 && (scheduler.tasks || scheduler.asyncify) + +package runtime + +import ( + monotonicclock "internal/wasi/clocks/v0.2.0/monotonic-clock" +) + +// sleepTicks is the cooperative scheduler's "wait until the next deadline" +// primitive on wasip2. 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 pollables are registered via netpollAddPollable, this routes through +// pollIO so the same wasi:io/poll.Poll call observes both the clock +// subscription and the registered pollables. With no pollables it falls +// back to the cheap monotonic-clock-Block path. +func sleepTicks(d timeUnit) { + if pollCount > 0 { + pollIO(ticksToNanoseconds(d)) + return + } + p := monotonicclock.SubscribeDuration(monotonicclock.Duration(d)) + p.Block() + p.ResourceDrop() +} + +// 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 pollables registered this is a +// genuine deadlock; with pollables 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_wasip2_none.go b/src/runtime/scheduler_idle_wasip2_none.go new file mode 100644 index 0000000000..e80e9c9e45 --- /dev/null +++ b/src/runtime/scheduler_idle_wasip2_none.go @@ -0,0 +1,24 @@ +//go:build wasip2 && !scheduler.tasks && !scheduler.asyncify + +package runtime + +import ( + monotonicclock "internal/wasi/clocks/v0.2.0/monotonic-clock" +) + +// sleepTicks blocks the current execution context for d ticks. This is the +// fallback used when no cooperative scheduler is configured on wasip2 — it +// has no pollable-polling integration, see scheduler_idle_wasip2.go for the +// cooperative variant. +func sleepTicks(d timeUnit) { + p := monotonicclock.SubscribeDuration(monotonicclock.Duration(d)) + p.Block() + p.ResourceDrop() +} + +// waitForEvents is only meaningful when there's an event source available. +// Without the cooperative scheduler running poll on registered pollables, +// wasip2 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..d307903ba6 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 && !wasip2 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) +}