Skip to content

Commit

Permalink
wasi: nonblocking I/O for sockets and pipes on Windows
Browse files Browse the repository at this point in the history
Further work to improve the support to nonblocking I/O on Windows.
This drops the special-casing for stdin, and instead checks if
a handle is a named pipe (which includes sockets and other
kinds of pipes); moreover it further improve _select by
introducing a proper wrapper for winsock's select, which
is BSD socket's select, and a compatible implementation of FdSet.

Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
  • Loading branch information
evacchi committed Jul 17, 2023
1 parent 1cdb72d commit e920593
Show file tree
Hide file tree
Showing 11 changed files with 588 additions and 170 deletions.
7 changes: 2 additions & 5 deletions imports/wasi_snapshot_preview1/wasi_stdlib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"os"
"os/exec"
"path"
"runtime"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -478,13 +477,11 @@ func testSock(t *testing.T, bin []byte) {
console := <-ch
require.NotEqual(t, 0, n)
require.NoError(t, err)
require.Equal(t, "wazero\n", console)
// Nonblocking connections may contain error logging, we ignore those.
require.Equal(t, "wazero\n", console[len(console)-7:])
}

func Test_HTTP(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fsapi.Nonblocking() is not supported on wasip1+windows.")
}
toolchains := map[string][]byte{}
if wasmGotip != nil {
toolchains["gotip"] = wasmGotip
Expand Down
2 changes: 2 additions & 0 deletions internal/platform/fdset.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package platform

// Set adds the given fd to the set.
Expand Down
7 changes: 2 additions & 5 deletions internal/platform/fdset_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
//go:build !windows

package platform

import (
"runtime"
"testing"

"github.com/tetratelabs/wazero/internal/testing/require"
)

func TestFdSet(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.Skip("not supported")
}

allBitsSetAtIndex0 := FdSet{}
allBitsSetAtIndex0.Bits[0] = -1

Expand Down
2 changes: 1 addition & 1 deletion internal/platform/fdset_unsupported.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !darwin && !linux
//go:build !darwin && !linux && !windows

package platform

Expand Down
209 changes: 209 additions & 0 deletions internal/platform/fdset_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package platform

import (
"syscall"
"unsafe"
)

var procGetNamedPipeInfo = kernel32.NewProc("GetNamedPipeInfo")

// Maximum number of fds in a WinSockFdSet.
const _FD_SETSIZE = 64

// WinSockFdSet implements the FdSet representation that is used internally by WinSock.
//
// Note: this representation is quite different from the one used in most POSIX implementations
// where a bitfield is usually implemented; instead on Windows we have a simpler array+count pair.
// Notice that because it keeps a count of the inserted handles, the first argument of select
// in WinSock is actually ignored.
//
// The implementation of the Set, Clear, IsSet, Zero, methods follows exactly
// the real implementation found in WinSock2.h, e.g. see:
// https://github.com/microsoft/win32metadata/blob/ef7725c75c6b39adfdc13ba26fb1d89ac954449a/generation/WinSDK/RecompiledIdlHeaders/um/WinSock2.h#L124-L175
type WinSockFdSet struct {
// count is the number of used slots used in the handles slice.
count uint64
// handles is the array of handles. This is called "array" in the WinSock implementation
// and it has a fixed length of _FD_SETSIZE.
handles [_FD_SETSIZE]syscall.Handle
}

// FdSet implements the same methods provided on other plaforms.
//
// Note: the implementation is very different from POSIX; Windows provides
// POSIX select only for sockets. We emulate a select for other APIs in the sysfs
// package, but we still want to use the "real" select in the case of sockets.
// So, we keep a separate FdSet of sockets, so that we can pass it directly
// to the winsock select implementation
type FdSet struct {
sockets WinSockFdSet
pipes WinSockFdSet
regular WinSockFdSet
}

// Sockets returns a WinSockFdSet containing the handles in this FdSet that are sockets.
func (f *FdSet) Sockets() *WinSockFdSet {
if f == nil {
return nil
}
return &f.sockets
}

func (f *FdSet) SetSockets(s WinSockFdSet) {
f.sockets = s
}

// Regular returns a WinSockFdSet containing the handles in this FdSet that are regular files.
func (f *FdSet) Regular() *WinSockFdSet {
if f == nil {
return nil
}
return &f.regular
}

func (f *FdSet) SetRegular(r WinSockFdSet) {
f.regular = r
}

// Regular returns a WinSockFdSet containing the handles in this FdSet that are pipes.
func (f *FdSet) Pipes() *WinSockFdSet {
if f == nil {
return nil
}
return &f.pipes
}

func (f *FdSet) SetPipes(p WinSockFdSet) {
f.pipes = p
}

func (f *FdSet) getFdSetFor(fd int) *WinSockFdSet {
h := syscall.Handle(fd)
t, err := syscall.GetFileType(h)
if err != nil {
return nil
}
switch t {
case syscall.FILE_TYPE_CHAR, syscall.FILE_TYPE_DISK:
return &f.regular
case syscall.FILE_TYPE_PIPE:
if isSocket(h) {
return &f.sockets
} else {
return &f.pipes
}
default:
return nil
}
}

// Set adds the given fd to the set.
func (f *FdSet) Set(fd int) {
if s := f.getFdSetFor(fd); s != nil {
s.Set(fd)
}
}

// Clear removes the given fd from the set.
func (f *FdSet) Clear(fd int) {
if s := f.getFdSetFor(fd); s != nil {
s.Clear(fd)
}
}

// IsSet returns true when fd is in the set.
func (f *FdSet) IsSet(fd int) bool {
if s := f.getFdSetFor(fd); s != nil {
return s.IsSet(fd)
}
return false
}

// Zero clears the set.
func (f *FdSet) Zero() {
f.sockets.Zero()
f.regular.Zero()
f.pipes.Zero()
}

// Set adds the given fd to the set.
func (f *WinSockFdSet) Set(fd int) {
if f.count < _FD_SETSIZE {
f.handles[f.count] = syscall.Handle(fd)
f.count++
}
}

// Clear removes the given fd from the set.
func (f *WinSockFdSet) Clear(fd int) {
h := syscall.Handle(fd)
if !isSocket(h) {
return
}

for i := uint64(0); i < f.count; i++ {
if f.handles[i] == h {
for ; i < f.count-1; i++ {
f.handles[i] = f.handles[i+1]
}
f.count--
break
}
}
}

// IsSet returns true when fd is in the set.
func (f *WinSockFdSet) IsSet(fd int) bool {
h := syscall.Handle(fd)
for i := uint64(0); i < f.count; i++ {
if f.handles[i] == h {
return true
}
}
return false
}

// Zero clears the set.
func (f *WinSockFdSet) Zero() {
f.count = 0
}

func (f *WinSockFdSet) Count() int {
if f == nil {
return 0
}
return int(f.count)
}

func (f *WinSockFdSet) Copy() *WinSockFdSet {
if f == nil {
return nil
}
copy := *f
return &copy
}

func (f *WinSockFdSet) Get(index int) syscall.Handle {
return f.handles[index]
}

// isSocket returns true if the given file handle
// is a pipe.
func isSocket(fd syscall.Handle) bool {
// n, err := syscall.GetFileType(fd)
// if err != nil {
// return false
// }
// if n != syscall.FILE_TYPE_PIPE {
// return false
// }
// If the call to GetNamedPipeInfo succeeds then
// the handle is a pipe handle, otherwise it is a socket.
r, _, errno := syscall.SyscallN(
procGetNamedPipeInfo.Addr(),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)))
return r != 0 && errno == 0
}
8 changes: 0 additions & 8 deletions internal/sysfs/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ func TestStdioFileSetNonblock(t *testing.T) {
}

