Skip to content

Commit 9014ec4

Browse files
runtime,syscall,internal/poll,os,sync: 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. End to end: $ tinygo build -target=wasip1 -o tcpecho.wasm ./tcpecho.go $ wasmtime run -Spreview2=false -Stcplisten=127.0.0.1:9999 ./tcpecho.wasm & listening on FD 3 $ echo hello | nc 127.0.0.1 9999 hello # echoed by the wasm Concurrent connections work too — multiple nc clients hit the server back-to- back; both got accepted and echoed while the cooperative scheduler kept running goroutines parked on sock_recv. The tcpecho.go source is the minimal upstream-Go-idiomatic TCP echo server, no TinyGo net override required: f := os.NewFile(3, "tcplisten") ln, _ := net.FileListener(f) for { c, _ := ln.Accept() go func(c net.Conn) { defer c.Close(); io.Copy(c, c) }(c) } Architecture: The cooperative scheduler's idle path now calls poll_oneoff with one combined subscription array: a clock subscription for the next timer/sleep deadline, plus one FD subscription per goroutine that's parked waiting on I/O. syscall.Read/Write ─EAGAIN─► internal/poll registry ─► task.Pause() │ ▼ scheduler idle ──► pollIO(timeoutNs) │ ├─ build subs: [clock, fd1, fd2, …] ├─ poll_oneoff(...) └─ wake matched tasks → run queue Upstream net's net.FileListener(f) flow plumbs through these layers: 1. (*os.File).PollFD() returns a cached *poll.FD stored in a new pfd pollFD field on the shared file struct. pollFD is a per-target alias — *poll.FD on wasip1, literal struct{} (zero bytes, non- trailing) on every other target. 2. (*poll.FD).Copy() increments a SysFile refcount so f.Close() and ln.Close() cooperatively release the syscall FD. 3. Upstream net/file_wasip1.go calls fd_fdstat_get_type (linknamed into our syscall) to detect FILETYPE_SOCKET_STREAM. 4. Listener.Accept → (*poll.FD).Accept → syscall.Accept → sock_accept wasmimport. EAGAIN parks the goroutine via the runtime netpoll registry. 5. Conn.Read/Write see isSocket() == true (cached SysFile.Filetype) and dispatch to sock_recv / sock_send direct wasmimports with park-on- EAGAIN + deadline support. 6. Conn.Close triggers Shutdown (→ sock_shutdown) and refcount-aware Close. Non-goals (deferred): - net.Dial("tcp", ...), net.Listen("tcp", ...) — wasip1 has no sock_connect / bind / listen (only sock_accept / recv / send / shutdown). Outbound TCP requires the wasi-sockets proposal (preview2+) or a runtime-specific extension. The relevant stubs return ENOSYS so callers see a clean error. - UDP / PacketConn — upstream's filePacketConn returns ENOPROTOOPT on wasip1. - DNS resolution — FileListener / FileConn paths bypass the resolver entirely. - wasip2 — uses pollable resources, structurally different. Future PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1a1506e commit 9014ec4

18 files changed

Lines changed: 1436 additions & 47 deletions

loader/goroot.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool {
246246
"internal/futex/": false,
247247
"internal/fuzz/": false,
248248
"internal/itoa": false,
249+
"internal/poll/": false,
249250
"internal/reflectlite/": false,
250251
"internal/gclayout": false,
251252
"internal/task/": false,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//go:build wasip1
2+
3+
// Internal-test helpers exposed via go:linkname so user code can drive
4+
// the deadline-aware Read/Write loop without becoming a stdlib package.
5+
// Not part of the public API; the names are intentionally awkward to
6+
// signal "for tests only".
7+
8+
package poll
9+
10+
import (
11+
"syscall"
12+
"time"
13+
)
14+
15+
// pollTestReadWithDeadline opens a pollable FD wrapper for sysfd, sets
16+
// a read deadline d into the future, calls Read once, and returns
17+
// (n, err). Caller is responsible for closing sysfd.
18+
//
19+
//go:linkname pollTestReadWithDeadline
20+
func pollTestReadWithDeadline(sysfd int, d time.Duration, p []byte) (int, error) {
21+
fd := &FD{Sysfd: sysfd, IsStream: true}
22+
// Best-effort init; ignore error so a caller using a not-fcntl-able FD
23+
// (stdin under wazero, etc.) still gets to test the deadline path on
24+
// whatever park behaviour the runtime gives.
25+
_ = fd.Init("test", true)
26+
if err := fd.SetReadDeadline(time.Now().Add(d)); err != nil {
27+
return 0, err
28+
}
29+
return fd.Read(p)
30+
}
31+
32+
// pollTestSetNonblock toggles O_NONBLOCK on a raw sysfd. Useful in
33+
// tests when the caller wants to ensure the FD is in nonblocking mode
34+
// before calling pollTestReadWithDeadline (Init is best-effort and may
35+
// silently skip).
36+
//
37+
//go:linkname pollTestSetNonblock
38+
func pollTestSetNonblock(sysfd int) error {
39+
flags, err := syscall.Fcntl(sysfd, syscall.F_GETFL, 0)
40+
if err != nil {
41+
return err
42+
}
43+
_, err = syscall.Fcntl(sysfd, syscall.F_SETFL, flags|syscall.O_NONBLOCK)
44+
return err
45+
}

0 commit comments

Comments
 (0)