Repository: tetratelabs/wazero
Pull request #1128 (2023-02-15 21:30:44)

Implements fd_datasync in WASI and sync in GOOS=js
This implements `fd_datasync` in WASI, falling back to normal `File.Sync` when unsupported. This also backfills missing usage of sync in GOOS=js. Finally, this updates the WASI status chart based on what's implemented.


Patch:
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index 1eb7e903..ca20bc05 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -20,6 +20,14 @@ import (
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
+// The following interfaces are used until we finalize our own FD-scoped file.
+type (
+	// syncer is implemented by os.File in file_posix.go
+	syncer interface{ Sync() error }
+	// truncater is implemented by os.File in file_posix.go
+	truncater interface{ Truncate(size int64) error }
+)
+
 // fdAdvise is the WASI function named FdAdviseName which provides file
 // advisory information on a file descriptor.
 //
@@ -148,7 +156,20 @@ func fdCloseFn(_ context.Context, mod api.Module, params []uint64) Errno {
 // the data of a file to disk.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_datasyncfd-fd---errno
-var fdDatasync = stubFunction(FdDatasyncName, []api.ValueType{i32}, "fd")
+var fdDatasync = newHostFunc(FdDatasyncName, fdDatasyncFn, []api.ValueType{i32}, "fd")
+
+func fdDatasyncFn(_ context.Context, mod api.Module, params []uint64) Errno {
+	fsc := mod.(*wasm.CallContext).Sys.FS()
+	fd := uint32(params[0])
+
+	// Check to see if the file descriptor is available
+	if f, ok := fsc.LookupFile(fd); !ok {
+		return ErrnoBadf
+	} else if err := sysfs.FileDatasync(f.File); err != nil {
+		return ToErrno(err)
+	}
+	return ErrnoSuccess
+}
 
 // fdFdstatGet is the WASI function named FdFdstatGetName which returns the
 // attributes of a file descriptor.
@@ -1131,7 +1152,7 @@ func fdSeekFn(_ context.Context, mod api.Module, params []uint64) Errno {
 
 	newOffset, err := seeker.Seek(int64(offset), int(whence))
 	if err != nil {
-		return ErrnoIo
+		return ToErrno(err)
 	}
 
 	if !mod.Memory().WriteUint64Le(resultNewoffset, uint64(newOffset)) {
@@ -1146,11 +1167,6 @@ func fdSeekFn(_ context.Context, mod api.Module, params []uint64) Errno {
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_syncfd-fd---errno
 var fdSync = newHostFunc(FdSyncName, fdSyncFn, []api.ValueType{i32}, "fd")
 
-type (
-	syncer    interface{ Sync() error }
-	truncater interface{ Truncate(size int64) error }
-)
-
 func fdSyncFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 	fd := uint32(params[0])
@@ -1161,7 +1177,7 @@ func fdSyncFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	} else if syncer, ok := f.File.(syncer); !ok {
 		return ErrnoBadf // possibly a fake file
 	} else if err := syncer.Sync(); err != nil {
-		return ErrnoIo
+		return ToErrno(err)
 	}
 	return ErrnoSuccess
 }
@@ -1293,7 +1309,7 @@ func fdWriteOrPwrite(mod api.Module, params []uint64, isPwrite bool) Errno {
 			}
 			n, err = writer.Write(b)
 			if err != nil {
-				return ErrnoIo
+				return ToErrno(err)
 			}
 		}
 		nwritten += uint32(n)
diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go
index 29322472..9b4ce0ba 100644
--- a/internal/gojs/fs.go
+++ b/internal/gojs/fs.go
@@ -82,6 +82,14 @@ var (
 	oEXCL = float64(os.O_EXCL)
 )
 
+// The following interfaces are used until we finalize our own FD-scoped file.
+type (
+	// syncer is implemented by os.File in file_posix.go
+	syncer interface{ Sync() error }
+	// truncater is implemented by os.File in file_posix.go
+	truncater interface{ Truncate(size int64) error }
+)
+
 // jsfsOpen implements implements jsFn for syscall.Open
 //
 //	jsFD /* Int */, err := fsCall("open", path, flags, perm)
@@ -411,7 +419,7 @@ func (processChdir) invoke(ctx context.Context, mod api.Module, args ...interfac
 	path := args[0].(string)
 
 	if s, err := syscallStat(mod, path); err != nil {
-		return nil, ToErrno(err)
+		return nil, err
 	} else if !s.isDir {
 		return nil, syscall.ENOTDIR
 	} else {
@@ -609,8 +617,6 @@ func (jsfsTruncate) invoke(ctx context.Context, mod api.Module, args ...interfac
 //	_, err := fsCall("ftruncate", fd, length) // syscall.Ftruncate
 type jsfsFtruncate struct{}
 
-type truncater interface{ Truncate(size int64) error }
-
 func (jsfsFtruncate) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	fd := goos.ValueToUint32(args[0])
 	length := toInt64(args[1])
@@ -687,8 +693,16 @@ func (jsfsFsync) invoke(ctx context.Context, mod api.Module, args ...interface{}
 	fd := goos.ValueToUint32(args[0])
 	callback := args[1].(funcWrapper)
 
-	_ = fd // TODO
-	var err error = syscall.ENOSYS
+	// Check to see if the file descriptor is available
+	fsc := mod.(*wasm.CallContext).Sys.FS()
+	var err error
+	if f, ok := fsc.LookupFile(fd); !ok {
+		err = syscall.EBADF
+	} else if syncer, ok := f.File.(syncer); !ok {
+		err = syscall.EBADF // possibly a fake file
+	} else {
+		err = syncer.Sync()
+	}
 
 	return jsfsInvoke(ctx, mod, callback, err)
 }
diff --git a/internal/platform/stat_windows.go b/internal/platform/stat_windows.go
index bcfb97e1..a10e46aa 100644
--- a/internal/platform/stat_windows.go
+++ b/internal/platform/stat_windows.go
@@ -8,6 +8,12 @@ import (
 	"syscall"
 )
 
+// The following interfaces are used until we finalize our own FD-scoped file.
+type (
+	// fder is implemented by os.File in file_unix.go and file_windows.go
+	fder interface{ Fd() (fd uintptr) }
+)
+
 func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
 	d := t.Sys().(*syscall.Win32FileAttributeData)
 	atimeNsec = d.LastAccessTime.Nanoseconds()
@@ -16,26 +22,13 @@ func statTimes(t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64) {
 	return
 }
 
-// fdGetter is implemented by *os.File on Windows, but not a part of fs.File.
-// On the other hand, fs.File is implemented by the wrapped version of os.File,
-// therefore we define the interface here temporarily.
-//
-// This is only needed on Windows in order to nlink by making another raw system call
-// on the file handle.
-//
-// TODO: once we have our own File/FileInfo type, this shouldn't be needed.
-type fdGetter interface {
-	// Fd is the same as os.File Fd.
-	Fd() uintptr
-}
-
 func stat(f fs.File, t os.FileInfo) (atimeNsec, mtimeNsec, ctimeNsec int64, nlink uint64, err error) {
 	d := t.Sys().(*syscall.Win32FileAttributeData)
 	atimeNsec = d.LastAccessTime.Nanoseconds()
 	mtimeNsec = d.LastWriteTime.Nanoseconds()
 	ctimeNsec = d.CreationTime.Nanoseconds()
 
-	of, ok := f.(fdGetter)
+	of, ok := f.(fder)
 	if !ok {
 		return
 	}
diff --git a/internal/platform/sync.go b/internal/platform/sync.go
new file mode 100644
index 00000000..e110ffd4
--- /dev/null
+++ b/internal/platform/sync.go
@@ -0,0 +1,7 @@
+package platform
+
+// Fdatasync is like syscall.Fdatasync except that's only defined in linux.
+// This returns syscall.ENOSYS when unimplemented.
+func Fdatasync(fd uintptr) (err error) {
+	return fdatasync(fd)
+}
diff --git a/internal/platform/sync_linux.go b/internal/platform/sync_linux.go
new file mode 100644
index 00000000..ec67198f
--- /dev/null
+++ b/internal/platform/sync_linux.go
@@ -0,0 +1,9 @@
+//go:build linux
+
+package platform
+
+import "syscall"
+
+func fdatasync(fd uintptr) error {
+	return syscall.Fdatasync(int(fd))
+}
diff --git a/internal/platform/sync_unsupported.go b/internal/platform/sync_unsupported.go
new file mode 100644
index 00000000..4f039f5e
--- /dev/null
+++ b/internal/platform/sync_unsupported.go
@@ -0,0 +1,9 @@
+//go:build !linux
+
+package platform
+
+import "syscall"
+
+func fdatasync(fd uintptr) error {
+	return syscall.ENOSYS
+}
diff --git a/internal/sysfs/sysfs.go b/internal/sysfs/sysfs.go
index 970d63b8..554a17bc 100644
--- a/internal/sysfs/sysfs.go
+++ b/internal/sysfs/sysfs.go
@@ -10,6 +10,8 @@ import (
 	"io/fs"
 	"os"
 	"syscall"
+
+	"github.com/tetratelabs/wazero/internal/platform"
 )
 
 // FS is a writeable fs.FS bridge backed by syscall functions needed for ABI
@@ -224,10 +226,14 @@ type file interface {
 	fder // for the number of links.
 }
 
+// The following interfaces are used until we finalize our own FD-scoped file.
 type (
-	syncer    interface{ Sync() error }
+	// syncer is implemented by os.File in file_posix.go
+	syncer interface{ Sync() error }
+	// truncater is implemented by os.File in file_posix.go
 	truncater interface{ Truncate(size int64) error }
-	fder      interface{ Fd() (fd uintptr) }
+	// fder is implemented by os.File in file_unix.go and file_windows.go
+	fder interface{ Fd() (fd uintptr) }
 )
 
 // ReaderAtOffset gets an io.Reader from a fs.File that reads from an offset,
@@ -247,6 +253,21 @@ func ReaderAtOffset(f fs.File, offset int64) io.Reader {
 	}
 }
 
+// FileDatasync is like syscall.Fdatasync except that's only defined in linux.
+func FileDatasync(f fs.File) (err error) {
+	if fd, ok := f.(fder); ok {
+		if err := platform.Fdatasync(fd.Fd()); err != syscall.ENOSYS {
+			return err
+		}
+	}
+
+	// Attempt to sync everything, even if we only need to sync the data.
+	if s, ok := f.(syncer); ok {
+		err = s.Sync()
+	}
+	return
+}
+
 type enosysReader struct{}
 
 // enosysReader implements io.Reader
diff --git a/site/content/specs.md b/site/content/specs.md
index a0a25c52..01952459 100644
--- a/site/content/specs.md
+++ b/site/content/specs.md
@@ -90,12 +90,12 @@ Notes:
 | environ_sizes_get       |   ✅    |          TinyGo |
 | clock_res_get           |   ✅    |                 |
 | clock_time_get          |   ✅    |          TinyGo |
-| fd_advise               |   ❌    |                 |
-| fd_allocate             |   ❌    |                 |
+| fd_advise               |   👷   |                 |
+| fd_allocate             |   ✅    |            Rust |
 | fd_close                |   ✅    |          TinyGo |
-| fd_datasync             |   ❌    |                 |
+| fd_datasync             |   ✅    |            Rust |
 | fd_fdstat_get           |   ✅    |          TinyGo |
-| fd_fdstat_set_flags     |   ❌    |                 |
+| fd_fdstat_set_flags     |   ✅    |            Rust |
 | fd_fdstat_set_rights    |   💀   |                 |
 | fd_filestat_get         |   ✅    |             Zig |
 | fd_filestat_set_size    |   ✅    |        Rust,Zig |
@@ -106,10 +106,10 @@ Notes:
 | fd_pwrite               |   ✅    |        Rust,Zig |
 | fd_read                 |   ✅    | Rust,TinyGo,Zig |
 | fd_readdir              |   ✅    |        Rust,Zig |
-| fd_renumber             |   ❌    |                 |
+| fd_renumber             |   ✅    |            libc |
 | fd_seek                 |   ✅    |          TinyGo |
-| fd_sync                 |   ❌    |                 |
-| fd_tell                 |   ❌    |                 |
+| fd_sync                 |   ✅    |              Go |
+| fd_tell                 |   ✅    |            Rust |
 | fd_write                |   ✅    | Rust,TinyGo,Zig |
 | path_create_directory   |   ✅    | Rust,TinyGo,Zig |
 | path_filestat_get       |   ✅    | Rust,TinyGo,Zig |


Test patch:
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index e27fde30..d30148a8 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -170,13 +170,48 @@ func Test_fdClose(t *testing.T) {
 	})
 }
 
-// Test_fdDatasync only tests it is stubbed for GrainLang per #271
+// Test_fdDatasync only tests that the call succeeds; it's hard to test its effectiveness.
 func Test_fdDatasync(t *testing.T) {
-	log := requireErrnoNosys(t, FdDatasyncName, 0)
-	require.Equal(t, `
---> wasi_snapshot_preview1.fd_datasync(fd=0)
-<-- errno=ENOSYS
-`, log)
+	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
+	pathName := "test_path"
+	mod, fd, log, r := requireOpenFile(t, tmpDir, pathName, []byte{}, false)
+	defer r.Close(testCtx)
+
+	tests := []struct {
+		name          string
+		fd            uint32
+		expectedErrno Errno
+		expectedLog   string
+	}{
+		{
+			name:          "invalid fd",
+			fd:            42, // arbitrary invalid fd
+			expectedErrno: ErrnoBadf,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_datasync(fd=42)
+<== errno=EBADF
+`,
+		},
+		{
+			name:          "valid fd",
+			fd:            fd,
+			expectedErrno: ErrnoSuccess,
+			expectedLog: `
+==> wasi_snapshot_preview1.fd_datasync(fd=4)
+<== errno=ESUCCESS
+`,
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			defer log.Reset()
+
+			requireErrno(t, tc.expectedErrno, mod, FdDatasyncName, uint64(tc.fd))
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
+		})
+	}
 }
 
 func Test_fdFdstatGet(t *testing.T) {
diff --git a/internal/gojs/testdata/writefs/main.go b/internal/gojs/testdata/writefs/main.go
index a114453d..95fe71ff 100644
--- a/internal/gojs/testdata/writefs/main.go
+++ b/internal/gojs/testdata/writefs/main.go
@@ -69,6 +69,10 @@ func Main() {
 	if err = f.Truncate(2); err != nil {
 		log.Panicln(err)
 	}
+	// Next, sync it.
+	if err = f.Sync(); err != nil {
+		log.Panicln(err)
+	}
 	if err = f.Close(); err != nil {
 		log.Panicln(err)
 	}
diff --git a/internal/platform/sync_test.go b/internal/platform/sync_test.go
new file mode 100644
index 00000000..b2464996
--- /dev/null
+++ b/internal/platform/sync_test.go
@@ -0,0 +1,44 @@
+package platform
+
+import (
+	"bytes"
+	"io"
+	"os"
+	"syscall"
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+// Test_Fdatasync doesn't guarantee sync works because the operating system may
+// sync anyway. There is no test in Go for syscall.Fdatasync, but closest is
+// similar to below. Effectively, this only tests that things don't error.
+func Test_Fdatasync(t *testing.T) {
+	f, err := os.CreateTemp("", t.Name())
+	require.NoError(t, err)
+	defer f.Close()
+
+	expected := "hello world!"
+
+	// Write the expected data
+	_, err = f.Write([]byte(expected))
+	require.NoError(t, err)
+
+	// Sync the data.
+	if err = Fdatasync(f.Fd()); err == syscall.ENOSYS {
+		return // don't continue if it isn't supported.
+	}
+	require.NoError(t, err)
+
+	// Rewind while the file is still open.
+	_, err = f.Seek(0, io.SeekStart)
+	require.NoError(t, err)
+
+	// Read data from the file
+	var buf bytes.Buffer
+	_, err = io.Copy(&buf, f)
+	require.NoError(t, err)
+
+	// It may be the case that sync worked.
+	require.Equal(t, expected, buf.String())
+}
diff --git a/internal/sysfs/sysfs_test.go b/internal/sysfs/sysfs_test.go
index 4e9cf6f7..49739f59 100644
--- a/internal/sysfs/sysfs_test.go
+++ b/internal/sysfs/sysfs_test.go
@@ -1,6 +1,7 @@
 package sysfs
 
 import (
+	"bytes"
 	"embed"
 	_ "embed"
 	"errors"
@@ -603,3 +604,44 @@ func TestStatPath(t *testing.T) {
 	require.NoError(t, err)
 	require.True(t, s.IsDir())
 }
+
+// Test_FileSync doesn't guarantee sync works because the operating system may
+// sync anyway. There is no test in Go for os.File Sync, but closest is similar
+// to below. Effectively, this only tests that things don't error.
+func Test_FileSync(t *testing.T) {
+	testSync(t, func(f fs.File) error {
+		return f.(syncer).Sync()
+	})
+}
+
+// Test_FileDatasync has same issues as Test_Sync.
+func Test_FileDatasync(t *testing.T) {
+	testSync(t, FileDatasync)
+}
+
+func testSync(t *testing.T, sync func(fs.File) error) {
+	f, err := os.CreateTemp("", t.Name())
+	require.NoError(t, err)
+	defer f.Close()
+
+	expected := "hello world!"
+
+	// Write the expected data
+	_, err = f.Write([]byte(expected))
+	require.NoError(t, err)
+
+	// Sync the data.
+	require.NoError(t, sync(f))
+
+	// Rewind while the file is still open.
+	_, err = f.Seek(0, io.SeekStart)
+	require.NoError(t, err)
+
+	// Read data from the file
+	var buf bytes.Buffer
+	_, err = io.Copy(&buf, f)
+	require.NoError(t, err)
+
+	// It may be the case that sync worked.
+	require.Equal(t, expected, buf.String())
+}