func TestRegularFileSetNonblock(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Nonblock on regular files is not supported on Windows")
}

// Test using os.Pipe as it is known to support non-blocking reads.
r, w, err := os.Pipe()
require.NoError(t, err)
Expand Down Expand Up @@ -339,10 +335,6 @@ func TestFilePollRead(t *testing.T) {

// When there's nothing in the pipe, it isn't ready.
ready, errno := rF.PollRead(&timeout)
if runtime.GOOS == "windows" {
require.EqualErrno(t, experimentalsys.ENOSYS, errno)
t.Skip("TODO: windows File.PollRead")
}
require.EqualErrno(t, 0, errno)
require.False(t, ready)

Expand Down
31 changes: 22 additions & 9 deletions internal/sysfs/file_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

const (
nonBlockingFileReadSupported = true
nonBlockingFileWriteSupported = false
nonBlockingFileWriteSupported = true
)

var kernel32 = syscall.NewLazyDLL("kernel32.dll")
Expand All @@ -24,7 +24,7 @@ var procPeekNamedPipe = kernel32.NewProc("PeekNamedPipe")
// https://learn.microsoft.com/en-us/windows/console/console-handles
func readFd(fd uintptr, buf []byte) (int, sys.Errno) {
handle := syscall.Handle(fd)
fileType, err := syscall.GetFileType(syscall.Stdin)
fileType, err := syscall.GetFileType(handle)
if err != nil {
return 0, sys.UnwrapOSError(err)
}
Expand All @@ -42,6 +42,26 @@ func readFd(fd uintptr, buf []byte) (int, sys.Errno) {
return un, sys.UnwrapOSError(err)
}

func readSocket(h syscall.Handle, buf []byte) (int, sys.Errno) {
var overlapped syscall.Overlapped
var done uint32
err := syscall.ReadFile(h, buf, &done, &overlapped)
if err == syscall.ERROR_IO_PENDING {
err = syscall.EAGAIN
}
return int(done), sys.UnwrapOSError(err)
}

func writeFd(fd uintptr, buf []byte) (int, sys.Errno) {
var done uint32
var overlapped syscall.Overlapped
err := syscall.WriteFile(syscall.Handle(fd), buf, &done, &overlapped)
if err == syscall.ERROR_IO_PENDING {
err = syscall.EAGAIN
}
return int(done), sys.UnwrapOSError(err)
}

// peekNamedPipe partially exposes PeekNamedPipe from the Win32 API
// see https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
func peekNamedPipe(handle syscall.Handle) (uint32, error) {
Expand All @@ -54,12 +74,5 @@ func peekNamedPipe(handle syscall.Handle) (uint32, error) {
0, // [out, optional] LPDWORD lpBytesRead
uintptr(totalBytesPtr), // [out, optional] LPDWORD lpTotalBytesAvail,
0) // [out, optional] LPDWORD lpBytesLeftThisMessage
if err == syscall.Errno(0) {
return totalBytesAvail, nil
}
return totalBytesAvail, err
}

func writeFd(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOSYS
}
6 changes: 0 additions & 6 deletions internal/sysfs/select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,6 @@ func TestSelect(t *testing.T) {

for {
n, err := _select(fd+1, rFdSet, nil, nil, nil)
if runtime.GOOS == "windows" {
// Not implemented for fds != wasiFdStdin
require.ErrorIs(t, err, sys.ENOSYS)
require.Equal(t, -1, n)
break
}
if err == sys.EINTR {
t.Log("Select interrupted")
continue
Expand Down
Loading

0 comments on commit e920593

Please sign in to comment.