mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
runtime,syscall,internal/poll,os: wasip1 poll_oneoff scheduler integration + net.FileListener (#5386)
* runtime,syscall,internal/poll,os: wasip1 poll_oneoff scheduler integration + net.FileListener On wasip1 today every syscall.Read/Write blocks the entire wasm module — the cooperative scheduler invokes poll_oneoff only for sleep/timer wakeups, and there's no path from the net package to a working TCP server. This change fixes both: it threads poll_oneoff through the scheduler's idle path so a goroutine doing FD I/O parks instead of blocking the module, and it provides enough internal/poll / os / syscall surface that upstream Go's net.FileListener / net.FileConn works on a host-pre-opened TCP socket. * runtime: keep scheduler_cooperative idle-wait calls direct TestBinarySize/hifive1b/examples/echo regressed by 32 bytes after the previous commit routed the scheduler's idle wait through a schedulerIdleWait helper. The extra call frame + branch landed on every non-wasip1 cooperative target, where the original direct sleepTicks / waitForEvents calls compile to a single inlined call.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+16
-3
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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{}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user