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:
Achille
2026-05-26 01:12:55 -07:00
committed by GitHub
parent 85d223c5e2
commit d01a932201
17 changed files with 1404 additions and 51 deletions
+163
View File
@@ -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 }
+86
View File
@@ -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
}
+4 -35
View File
@@ -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)
+48
View File
@@ -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
}
+110
View File
@@ -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)
}