mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-05 03:27:48 +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:
@@ -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