Repository: tetratelabs/wazero
Pull request #272 (2022-02-21 12:26:54)

wasi: backfill docs and tests of fd_close fd_prestat_get fd_prestat_dir_name fd_read
This PR backfills the docs and the tests of part of existing WASI APIs.

codefromthecrypt: ps I changed the title. let's keep this PR at max 4 functions (existing here are fd_close fd_prestat_get fd_prestat_dir_name fd_read) and of course you can axe one if you like.

Patch:
diff --git a/internal/wasi/RATIONALE.md b/internal/wasi/RATIONALE.md
new file mode 100644
index 00000000..9f206620
--- /dev/null
+++ b/internal/wasi/RATIONALE.md
@@ -0,0 +1,24 @@
+# Notable rationale of the WASI implementation in wazero
+
+## Semantics of WASI API
+
+Unfortunately, (WASI Snapshot Preview 1)[https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md] is not formally defined enough, and has APIs with ambiguous semantics.
+This section describes how Wazero interprets and implements the semantics of several WASI APIs that may be interpreted differently by different wasm runtimes.
+Those APIs may affect the portability of a WASI application.
+
+### FdPrestatDirName
+
+`FdPrestatDirName` is a WASI function to return the path of the pre-opened directory of a file descriptor.
+It has the following three parameters, and the third `pathLen` has ambiguous semantics.
+
+- `fd` - a file descriptor
+- `path` - the offset for the result path
+- `pathLen` - In wazero, `FdPrestatDirName` writes the result path string to `path` offset for the exact length of `pathLen`.
+
+Wasmer considers `pathLen` to be the maximum length instead of the exact length that should be written.
+See https://github.com/wasmerio/wasmer/blob/3463c51268ed551933392a4063bd4f8e7498b0f6/lib/wasi/src/syscalls/mod.rs#L764
+
+The semantics in wazero follows that of wasmtime.
+See https://github.com/bytecodealliance/wasmtime/blob/2ca01ae9478f199337cf743a6ab543e8c3f3b238/crates/wasi-common/src/snapshots/preview_1.rs#L578-L582
+
+Their semantics match when `pathLen` == the length of `path`, so in practice this difference won't matter match.
diff --git a/internal/wasi/wasi.go b/internal/wasi/wasi.go
index 175d68bf..e47359ce 100644
--- a/internal/wasi/wasi.go
+++ b/internal/wasi/wasi.go
@@ -80,19 +80,39 @@ const (
 	ImportClockTimeGet = `(import "wasi_snapshot_preview1" "clock_time_get"
     (func $wasi.clock_time_get (param $id i32) (param $precision i64) (param $result.timestamp i32) (result (;errno;) i32)))`
 
-	FunctionFdAdvise             = "fd_advise"
-	FunctionFdAllocate           = "fd_allocate"
-	FunctionFdClose              = "fd_close"
-	FunctionFdDataSync           = "fd_datasync"
-	FunctionFdFdstatGet          = "fd_fdstat_get"
-	FunctionFdFdstatSetFlags     = "fd_fdstat_set_flags"
-	FunctionFdFdstatSetRights    = "fd_fdstat_set_rights"
-	FunctionFdFilestatGet        = "fd_filestat_get"
-	FunctionFdFilestatSetSize    = "fd_filestat_set_size"
-	FunctionFdFilestatSetTimes   = "fd_filestat_set_times"
-	FunctionFdPread              = "fd_pread"
-	FunctionFdPrestatGet         = "fd_prestat_get"
-	FunctionFdPrestatDirName     = "fd_prestat_dir_name"
+	FunctionFdAdvise   = "fd_advise"
+	FunctionFdAllocate = "fd_allocate"
+
+	// FunctionFdClose closes a file descriptor.
+	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_close
+	FunctionFdClose = "fd_close"
+	// ImportFdClose is the WebAssembly 1.0 (MVP) Text format import of FunctionFdClose
+	ImportFdClose = `(import "wasi_snapshot_preview1" "fd_close"
+    (func $wasi.fd_close (param $fd i32) (result (;errno;) i32)))`
+
+	FunctionFdDataSync         = "fd_datasync"
+	FunctionFdFdstatGet        = "fd_fdstat_get"
+	FunctionFdFdstatSetFlags   = "fd_fdstat_set_flags"
+	FunctionFdFdstatSetRights  = "fd_fdstat_set_rights"
+	FunctionFdFilestatGet      = "fd_filestat_get"
+	FunctionFdFilestatSetSize  = "fd_filestat_set_size"
+	FunctionFdFilestatSetTimes = "fd_filestat_set_times"
+	FunctionFdPread            = "fd_pread"
+
+	// FunctionFdPrestatGet returns the prestat data of a file descriptor.
+	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_get
+	FunctionFdPrestatGet = "fd_prestat_get"
+	// ImportFdPrestatGet is the WebAssembly 1.0 (MVP) Text format import of FunctionFdPrestatGet
+	ImportFdPrestatGet = `(import "wasi_snapshot_preview1" "fd_prestat_get"
+    (func $wasi.fd_prestat_get (param $fd i32) (param $result.prestat i32) (result (;errno;) i32)))`
+
+	// FunctionFdPrestatDirName returns the path of the pre-opened directory of a file descriptor.
+	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name
+	FunctionFdPrestatDirName = "fd_prestat_dir_name"
+	// ImportFdPrestatDirName is the WebAssembly 1.0 (MVP) Text format import of FunctionFdPrestatGet
+	ImportFdPrestatDirName = `(import "wasi_snapshot_preview1" "fd_prestat_dir_name"
+    (func $wasi.fd_prestat_dir_name (param $fd i32) (param $path i32) (param $path_len i32) (result (;errno;) i32)))`
+
 	FunctionFdPwrite             = "fd_pwrite"
 	FunctionFdRead               = "fd_read"
 	FunctionFdReaddir            = "fd_readdir"
@@ -149,6 +169,8 @@ const (
 // parameter is a memory offset to write the result to. As memory offsets are uint32, each parameter representing a
 // result is uint32.
 //
+// Note: The WASI specification is sometimes ambiguous. Please see internal/wasi/RATIONALE.md for rationale on decisions that impact portability.
+//
 // Note: Errno mappings are not defined in WASI, yet, so these mappings are best efforts by maintainers.
 //
 // Note: In WebAssembly 1.0 (MVP), there may be up to one Memory per store, which means wasm.Memory is always the
@@ -297,7 +319,17 @@ type SnapshotPreview1 interface {
 
 	// TODO: wasi.FdAdvise
 	// TODO: wasi.FdAllocate
-	// TODO: wasi.FdClose
+
+	// FdClose is the WASI function to close a file descriptor. This returns ErrnoBadf if the fd is invalid.
+	//
+	// * fd - the file decriptor to close
+	//
+	// Note: ImportFdClose shows this signature in the WebAssembly 1.0 (MVP) Text Format.
+	// Note: This is similar to `close` in POSIX.
+	// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_close
+	// See https://linux.die.net/man/3/close
+	FdClose(ctx wasm.ModuleContext, fd uint32) wasi.Errno
+
 	// TODO: wasi.FdDataSync
 	// TODO: wasi.FdFdstatGet
 	// TODO: wasi.FdFdstatSetFlags
@@ -306,10 +338,113 @@ type SnapshotPreview1 interface {
 	// TODO: wasi.FdFilestatSetSize
 	// TODO: wasi.FdFilestatSetTimes
 	// TODO: wasi.FdPread
-	// TODO: wasi.FdPrestatGet
-	// TODO: wasi.FdPrestatDirName
+
+	// FdPrestatGet is the WASI function to return the prestat data of a file descriptor.
+	// This returns ErrnoBadf if the fd is invalid.
+	//
+	// * fd - the file decriptor to get the prestat
+	// * resultPrestat - the offset to write the result prestat data
+	//
+	// A prestat is a union data which consists of two fields, uint8 `tag` indicating the type of the prestat data,
+	// and uint32 contents data that varies according to `tag`. They have the following memory layout.
+	// * tag - uint8. offset 0
+	// * contents - uint32. offset 4
+	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#prestat
+	//
+	// For example, suppose fd 3 is a file descriptor with a prestat data of tag = `prestat_dir` (value: 0).
+	// For this tag, the contents is prNameLen. Suppose it has value 3 in this example.
+	// Now, if FdPrestatGet parameters fd = 3 and resultPrestat = 1, we expect `ctx.Memory` to contain:
+	//
+	//                     padding   uint32le
+	//          uint8 --+  +-----+  +--------+
+	//                  |  |     |  |        |
+	//        []byte{?, 0, 0, 0, 0, 3, 0, 0, 0, ?}
+	//  resultPrestat --^           ^
+	//            tag --+           |
+	//                              +-- prNameLen
+	//
+	// Note: ImportFdPrestatGet shows this signature in the WebAssembly 1.0 (MVP) Text Format.
+	// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_prestat_get
+	FdPrestatGet(ctx wasm.ModuleContext, fd uint32, resultPrestat uint32) wasi.Errno
+
+	// FdPrestatDirName is the WASI function to return the path of the pre-opened directory of a file descriptor.
+	//
+	// * fd - the file decriptor to get the path of the pre-opened directory
+	// * path - the offset in `ctx.Memory` to write the result path
+	// * pathLen - FdPrestatDirName writes the result path string to `path` offset for the length of `pathLen`.
+	//
+	// FdPrestatDirName returns the following errors.
+	// * ErrnoBadf - if `fd` is invalid
+	// * ErrnoFault - if `path` is an invalid offset due to the memory constraint
+	// * ErrnoNametoolong - if `pathLen` is longer than the actual length of the result path
+	// TODO: FdPrestatDirName may have to return ErrnoNotdir if the type of the prestat data of `fd` is not a PrestatDir.
+	//
+	// For example, if fd 3 is a file with path = "test",
+	// and FdPrestatDirName parameters fd = 3, path = 1, and pathLen = 3, we expect `ctx.Memory` to contain:
+	//
+	//                pathLen
+	//              +---------+
+	//              |         |
+	//   []byte{?, 't', 'e', 's', ?, ?, ?}
+	//       path --^
+	//
+	// Note: `prNameLen` field of the result of FdPrestatGet has the exact length of the actual path.
+	// See FdPrestatGet
+	// Note: Some runtimes may have another semantics. See internal/wasi/RATIONALE.md#FdPrestatDirName
+	// Note: ImportFdPrestatDirName shows this signature in the WebAssembly 1.0 (MVP) Text Format.
+	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name
+	FdPrestatDirName(ctx wasm.ModuleContext, fd uint32, path uint32, pathLen uint32) wasi.Errno
+
 	// TODO: wasi.FdPwrite
+
 	// TODO: wasi.FdRead
+	// FdRead is the WASI function to read from a file descriptor.
+	//
+	// * fd - the file decriptor to read from
+	// * iovs - the offset in `ctx.Memory` that contains a series of `iovec`, which indicates where to write the bytes read.
+	// * iovsLen - the number of `ioveC`s in `iovs`
+	// * resultSize - the offset in `ctx.Memory` to write the number of bytes read
+	// `iovec` is a memory offset / length pair, which are named buf/bufLen, encoded sequentially as uint32le.
+	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#iovec
+	//
+	// FdRead returns the following errors.
+	// * ErrnoBadf - if `fd` is invalid
+	// * ErrnoFault - if `iovs` or `resultSize` contain an invalid offset due to the memory constraint
+	// * ErrnoIo - if an IO related error happens during the operation
+	//
+	// For example, suppose fd 3 is a file with the contents "test",
+	// and FdRead parameters fd = 3, iovs = 1, and iovsLen = 2, resultSize = 24
+	// and two `iovec`s are {buf: 18, bufLen: 2} and {buf: 21, bufLen: 2}.
+	// Now, `ctx.Memory` contains
+	//
+	//                      iovs[0]                  iovs[1]
+	//              +--------------------+   +--------------------+
+	//              |uint32le    uint32le|   |uint32le    uint32le|
+	//              +--------+  +--------+   +--------+  +--------+
+	//              |        |  |        |   |        |  |        |
+	//   []byte{?, 18, 0, 0, 0, 2, 0, 0, 0, 21, 0, 0, 0, 2, 0, 0, 0, ?... }
+	//       iovs --^           ^            ^           ^    17th --^
+	//              |           |            |           |
+	//        buf --+  bufLen --+      buf --+  bufLen --+
+	//
+	// After FdRead completes, we expect `ctx.Memory` to contain:
+	//
+	//                             iovs[0]      iovs[1]
+	//                             bufLen       bufLen       uint32le
+	//                             +----+       +----+      +--------+
+	//                             |    |       |    |      |        |
+	//   []byte{ same as above.., 't', 'e', ?, 's', 't', ?, 4, 0, 0, 0 }
+	//               iovs[0] buf --^            ^           ^
+	//                  (= 18th)  iovs[1] buf --+           |
+	//                               (= 21st)  resultSize --+
+	//                                           (= 24th)
+	//
+	// Note: ImportFdRead shows this signature in the WebAssembly 1.0 (MVP) Text Format.
+	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_read
+	// Note: This is similar to `readv` in POSIX.
+	// See https://linux.die.net/man/3/readv
+	// FdRead(ctx wasm.ModuleContext, fd uint32, iovs uint32, iovsLen uint32, resultSize uint32) wasi.Errno
+
 	// TODO: wasi.FdReaddir
 	// TODO: wasi.FdRenumber
 	// TODO: wasi.FdSeek
@@ -396,7 +531,7 @@ func SnapshotPreview1Functions(opts ...Option) (nameToGoFunc map[string]interfac
 		FunctionClockTimeGet: a.ClockTimeGet,
 		// TODO: FunctionFdAdvise
 		// TODO: FunctionFdAllocate
-		FunctionFdClose: a.fd_close,
+		FunctionFdClose: a.FdClose,
 		// TODO: FunctionFdDataSync
 		FunctionFdFdstatGet: a.fd_fdstat_get,
 		// TODO: FunctionFdFdstatSetFlags
@@ -405,8 +540,8 @@ func SnapshotPreview1Functions(opts ...Option) (nameToGoFunc map[string]interfac
 		// TODO: FunctionFdFilestatSetSize
 		// TODO: FunctionFdFilestatSetTimes
 		// TODO: FunctionFdPread
-		FunctionFdPrestatGet:     a.fd_prestat_get,
-		FunctionFdPrestatDirName: a.fd_prestat_dir_name,
+		FunctionFdPrestatGet:     a.FdPrestatGet,
+		FunctionFdPrestatDirName: a.FdPrestatDirName,
 		// TODO: FunctionFdPwrite
 		FunctionFdRead: a.fd_read,
 		// TODO: FunctionFdReaddir
@@ -504,139 +639,19 @@ func (a *wasiAPI) ClockTimeGet(ctx wasm.ModuleContext, id uint32, precision uint
 	return wasi.ErrnoSuccess
 }
 
-// ProcExit implements SnapshotPreview1.ProcExit
-func (a *wasiAPI) ProcExit(exitCode uint32) {
-	// Panic in a host function is caught by the engines, and the value of the panic is returned as the error of the CallFunction.
-	// See the document of API.ProcExit.
-	panic(wasi.ExitCode(exitCode))
-}
-
-type fileEntry struct {
-	path    string
-	fileSys wasi.FS
-	file    wasi.File
-}
-
-type Option func(*wasiAPI)
-
-func Stdin(reader io.Reader) Option {
-	return func(a *wasiAPI) {
-		a.stdin = reader
-	}
-}
-
-func Stdout(writer io.Writer) Option {
-	return func(a *wasiAPI) {
-		a.stdout = writer
-	}
-}
-
-func Stderr(writer io.Writer) Option {
-	return func(a *wasiAPI) {
-		a.stderr = writer
-	}
-}
-
-// Args returns an option to give a command-line arguments in SnapshotPreview1 or errs if the inputs are too large.
-//
-// Note: The only reason to set this is to control what's written by SnapshotPreview1.ArgsSizesGet and SnapshotPreview1.ArgsGet
-// Note: While similar in structure to os.Args, this controls what's visible in Wasm (ex the WASI function "_start").
-func Args(args ...string) (Option, error) {
-	wasiStrings, err := newNullTerminatedStrings(math.MaxUint32, "arg", args...) // TODO: this is crazy high even if spec allows it
-	if err != nil {
-		return nil, err
-	}
-	return func(a *wasiAPI) {
-		a.args = wasiStrings
-	}, nil
-}
-
-// Environ returns an option to set environment variables in SnapshotPreview1.
-// Environ returns an error if the input contains a string not joined with `=`, or if the inputs are too large.
-//  * environ: environment variables in the same format as that of `os.Environ`, where key/value pairs are joined with `=`.
-// See os.Environ
-//
-// Note: Implicit environment variable propagation into WASI is intentionally not done.
-// Note: The only reason to set this is to control what's written by SnapshotPreview1.EnvironSizesGet and SnapshotPreview1.EnvironGet
-// Note: While similar in structure to os.Environ, this controls what's visible in Wasm (ex the WASI function "_start").
-func Environ(environ ...string) (Option, error) {
-	for i, env := range environ {
-		if !strings.Contains(env, "=") {
-			return nil, fmt.Errorf("environ[%d] is not joined with '='", i)
-		}
-	}
-	wasiStrings, err := newNullTerminatedStrings(math.MaxUint32, "environ", environ...) // TODO: this is crazy high even if spec allows it
-	if err != nil {
-		return nil, err
-	}
-	return func(w *wasiAPI) {
-		w.environ = wasiStrings
-	}, nil
-}
-
-func Preopen(dir string, fileSys wasi.FS) Option {
-	return func(a *wasiAPI) {
-		a.opened[uint32(len(a.opened))+3] = fileEntry{
-			path:    dir,
-			fileSys: fileSys,
-		}
-	}
-}
-
-func newAPI(opts ...Option) *wasiAPI {
-	ret := &wasiAPI{
-		args:    &nullTerminatedStrings{},
-		environ: &nullTerminatedStrings{},
-		stdin:   os.Stdin,
-		stdout:  os.Stdout,
-		stderr:  os.Stderr,
-		opened:  map[uint32]fileEntry{},
-		timeNowUnixNano: func() uint64 {
-			return uint64(time.Now().UnixNano())
-		},
-		randSource: func(p []byte) error {
-			_, err := crand.Read(p)
-			return err
-		},
-	}
-
-	// apply functional options
-	for _, f := range opts {
-		f(ret)
-	}
-	return ret
-}
-
-func (a *wasiAPI) randUnusedFD() uint32 {
-	fd := uint32(mrand.Int31())
-	for {
-		if _, ok := a.opened[fd]; !ok {
-			return fd
-		}
-		fd = (fd + 1) % (1 << 31)
-	}
-}
-
-func (a *wasiAPI) fd_prestat_get(ctx wasm.ModuleContext, fd uint32, bufPtr uint32) wasi.Errno {
-	if _, ok := a.opened[fd]; !ok {
-		return wasi.ErrnoBadf
-	}
-	return wasi.ErrnoSuccess
-}
-
-func (a *wasiAPI) fd_prestat_dir_name(ctx wasm.ModuleContext, fd uint32, pathPtr uint32, pathLen uint32) wasi.Errno {
+// FdClose implements SnaphotPreview1.FdClose
+func (a *wasiAPI) FdClose(ctx wasm.ModuleContext, fd uint32) wasi.Errno {
 	f, ok := a.opened[fd]
 	if !ok {
-		return wasi.ErrnoInval
+		return wasi.ErrnoBadf
 	}
 
-	if uint32(len(f.path)) < pathLen {
-		return wasi.ErrnoNametoolong
+	if f.file != nil {
+		f.file.Close()
 	}
 
-	if !ctx.Memory().Write(pathPtr, []byte(f.path)) {
-		return wasi.ErrnoFault
-	}
+	delete(a.opened, fd)
+
 	return wasi.ErrnoSuccess
 }
 
@@ -650,62 +665,49 @@ func (a *wasiAPI) fd_fdstat_get(ctx wasm.ModuleContext, fd uint32, bufPtr uint32
 	return wasi.ErrnoSuccess
 }
 
-func (a *wasiAPI) path_open(ctx wasm.ModuleContext, fd, dirFlags, pathPtr, pathLen, oFlags uint32,
-	fsRightsBase, fsRightsInheriting uint64,
-	fdFlags, fdPtr uint32) (errno wasi.Errno) {
-	dir, ok := a.opened[fd]
-	if !ok || dir.fileSys == nil {
-		return wasi.ErrnoInval
+// FdPrestatGet implements SnahpshotPreview1.FdPrestatGet
+// TODO: Currently FdPrestatGet implements nothing except returning ErrnoBadf
+func (a *wasiAPI) FdPrestatGet(ctx wasm.ModuleContext, fd uint32, bufPtr uint32) wasi.Errno {
+	if _, ok := a.opened[fd]; !ok {
+		return wasi.ErrnoBadf
 	}
+	return wasi.ErrnoSuccess
+}
 
-	b, ok := ctx.Memory().Read(pathPtr, pathLen)
+// FdPrestatDirName implements SnahpshotPreview1.FdPrestatDirName
+func (a *wasiAPI) FdPrestatDirName(ctx wasm.ModuleContext, fd uint32, pathPtr uint32, pathLen uint32) wasi.Errno {
+	f, ok := a.opened[fd]
 	if !ok {
-		return wasi.ErrnoFault
-	}
-	path := string(b)
-	f, err := dir.fileSys.OpenWASI(dirFlags, path, oFlags, fsRightsBase, fsRightsInheriting, fdFlags)
-	if err != nil {
-		switch {
-		case errors.Is(err, fs.ErrNotExist):
-			return wasi.ErrnoNoent
-		default:
-			return wasi.ErrnoInval
-		}
+		return wasi.ErrnoBadf
 	}
 
-	newFD := a.randUnusedFD()
-
-	a.opened[newFD] = fileEntry{
-		file: f,
+	// Some runtimes may have another semantics. See internal/wasi/RATIONALE.md
+	if uint32(len(f.path)) < pathLen {
+		return wasi.ErrnoNametoolong
 	}
 
-	if !ctx.Memory().WriteUint32Le(fdPtr, newFD) {
+	// TODO: FdPrestatDirName may have to return ErrnoNotdir if the type of the prestat data of `fd` is not a PrestatDir.
+	if !ctx.Memory().Write(pathPtr, []byte(f.path)[:pathLen]) {
 		return wasi.ErrnoFault
 	}
 	return wasi.ErrnoSuccess
 }
 
-func (a *wasiAPI) fd_seek(ctx wasm.ModuleContext, fd uint32, offset uint64, whence uint32, nwrittenPtr uint32) wasi.Errno {
-	return wasi.ErrnoNosys // TODO: implement
-}
-
-func (a *wasiAPI) fd_write(ctx wasm.ModuleContext, fd uint32, iovsPtr uint32, iovsLen uint32, nwrittenPtr uint32) wasi.Errno {
-	var writer io.Writer
+func (a *wasiAPI) fd_read(ctx wasm.ModuleContext, fd uint32, iovsPtr uint32, iovsLen uint32, nreadPtr uint32) wasi.Errno {
+	var reader io.Reader
 
 	switch fd {
-	case 1:
-		writer = a.stdout
-	case 2:
-		writer = a.stderr
+	case 0:
+		reader = a.stdin
 	default:
 		f, ok := a.opened[fd]
 		if !ok || f.file == nil {
 			return wasi.ErrnoBadf
 		}
-		writer = f.file
+		reader = f.file
 	}
 
-	var nwritten uint32
+	var nread uint32
 	for i := uint32(0); i < iovsLen; i++ {
 		iovPtr := iovsPtr + i*8
 		offset, ok := ctx.Memory().ReadUint32Le(iovPtr)
@@ -720,33 +722,41 @@ func (a *wasiAPI) fd_write(ctx wasm.ModuleContext, fd uint32, iovsPtr uint32, io
 		if !ok {
 			return wasi.ErrnoFault
 		}
-		n, err := writer.Write(b)
-		if err != nil {
-			panic(err)
+		n, err := reader.Read(b)
+		nread += uint32(n)
+		if errors.Is(err, io.EOF) {
+			break
+		} else if err != nil {
+			return wasi.ErrnoIo
 		}
-		nwritten += uint32(n)
 	}
-	if !ctx.Memory().WriteUint32Le(nwrittenPtr, nwritten) {
+	if !ctx.Memory().WriteUint32Le(nreadPtr, nread) {
 		return wasi.ErrnoFault
 	}
 	return wasi.ErrnoSuccess
 }
 
-func (a *wasiAPI) fd_read(ctx wasm.ModuleContext, fd uint32, iovsPtr uint32, iovsLen uint32, nreadPtr uint32) wasi.Errno {
-	var reader io.Reader
+func (a *wasiAPI) fd_seek(ctx wasm.ModuleContext, fd uint32, offset uint64, whence uint32, nwrittenPtr uint32) wasi.Errno {
+	return wasi.ErrnoNosys // TODO: implement
+}
+
+func (a *wasiAPI) fd_write(ctx wasm.ModuleContext, fd uint32, iovsPtr uint32, iovsLen uint32, nwrittenPtr uint32) wasi.Errno {
+	var writer io.Writer
 
 	switch fd {
-	case 0:
-		reader = a.stdin
+	case 1:
+		writer = a.stdout
+	case 2:
+		writer = a.stderr
 	default:
 		f, ok := a.opened[fd]
 		if !ok || f.file == nil {
 			return wasi.ErrnoBadf
 		}
-		reader = f.file
+		writer = f.file
 	}
 
-	var nread uint32
+	var nwritten uint32
 	for i := uint32(0); i < iovsLen; i++ {
 		iovPtr := iovsPtr + i*8
 		offset, ok := ctx.Memory().ReadUint32Le(iovPtr)
@@ -761,35 +771,60 @@ func (a *wasiAPI) fd_read(ctx wasm.ModuleContext, fd uint32, iovsPtr uint32, iov
 		if !ok {
 			return wasi.ErrnoFault
 		}
-		n, err := reader.Read(b)
-		nread += uint32(n)
-		if errors.Is(err, io.EOF) {
-			break
-		} else if err != nil {
-			return wasi.ErrnoIo
+		n, err := writer.Write(b)
+		if err != nil {
+			panic(err)
 		}
+		nwritten += uint32(n)
 	}
-	if !ctx.Memory().WriteUint32Le(nreadPtr, nread) {
+	if !ctx.Memory().WriteUint32Le(nwrittenPtr, nwritten) {
 		return wasi.ErrnoFault
 	}
 	return wasi.ErrnoSuccess
 }
 
-func (a *wasiAPI) fd_close(ctx wasm.ModuleContext, fd uint32) wasi.Errno {
-	f, ok := a.opened[fd]
-	if !ok {
-		return wasi.ErrnoBadf
+func (a *wasiAPI) path_open(ctx wasm.ModuleContext, fd, dirFlags, pathPtr, pathLen, oFlags uint32,
+	fsRightsBase, fsRightsInheriting uint64,
+	fdFlags, fdPtr uint32) (errno wasi.Errno) {
+	dir, ok := a.opened[fd]
+	if !ok || dir.fileSys == nil {
+		return wasi.ErrnoInval
 	}
 
-	if f.file != nil {
-		f.file.Close()
+	b, ok := ctx.Memory().Read(pathPtr, pathLen)
+	if !ok {
+		return wasi.ErrnoFault
+	}
+	path := string(b)
+	f, err := dir.fileSys.OpenWASI(dirFlags, path, oFlags, fsRightsBase, fsRightsInheriting, fdFlags)
+	if err != nil {
+		switch {
+		case errors.Is(err, fs.ErrNotExist):
+			return wasi.ErrnoNoent
+		default:
+			return wasi.ErrnoInval
+		}
 	}
 
-	delete(a.opened, fd)
+	newFD := a.randUnusedFD()
 
+	a.opened[newFD] = fileEntry{
+		file: f,
+	}
+
+	if !ctx.Memory().WriteUint32Le(fdPtr, newFD) {
+		return wasi.ErrnoFault
+	}
 	return wasi.ErrnoSuccess
 }
 
+// ProcExit implements SnapshotPreview1.ProcExit
+func (a *wasiAPI) ProcExit(exitCode uint32) {
+	// Panic in a host function is caught by the engines, and the value of the panic is returned as the error of the CallFunction.
+	// See the document of API.ProcExit.
+	panic(wasi.ExitCode(exitCode))
+}
+
 // RandomGet implements SnapshotPreview1.RandomGet
 func (a *wasiAPI) RandomGet(ctx wasm.ModuleContext, buf uint32, bufLen uint32) (errno wasi.Errno) {
 	randomBytes := make([]byte, bufLen)
@@ -806,6 +841,112 @@ func (a *wasiAPI) RandomGet(ctx wasm.ModuleContext, buf uint32, bufLen uint32) (
 	return wasi.ErrnoSuccess
 }
 
+type fileEntry struct {
+	path    string
+	fileSys wasi.FS
+	file    wasi.File
+}
+
+type Option func(*wasiAPI)
+
+func Stdin(reader io.Reader) Option {
+	return func(a *wasiAPI) {
+		a.stdin = reader
+	}
+}
+
+func Stdout(writer io.Writer) Option {
+	return func(a *wasiAPI) {
+		a.stdout = writer
+	}
+}
+
+func Stderr(writer io.Writer) Option {
+	return func(a *wasiAPI) {
+		a.stderr = writer
+	}
+}
+
+// Args returns an option to give a command-line arguments in SnapshotPreview1 or errs if the inputs are too large.
+//
+// Note: The only reason to set this is to control what's written by SnapshotPreview1.ArgsSizesGet and SnapshotPreview1.ArgsGet
+// Note: While similar in structure to os.Args, this controls what's visible in Wasm (ex the WASI function "_start").
+func Args(args ...string) (Option, error) {
+	wasiStrings, err := newNullTerminatedStrings(math.MaxUint32, "arg", args...) // TODO: this is crazy high even if spec allows it
+	if err != nil {
+		return nil, err
+	}
+	return func(a *wasiAPI) {
+		a.args = wasiStrings
+	}, nil
+}
+
+// Environ returns an option to set environment variables in SnapshotPreview1.
+// Environ returns an error if the input contains a string not joined with `=`, or if the inputs are too large.
+//  * environ: environment variables in the same format as that of `os.Environ`, where key/value pairs are joined with `=`.
+// See os.Environ
+//
+// Note: Implicit environment variable propagation into WASI is intentionally not done.
+// Note: The only reason to set this is to control what's written by SnapshotPreview1.EnvironSizesGet and SnapshotPreview1.EnvironGet
+// Note: While similar in structure to os.Environ, this controls what's visible in Wasm (ex the WASI function "_start").
+func Environ(environ ...string) (Option, error) {
+	for i, env := range environ {
+		if !strings.Contains(env, "=") {
+			return nil, fmt.Errorf("environ[%d] is not joined with '='", i)
+		}
+	}
+	wasiStrings, err := newNullTerminatedStrings(math.MaxUint32, "environ", environ...) // TODO: this is crazy high even if spec allows it
+	if err != nil {
+		return nil, err
+	}
+	return func(w *wasiAPI) {
+		w.environ = wasiStrings
+	}, nil
+}
+
+func Preopen(dir string, fileSys wasi.FS) Option {
+	return func(a *wasiAPI) {
+		a.opened[uint32(len(a.opened))+3] = fileEntry{
+			path:    dir,
+			fileSys: fileSys,
+		}
+	}
+}
+
+func newAPI(opts ...Option) *wasiAPI {
+	ret := &wasiAPI{
+		args:    &nullTerminatedStrings{},
+		environ: &nullTerminatedStrings{},
+		stdin:   os.Stdin,
+		stdout:  os.Stdout,
+		stderr:  os.Stderr,
+		opened:  map[uint32]fileEntry{},
+		timeNowUnixNano: func() uint64 {
+			return uint64(time.Now().UnixNano())
+		},
+		randSource: func(p []byte) error {
+			_, err := crand.Read(p)
+			return err
+		},
+	}
+
+	// apply functional options
+	for _, f := range opts {
+		f(ret)
+	}
+	return ret
+}
+
+func (a *wasiAPI) randUnusedFD() uint32 {
+	fd := uint32(mrand.Int31())
+	for {
+		if _, ok := a.opened[fd]; !ok {
+			return fd
+		}
+		fd = (fd + 1) % (1 << 31)
+	}
+}
+
 func ValidateWASICommand(module *internalwasm.Module, moduleName string) error {
 	if start, err := requireExport(module, moduleName, FunctionStart, internalwasm.ExportKindFunc); err != nil {
 		return err


Test patch:
diff --git a/internal/wasi/wasi_test.go b/internal/wasi/wasi_test.go
index 90afb8ff..978644f9 100644
--- a/internal/wasi/wasi_test.go
+++ b/internal/wasi/wasi_test.go
@@ -475,7 +475,51 @@ func TestAPI_ClockTimeGet_Errors(t *testing.T) {
 
 // TODO: TestAPI_FdAdvise TestAPI_FdAdvise_Errors
 // TODO: TestAPI_FdAllocate TestAPI_FdAllocate_Errors
-// TODO: TestAPI_FdClose TestAPI_FdClose_Errors
+
+func TestAPI_FdClose(t *testing.T) {
+	fdToClose := uint32(3) // arbitrary fd
+	fdToKeep := uint32(4)  // another arbitrary fd
+	setupFD := func() (*wasm.Store, *wasm.ModuleContext, *wasm.FunctionInstance, *wasiAPI) {
+		var api *wasiAPI
+		store, ctx, fn := instantiateWasmStore(t, FunctionFdClose, ImportFdClose, "test", func(a *wasiAPI) {
+			memFs := &MemFS{}
+			a.opened = map[uint32]fileEntry{
+				fdToClose: {
+					path:    "test",
+					fileSys: memFs,
+				},
+				fdToKeep: {
+					path:    "path to keep",
+					fileSys: memFs,
+				},
+			}
+			api = a // for later tests
+		})
+		return store, ctx, fn, api
+	}
+
+	t.Run("SnapshotPreview1.FdClose", func(t *testing.T) {
+		_, ctx, _, api := setupFD()
+		errno := api.FdClose(ctx, fdToClose)
+		require.Equal(t, wasi.ErrnoSuccess, errno)
+		require.NotContains(t, api.opened, fdToClose) // Fd is closed and removed from the opened FDs.
+		require.Contains(t, api.opened, fdToKeep)
+	})
+	t.Run(FunctionFdClose, func(t *testing.T) {
+		store, ctx, fn, api := setupFD()
+		ret, err := store.Engine.Call(ctx, fn, uint64(fdToClose), uint64(fdToClose))
+		require.NoError(t, err)
+		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64
+		require.NotContains(t, api.opened, fdToClose)           // Fd is closed and removed from the opened FDs.
+		require.Contains(t, api.opened, fdToKeep)
+	})
+	t.Run("ErrnoBadF for an invalid FD", func(t *testing.T) {
+		_, ctx, _, api := setupFD()
+		errno := api.FdClose(ctx, 42) // 42 is an arbitrary invalid FD
+		require.Equal(t, wasi.ErrnoBadf, errno)
+	})
+}
+
 // TODO: TestAPI_FdDataSync TestAPI_FdDataSync_Errors
 // TODO: TestAPI_FdFdstatGet TestAPI_FdFdstatGet_Errors
 // TODO: TestAPI_FdFdstatSetFlags TestAPI_FdFdstatSetFlags_Errors
@@ -485,7 +529,102 @@ func TestAPI_ClockTimeGet_Errors(t *testing.T) {
 // TODO: TestAPI_FdFilestatSetTimes TestAPI_FdFilestatSetTimes_Errors
 // TODO: TestAPI_FdPread TestAPI_FdPread_Errors
 // TODO: TestAPI_FdPrestatGet TestAPI_FdPrestatGet_Errors
-// TODO: TestAPI_FdPrestatDirName TestAPI_FdPrestatDirName_Errors
+
+func TestAPI_FdPrestatDirName(t *testing.T) {
+	fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
+	var api *wasiAPI
+	store, ctx, fn := instantiateWasmStore(t, FunctionFdPrestatDirName, ImportFdPrestatDirName, "test", func(a *wasiAPI) {
+		a.opened[fd] = fileEntry{
+			path:    "test",
+			fileSys: &MemFS{},
+		}
+		api = a // for later tests
+	})
+
+	path := uint32(1)    // arbitrary offset
+	pathLen := uint32(3) // shorter than len("test") to test the path is written for the length of pathLen
+	maskLength := 7      // number of bytes to write '?' to tell what we've written
+	expectedMemory := []byte{
+		'?',
+		't', 'e', 's',
+		'?', '?', '?',
+	}
+
+	t.Run("SnapshotPreview1.FdPrestatDirName", func(t *testing.T) {
+		maskMemory(store, maskLength)
+
+		errno := api.FdPrestatDirName(ctx, fd, path, pathLen)
+		require.Equal(t, wasi.ErrnoSuccess, errno)
+		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
+	})
+	t.Run(FunctionFdPrestatDirName, func(t *testing.T) {
+		maskMemory(store, maskLength)
+
+		ret, err := store.Engine.Call(ctx, fn, uint64(fd), uint64(path), uint64(pathLen))
+		require.NoError(t, err)
+		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64
+		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
+	})
+}
+
+func TestAPI_FdPrestatDirName_Errors(t *testing.T) {
+	dirName := "test"
+	opt := Preopen(dirName, &MemFS{})
+	store, ctx, fn := instantiateWasmStore(t, FunctionFdPrestatDirName, ImportFdPrestatDirName, "test", opt)
+
+	memorySize := uint32(len(store.Memories[0].Buffer))
+	validAddress := uint32(0)     // Arbitrary valid address as arguments to fd_prestat_dir_name. We chose 0 here.
+	actualPathLen := len(dirName) // Actual length of the dirName as a valid pathLen.
+	fd := uint32(3)               // fd 3 will be opened for the "test" directory after 0, 1, and 2, that are stdin/out/err
+
+	tests := []struct {
+		name          string
+		fd            uint32
+		path          uint32
+		pathLen       uint32
+		expectedErrno wasi.Errno
+	}{
+		{
+			name:          "out-of-memory path",
+			fd:            fd,
+			path:          memorySize,
+			pathLen:       uint32(actualPathLen),
+			expectedErrno: wasi.ErrnoFault,
+		},
+		{
+			name:          "path exceeds the maximum valid address by 1",
+			fd:            fd,
+			path:          memorySize - uint32(actualPathLen) + 1,
+			pathLen:       uint32(actualPathLen),
+			expectedErrno: wasi.ErrnoFault,
+		},
+		{
+			name:          "pathLen exceeds the actual length of the dir name",
+			fd:            fd,
+			path:          validAddress,
+			pathLen:       uint32(actualPathLen) + 1,
+			expectedErrno: wasi.ErrnoNametoolong,
+		},
+		{
+			name:          "invalid fd",
+			fd:            42, // arbitrary invalid fd
+			path:          validAddress,
+			pathLen:       uint32(actualPathLen) + 1,
+			expectedErrno: wasi.ErrnoBadf,
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			results, err := store.Engine.Call(ctx, fn, uint64(tc.fd), uint64(tc.path), uint64(tc.pathLen))
+			require.NoError(t, err)
+			require.Equal(t, tc.expectedErrno, wasi.Errno(results[0])) // results[0] is the errno
+		})
+	}
+}
+
 // TODO: TestAPI_FdPwrite TestAPI_FdPwrite_Errors
 // TODO: TestAPI_FdRead TestAPI_FdRead_Errors
 // TODO: TestAPI_FdReaddir TestAPI_FdReaddir_Errors