Repository: tetratelabs/wazero
Pull request #579 (2022-05-19 19:32:46)

experimental: consolidates context patterns
This consolidates the pattern used for context overrides, notably
replacing clock overrides via experimental.WithTimeNowUnixNano
and making all context keys internal.

This also makes sure experimental example tests are handled the same
way, notably backfilling one for WithFS

codefromthecrypt: cc @pkedy @sam-at-luther @knqyf263 on the experimental sig change. I think once we figure out what's going on with WASI 2, we'll end up with either a bunch of WithXXX functions or possibly a ContextBuilder to overlay certain parts of the system context per-function in one go. For now, we'll stick with a bunch of WithXXX and keep them named same as ModuleConfig.WithXXX if they are also there.

Patch:
diff --git a/assemblyscript/assemblyscript.go b/assemblyscript/assemblyscript.go
index aeb2da08..bdc59129 100644
--- a/assemblyscript/assemblyscript.go
+++ b/assemblyscript/assemblyscript.go
@@ -100,7 +100,13 @@ func (m *moduleBuilder) Instantiate(ctx context.Context) (api.Closer, error) {
 		Instantiate(ctx)
 }
 
-// assemblyscript implements the AssemblyScript special functions.
+// assemblyScript includes "Special imports" only used In AssemblyScript when a user didn't add `import "wasi"` to their
+// entry file.
+//
+// See https://www.assemblyscript.org/concepts.html#special-imports
+// See https://www.assemblyscript.org/concepts.html#targeting-wasi
+// See https://www.assemblyscript.org/compiler.html#compiler-options
+// See https://github.com/AssemblyScript/assemblyscript/issues/1562
 type assemblyscript struct {
 	abortMessageDisabled bool
 	traceMode            traceMode
diff --git a/config.go b/config.go
index a31e498e..18c01d49 100644
--- a/config.go
+++ b/config.go
@@ -10,7 +10,7 @@ import (
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/engine/compiler"
 	"github.com/tetratelabs/wazero/internal/engine/interpreter"
-	fs2 "github.com/tetratelabs/wazero/internal/fs"
+	"github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -452,7 +452,7 @@ type moduleConfig struct {
 	// environKeys allow overwriting of existing values.
 	environKeys map[string]int
 
-	fs *fs2.FSConfig
+	fs *sys.FSConfig
 }
 
 func NewModuleConfig() ModuleConfig {
@@ -460,7 +460,7 @@ func NewModuleConfig() ModuleConfig {
 		startFunctions: []string{"_start"},
 		environKeys:    map[string]int{},
 
-		fs: fs2.NewFSConfig(),
+		fs: sys.NewFSConfig(),
 	}
 }
 
diff --git a/examples/README.md b/examples/README.md
index 146b0115..217f57af 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -3,6 +3,7 @@
 The following example projects can help you practice WebAssembly with wazero:
 
 * [allocation](allocation) - how to pass strings in and out of WebAssembly functions defined in Rust or TinyGo.
+* [assemblyscript](assemblyscript) - how to configure special imports needed by AssemblyScript when not using WASI.
 * [basic](basic) - how to use both WebAssembly and Go-defined functions.
 * [import-go](import-go) - how to define, import and call a Go-defined function from a WebAssembly-defined function.
 * [multiple-results](multiple-results) - how to return more than one result from WebAssembly or Go-defined functions.
diff --git a/experimental/fs.go b/experimental/fs.go
index b00bd3ee..9e1187ec 100644
--- a/experimental/fs.go
+++ b/experimental/fs.go
@@ -5,10 +5,12 @@ import (
 	"io/fs"
 
 	"github.com/tetratelabs/wazero/api"
-	internalfs "github.com/tetratelabs/wazero/internal/fs"
+	internalfs "github.com/tetratelabs/wazero/internal/sys"
 )
 
 // WithFS overrides fs.FS in the context-based manner. Caller needs to take responsibility for closing the filesystem.
+//
+// Note: This has the same effect as the same function name on wazero.ModuleConfig.
 func WithFS(ctx context.Context, fs fs.FS) (context.Context, api.Closer, error) {
 	fsConfig := internalfs.NewFSConfig().WithFS(fs)
 	preopens, err := fsConfig.Preopens()
@@ -16,6 +18,6 @@ func WithFS(ctx context.Context, fs fs.FS) (context.Context, api.Closer, error)
 		return nil, nil, err
 	}
 
-	fsCtx := internalfs.NewContext(preopens)
-	return context.WithValue(ctx, internalfs.Key{}, fsCtx), fsCtx, nil
+	fsCtx := internalfs.NewFSContext(preopens)
+	return context.WithValue(ctx, internalfs.FSKey{}, fsCtx), fsCtx, nil
 }
diff --git a/experimental/listener_test.go b/experimental/listener_example_test.go
similarity index 100%
rename from experimental/listener_test.go
rename to experimental/listener_example_test.go
diff --git a/experimental/sys.go b/experimental/sys.go
deleted file mode 100644
index 227e4075..00000000
--- a/experimental/sys.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package experimental
-
-// SysKey is a context.Context Value key. Its associated value should be a Sys.
-//
-// See https://github.com/tetratelabs/wazero/issues/491
-type SysKey struct{}
-
-// Sys controls experimental aspects currently only used by WASI.
-type Sys interface {
-	// TimeNowUnixNano allows you to control the value otherwise returned by time.Now().UnixNano()
-	TimeNowUnixNano() uint64
-}
diff --git a/experimental/time.go b/experimental/time.go
new file mode 100644
index 00000000..44c0cca0
--- /dev/null
+++ b/experimental/time.go
@@ -0,0 +1,12 @@
+package experimental
+
+import (
+	"context"
+
+	"github.com/tetratelabs/wazero/internal/sys"
+)
+
+// WithTimeNowUnixNano allows you to control the value otherwise returned by time.Now().UnixNano()
+func WithTimeNowUnixNano(ctx context.Context, timeUnixNano func() uint64) context.Context {
+	return context.WithValue(ctx, sys.TimeNowUnixNanoKey{}, timeUnixNano)
+}
diff --git a/internal/fs/fs.go b/internal/sys/fs.go
similarity index 89%
rename from internal/fs/fs.go
rename to internal/sys/fs.go
index fb25de87..4e72f2cd 100644
--- a/internal/fs/fs.go
+++ b/internal/sys/fs.go
@@ -1,4 +1,4 @@
-package fs
+package sys
 
 import (
 	"context"
@@ -8,10 +8,10 @@ import (
 	"sync/atomic"
 )
 
-// Key is a context.Context Value key. It allows overriding fs.FS for WASI.
+// FSKey is a context.Context Value key. It allows overriding fs.FS for WASI.
 //
 // See https://github.com/tetratelabs/wazero/issues/491
-type Key struct{}
+type FSKey struct{}
 
 // FileEntry maps a path to an open file in a file system.
 //
@@ -23,7 +23,7 @@ type FileEntry struct {
 	File fs.File
 }
 
-type Context struct {
+type FSContext struct {
 	// openedFiles is a map of file descriptor numbers (>=3) to open files (or directories) and defaults to empty.
 	// TODO: This is unguarded, so not goroutine-safe!
 	openedFiles map[uint32]*FileEntry
@@ -32,8 +32,8 @@ type Context struct {
 	lastFD uint32
 }
 
-func NewContext(openedFiles map[uint32]*FileEntry) *Context {
-	var fsCtx Context
+func NewFSContext(openedFiles map[uint32]*FileEntry) *FSContext {
+	var fsCtx FSContext
 	if openedFiles == nil {
 		fsCtx.openedFiles = map[uint32]*FileEntry{}
 		fsCtx.lastFD = 2 // STDERR
@@ -52,7 +52,7 @@ func NewContext(openedFiles map[uint32]*FileEntry) *Context {
 // nextFD gets the next file descriptor number in a goroutine safe way (monotonically) or zero if we ran out.
 // TODO: opendFiles is still not goroutine safe!
 // TODO: This can return zero if we ran out of file descriptors. A future change can optimize by re-using an FD pool.
-func (c *Context) nextFD() uint32 {
+func (c *FSContext) nextFD() uint32 {
 	if c.lastFD == math.MaxUint32 {
 		return 0
 	}
@@ -60,7 +60,7 @@ func (c *Context) nextFD() uint32 {
 }
 
 // Close implements io.Closer
-func (c *Context) Close(_ context.Context) (err error) {
+func (c *FSContext) Close(_ context.Context) (err error) {
 	// Close any files opened in this context
 	for fd, entry := range c.openedFiles {
 		delete(c.openedFiles, fd)
@@ -74,7 +74,7 @@ func (c *Context) Close(_ context.Context) (err error) {
 }
 
 // CloseFile returns true if a file was opened and closed without error, or false if not.
-func (c *Context) CloseFile(fd uint32) (bool, error) {
+func (c *FSContext) CloseFile(fd uint32) (bool, error) {
 	f, ok := c.openedFiles[fd]
 	if !ok {
 		return false, nil
@@ -91,13 +91,13 @@ func (c *Context) CloseFile(fd uint32) (bool, error) {
 }
 
 // OpenedFile returns a file and true if it was opened or nil and false, if not.
-func (c *Context) OpenedFile(fd uint32) (*FileEntry, bool) {
+func (c *FSContext) OpenedFile(fd uint32) (*FileEntry, bool) {
 	f, ok := c.openedFiles[fd]
 	return f, ok
 }
 
 // OpenFile returns the file descriptor of the new file or false if we ran out of file descriptors
-func (c *Context) OpenFile(f *FileEntry) (uint32, bool) {
+func (c *FSContext) OpenFile(f *FileEntry) (uint32, bool) {
 	newFD := c.nextFD()
 	if newFD == 0 {
 		return 0, false
diff --git a/internal/sys/time.go b/internal/sys/time.go
new file mode 100644
index 00000000..fa5e9431
--- /dev/null
+++ b/internal/sys/time.go
@@ -0,0 +1,6 @@
+package sys
+
+// TimeNowUnixNanoKey is a context.Context Value key. Its associated value should be a func() uint64.
+//
+// See https://github.com/tetratelabs/wazero/issues/491
+type TimeNowUnixNanoKey struct{}
diff --git a/internal/wasm/sys.go b/internal/wasm/sys.go
index 20977ccb..9ff71db9 100644
--- a/internal/wasm/sys.go
+++ b/internal/wasm/sys.go
@@ -6,7 +6,7 @@ import (
 	"fmt"
 	"io"
 
-	"github.com/tetratelabs/wazero/internal/fs"
+	"github.com/tetratelabs/wazero/internal/sys"
 )
 
 // SysContext holds module-scoped system resources currently only used by internalwasi.
@@ -17,7 +17,7 @@ type SysContext struct {
 	stdout, stderr        io.Writer
 	randSource            io.Reader
 
-	fs *fs.Context
+	fs *sys.FSContext
 }
 
 // Args is like os.Args and defaults to nil.
@@ -72,9 +72,9 @@ func (c *SysContext) Stderr() io.Writer {
 	return c.stderr
 }
 
-func (c *SysContext) FS() *fs.Context {
+func (c *SysContext) FS() *sys.FSContext {
 	if c.fs == nil {
-		return &fs.Context{}
+		return &sys.FSContext{}
 	}
 	return c.fs
 }
@@ -110,42 +110,42 @@ var _ = DefaultSysContext() // Force panic on bug.
 
 // NewSysContext is a factory function which helps avoid needing to know defaults or exporting all fields.
 // Note: max is exposed for testing. max is only used for env/args validation.
-func NewSysContext(max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randSource io.Reader, openedFiles map[uint32]*fs.FileEntry) (sys *SysContext, err error) {
-	sys = &SysContext{args: args, environ: environ}
+func NewSysContext(max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randSource io.Reader, openedFiles map[uint32]*sys.FileEntry) (sysCtx *SysContext, err error) {
+	sysCtx = &SysContext{args: args, environ: environ}
 
-	if sys.argsSize, err = nullTerminatedByteCount(max, args); err != nil {
+	if sysCtx.argsSize, err = nullTerminatedByteCount(max, args); err != nil {
 		return nil, fmt.Errorf("args invalid: %w", err)
 	}
 
-	if sys.environSize, err = nullTerminatedByteCount(max, environ); err != nil {
+	if sysCtx.environSize, err = nullTerminatedByteCount(max, environ); err != nil {
 		return nil, fmt.Errorf("environ invalid: %w", err)
 	}
 
 	if stdin == nil {
-		sys.stdin = eofReader{}
+		sysCtx.stdin = eofReader{}
 	} else {
-		sys.stdin = stdin
+		sysCtx.stdin = stdin
 	}
 
 	if stdout == nil {
-		sys.stdout = io.Discard
+		sysCtx.stdout = io.Discard
 	} else {
-		sys.stdout = stdout
+		sysCtx.stdout = stdout
 	}
 
 	if stderr == nil {
-		sys.stderr = io.Discard
+		sysCtx.stderr = io.Discard
 	} else {
-		sys.stderr = stderr
+		sysCtx.stderr = stderr
 	}
 
 	if randSource == nil {
-		sys.randSource = rand.Reader
+		sysCtx.randSource = rand.Reader
 	} else {
-		sys.randSource = randSource
+		sysCtx.randSource = randSource
 	}
 
-	sys.fs = fs.NewContext(openedFiles)
+	sysCtx.fs = sys.NewFSContext(openedFiles)
 
 	return
 }
diff --git a/wasi/wasi.go b/wasi/wasi.go
index d439fe4d..d5f47e41 100644
--- a/wasi/wasi.go
+++ b/wasi/wasi.go
@@ -15,8 +15,7 @@ import (
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
-	"github.com/tetratelabs/wazero/experimental"
-	internalfs "github.com/tetratelabs/wazero/internal/fs"
+	"github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -437,15 +436,13 @@ const (
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md
 // See https://github.com/WebAssembly/WASI/issues/215
 // See https://wwa.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0.
-type snapshotPreview1 struct {
-	sys experimental.Sys
-}
+type snapshotPreview1 struct{}
 
 // snapshotPreview1Functions returns all go functions that implement snapshotPreview1.
 // These should be exported in the module named "wasi_snapshot_preview1".
 // See wasm.NewHostModule
 func snapshotPreview1Functions(ctx context.Context) (a *snapshotPreview1, nameToGoFunc map[string]interface{}) {
-	a = newSnapshotPreview1(ctx)
+	a = &snapshotPreview1{}
 	// Note: these are ordered per spec for consistency even if the resulting map can't guarantee that.
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#functions
 	nameToGoFunc = map[string]interface{}{
@@ -689,7 +686,16 @@ func (a *snapshotPreview1) ClockResGet(ctx context.Context, m api.Module, id uin
 // See https://linux.die.net/man/3/clock_gettime
 func (a *snapshotPreview1) ClockTimeGet(ctx context.Context, m api.Module, id uint32, precision uint64, resultTimestamp uint32) Errno {
 	// TODO: id and precision are currently ignored.
-	if !m.Memory().WriteUint64Le(ctx, resultTimestamp, a.sys.TimeNowUnixNano()) {
+	// Override Context when it is passed via context
+	clock := timeNowUnixNano
+	if clockVal := ctx.Value(sys.TimeNowUnixNanoKey{}); clockVal != nil {
+		clockCtx, ok := clockVal.(func() uint64)
+		if !ok {
+			panic(fmt.Errorf("unsupported clock key: %v", clockVal))
+		}
+		clock = clockCtx
+	}
+	if !m.Memory().WriteUint64Le(ctx, resultTimestamp, clock()) {
 		return ErrnoFault
 	}
 	return ErrnoSuccess
@@ -1359,24 +1365,10 @@ const (
 	fdStderr = 2
 )
 
-// compile-time check to ensure defaultSys implements experimental.Sys.
-var _ experimental.Sys = &defaultSys{}
-
-type defaultSys struct{}
-
-func (d *defaultSys) TimeNowUnixNano() uint64 {
+func timeNowUnixNano() uint64 {
 	return uint64(time.Now().UnixNano())
 }
 
-func newSnapshotPreview1(ctx context.Context) *snapshotPreview1 {
-	if ctx != nil { // Test to see if internal code are using an experimental feature.
-		if sys := ctx.Value(experimental.SysKey{}); sys != nil {
-			return &snapshotPreview1{sys: sys.(experimental.Sys)}
-		}
-	}
-	return &snapshotPreview1{sys: &defaultSys{}}
-}
-
 func sysCtx(m api.Module) *wasm.SysContext {
 	if internal, ok := m.(*wasm.CallContext); !ok {
 		panic(fmt.Errorf("unsupported wasm.Module implementation: %v", m))
@@ -1385,13 +1377,13 @@ func sysCtx(m api.Module) *wasm.SysContext {
 	}
 }
 
-func sysFSCtx(ctx context.Context, m api.Module) (*wasm.SysContext, *internalfs.Context) {
+func sysFSCtx(ctx context.Context, m api.Module) (*wasm.SysContext, *sys.FSContext) {
 	if internal, ok := m.(*wasm.CallContext); !ok {
 		panic(fmt.Errorf("unsupported wasm.Module implementation: %v", m))
 	} else {
 		// Override Context when it is passed via context
-		if fsValue := ctx.Value(internalfs.Key{}); fsValue != nil {
-			fsCtx, ok := fsValue.(*internalfs.Context)
+		if fsValue := ctx.Value(sys.FSKey{}); fsValue != nil {
+			fsCtx, ok := fsValue.(*sys.FSContext)
 			if !ok {
 				panic(fmt.Errorf("unsupported fs key: %v", fsValue))
 			}
@@ -1401,7 +1393,7 @@ func sysFSCtx(ctx context.Context, m api.Module) (*wasm.SysContext, *internalfs.
 	}
 }
 
-func openFileEntry(rootFS fs.FS, pathName string) (*internalfs.FileEntry, Errno) {
+func openFileEntry(rootFS fs.FS, pathName string) (*sys.FileEntry, Errno) {
 	f, err := rootFS.Open(pathName)
 	if err != nil {
 		switch {
@@ -1417,7 +1409,7 @@ func openFileEntry(rootFS fs.FS, pathName string) (*internalfs.FileEntry, Errno)
 	// TODO: verify if oflags is a directory and fail with wasi.ErrnoNotdir if not
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-oflags-flagsu16
 
-	return &internalfs.FileEntry{Path: pathName, FS: rootFS, File: f}, ErrnoSuccess
+	return &sys.FileEntry{Path: pathName, FS: rootFS, File: f}, ErrnoSuccess
 }
 
 func writeOffsetsAndNullTerminatedValues(ctx context.Context, mem api.Memory, values []string, offsets, bytes uint32) Errno {


Test patch:
diff --git a/assemblyscript/assemblyscript_test.go b/assemblyscript/assemblyscript_test.go
index d27fb330..58abdbe9 100644
--- a/assemblyscript/assemblyscript_test.go
+++ b/assemblyscript/assemblyscript_test.go
@@ -13,6 +13,7 @@ import (
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/sys"
 )
 
 var abortWasm = []byte(`(module
@@ -81,6 +82,7 @@ func TestAbort(t *testing.T) {
 
 			_, err = mod.ExportedFunction("abort").Call(testCtx, uint64(messageOff), uint64(filenameOff), 1, 2)
 			require.Error(t, err)
+			require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode())
 
 			require.Equal(t, tc.expected, out.String())
 		})
diff --git a/config_test.go b/config_test.go
index 8d4600b6..280917b6 100644
--- a/config_test.go
+++ b/config_test.go
@@ -9,7 +9,7 @@ import (
 	"testing/fstest"
 
 	"github.com/tetratelabs/wazero/api"
-	"github.com/tetratelabs/wazero/internal/fs"
+	"github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -439,7 +439,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
 				nil,            // stdout
 				nil,            // stderr
 				nil,            // randSource
-				map[uint32]*fs.FileEntry{ // openedFiles
+				map[uint32]*sys.FileEntry{ // openedFiles
 					3: {Path: "/", FS: testFS},
 					4: {Path: ".", FS: testFS},
 				},
@@ -456,7 +456,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
 				nil,            // stdout
 				nil,            // stderr
 				nil,            // randSource
-				map[uint32]*fs.FileEntry{ // openedFiles
+				map[uint32]*sys.FileEntry{ // openedFiles
 					3: {Path: "/", FS: testFS2},
 					4: {Path: ".", FS: testFS2},
 				},
@@ -473,7 +473,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
 				nil,            // stdout
 				nil,            // stderr
 				nil,            // randSource
-				map[uint32]*fs.FileEntry{ // openedFiles
+				map[uint32]*sys.FileEntry{ // openedFiles
 					3: {Path: ".", FS: testFS},
 				},
 			),
@@ -489,7 +489,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
 				nil,            // stdout
 				nil,            // stderr
 				nil,            // randSource
-				map[uint32]*fs.FileEntry{ // openedFiles
+				map[uint32]*sys.FileEntry{ // openedFiles
 					3: {Path: "/", FS: testFS},
 					4: {Path: ".", FS: testFS2},
 				},
@@ -506,7 +506,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
 				nil,            // stdout
 				nil,            // stderr
 				nil,            // randSource
-				map[uint32]*fs.FileEntry{ // openedFiles
+				map[uint32]*sys.FileEntry{ // openedFiles
 					3: {Path: ".", FS: testFS},
 					4: {Path: "/", FS: testFS2},
 				},
@@ -577,7 +577,7 @@ func TestModuleConfig_toSysContext_Errors(t *testing.T) {
 }
 
 // requireSysContext ensures wasm.NewSysContext doesn't return an error, which makes it usable in test matrices.
-func requireSysContext(t *testing.T, max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randsource io.Reader, openedFiles map[uint32]*fs.FileEntry) *wasm.SysContext {
+func requireSysContext(t *testing.T, max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randsource io.Reader, openedFiles map[uint32]*sys.FileEntry) *wasm.SysContext {
 	sys, err := wasm.NewSysContext(max, args, environ, stdin, stdout, stderr, randsource, openedFiles)
 	require.NoError(t, err)
 	return sys
diff --git a/experimental/fs_example_test.go b/experimental/fs_example_test.go
new file mode 100644
index 00000000..a1c4c334
--- /dev/null
+++ b/experimental/fs_example_test.go
@@ -0,0 +1,68 @@
+package experimental_test
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/tetratelabs/wazero"
+	"github.com/tetratelabs/wazero/experimental"
+	"github.com/tetratelabs/wazero/wasi"
+)
+
+// This is a basic example of overriding the file system via WithFS. The main goal is to show how it is configured.
+func Example_withFS() {
+	ctx := context.Background()
+
+	r := wazero.NewRuntime()
+	defer r.Close(ctx) // This closes everything this Runtime created.
+
+	if _, err := wasi.InstantiateSnapshotPreview1(ctx, r); err != nil {
+		log.Panicln(err)
+	}
+
+	// Instantiate a module that only re-exports a WASI function that uses the filesystem.
+	mod, err := r.InstantiateModuleFromCode(ctx, []byte(`(module
+  (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)))
+
+  (memory 1 1) ;; memory is required for WASI
+
+  (export "fd_prestat_dir_name" (func 0))
+)`))
+	if err != nil {
+		log.Panicln(err)
+	}
+
+	// Setup the filesystem overlay, noting that it can fail if the directory is invalid and must be closed.
+	ctx, closer, err := experimental.WithFS(ctx, os.DirFS("."))
+	if err != nil {
+		log.Panicln(err)
+	}
+	defer closer.Close(ctx)
+
+	fdPrestatDirName := mod.ExportedFunction("fd_prestat_dir_name")
+	fd := 3         // after stderr
+	pathLen := 1    // length we expect the path to be.
+	pathOffset := 0 // where to write pathLen bytes.
+
+	// By default, there are no pre-opened directories. If the configuration was wrong, this call would fail.
+	results, err := fdPrestatDirName.Call(ctx, uint64(fd), uint64(pathOffset), uint64(pathLen))
+	if err != nil {
+		log.Panicln(err)
+	}
+	if results[0] != 0 {
+		log.Panicf("received errno %d\n", results[0])
+	}
+
+	// Try to read the path!
+	if path, ok := mod.Memory().Read(ctx, uint32(pathOffset), uint32(pathLen)); !ok {
+		log.Panicf("Memory.Read(%d,%d) out of range of memory size %d", pathOffset, pathLen, mod.Memory().Size(ctx))
+	} else {
+		fmt.Println(string(path))
+	}
+
+	// Output:
+	// /
+}
diff --git a/experimental/fs_test.go b/experimental/fs_test.go
index abdef6ae..4c5bbacd 100644
--- a/experimental/fs_test.go
+++ b/experimental/fs_test.go
@@ -8,7 +8,7 @@ import (
 	"testing/fstest"
 
 	"github.com/tetratelabs/wazero/experimental"
-	"github.com/tetratelabs/wazero/internal/fs"
+	"github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
@@ -24,9 +24,9 @@ func TestWithFS(t *testing.T) {
 	}
 	defer closer.Close(ctx)
 
-	v := ctx.Value(fs.Key{})
+	v := ctx.Value(sys.FSKey{})
 	require.NotNil(t, v)
-	fsCtx, ok := v.(*fs.Context)
+	fsCtx, ok := v.(*sys.FSContext)
 	require.True(t, ok)
 
 	entry, ok := fsCtx.OpenedFile(3)
diff --git a/experimental/sys_test.go b/experimental/sys_test.go
deleted file mode 100644
index 9a2d8abd..00000000
--- a/experimental/sys_test.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package experimental_test
-
-import (
-	"context"
-	"encoding/hex"
-	"fmt"
-	"log"
-	"math/rand"
-	"os"
-
-	"github.com/tetratelabs/wazero"
-	"github.com/tetratelabs/wazero/experimental"
-	"github.com/tetratelabs/wazero/wasi"
-)
-
-type fakeSys struct{}
-
-const (
-	epochNanos = uint64(1640995200000000000) // midnight UTC 2022-01-01
-	seed       = int64(42)                   // fixed seed value
-)
-
-func (d fakeSys) TimeNowUnixNano() uint64 {
-	return epochNanos
-}
-
-// This is a very basic integration of sys config. The main goal is to show how it is configured.
-func Example_sys() {
-	// Set context to one that has experimental sys config
-	ctx := context.WithValue(context.Background(), experimental.SysKey{}, &fakeSys{})
-
-	r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
-	defer r.Close(ctx) // This closes everything this Runtime created.
-
-	if _, err := wasi.InstantiateSnapshotPreview1(ctx, r); err != nil {
-		log.Panicln(err)
-	}
-
-	code, err := r.CompileModule(ctx, []byte(`(module
-  (import "wasi_snapshot_preview1" "random_get"
-    (func $wasi.random_get (param $buf i32) (param $buf_len i32) (result (;errno;) i32)))
-  (func i32.const 0 i32.const 4 call 0 drop) ;; write 4 bytes of random data
-  (memory 1 1)
-  (start 1) ;; call the second function
-)`), wazero.NewCompileConfig())
-	if err != nil {
-		log.Panicln(err)
-	}
-
-	randSource := rand.New(rand.NewSource(seed))
-
-	mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout).WithRandSource(randSource))
-	if err != nil {
-		log.Panicln(err)
-	}
-
-	// Try to read 4 bytes of random data.
-	if bytes, ok := mod.Memory().Read(ctx, 0, 4); !ok {
-		log.Panicf("Memory.Read(0, 4) out of range of memory size %d", mod.Memory().Size(ctx))
-	} else {
-		fmt.Println(hex.EncodeToString(bytes))
-	}
-
-	// Output:
-	// 538c7f96
-}
diff --git a/experimental/time_example_test.go b/experimental/time_example_test.go
new file mode 100644
index 00000000..5802a33f
--- /dev/null
+++ b/experimental/time_example_test.go
@@ -0,0 +1,59 @@
+package experimental_test
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"time"
+
+	"github.com/tetratelabs/wazero"
+	"github.com/tetratelabs/wazero/experimental"
+	"github.com/tetratelabs/wazero/wasi"
+)
+
+const epochNanos = uint64(1640995200000000000) // midnight UTC 2022-01-01
+
+// This is a basic example of overriding the clock via WithTimeNowUnixNano. The main goal is to show how it is configured.
+func Example_withTimeNowUnixNano() {
+	ctx := context.Background()
+
+	r := wazero.NewRuntime()
+	defer r.Close(ctx) // This closes everything this Runtime created.
+
+	if _, err := wasi.InstantiateSnapshotPreview1(ctx, r); err != nil {
+		log.Panicln(err)
+	}
+
+	// Instantiate a module that only re-exports a WASI function that uses the clock.
+	mod, err := r.InstantiateModuleFromCode(ctx, []byte(`(module
+  (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)))
+
+  (memory 1 1) ;; memory is required for WASI
+
+  (export "clock_time_get" (func 0))
+)`))
+	if err != nil {
+		log.Panicln(err)
+	}
+
+	// Call clock_time_get in context of an experimental clock function
+	ctx = experimental.WithTimeNowUnixNano(ctx, func() uint64 { return epochNanos })
+	results, err := mod.ExportedFunction("clock_time_get").Call(ctx, 0, 0, 0)
+	if err != nil {
+		log.Panicln(err)
+	}
+	if results[0] != 0 {
+		log.Panicf("received errno %d\n", results[0])
+	}
+
+	// Try to read the time WASI wrote to memory at offset zero.
+	if nanos, ok := mod.Memory().ReadUint64Le(ctx, 0); !ok {
+		log.Panicf("Memory.ReadUint64Le(0) out of range of memory size %d", mod.Memory().Size(ctx))
+	} else {
+		fmt.Println(time.UnixMicro(int64(nanos / 1000)).UTC())
+	}
+
+	// Output:
+	// 2022-01-01 00:00:00 +0000 UTC
+}
diff --git a/internal/fs/fs_test.go b/internal/sys/fs_test.go
similarity index 95%
rename from internal/fs/fs_test.go
rename to internal/sys/fs_test.go
index 4f367dd4..50b535ab 100644
--- a/internal/fs/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -1,4 +1,4 @@
-package fs
+package sys
 
 import (
 	"context"
@@ -18,7 +18,7 @@ func TestContext_Close(t *testing.T) {
 	pathName := "test"
 	file, _ := createWriteableFile(t, tempDir, pathName, make([]byte, 0))
 
-	fsc := NewContext(map[uint32]*FileEntry{
+	fsc := NewFSContext(map[uint32]*FileEntry{
 		3: {Path: "."},
 		4: {Path: path.Join(".", pathName), File: file},
 	})
diff --git a/internal/wasm/call_context_test.go b/internal/wasm/call_context_test.go
index 71305924..17b2c725 100644
--- a/internal/wasm/call_context_test.go
+++ b/internal/wasm/call_context_test.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/tetratelabs/wazero/internal/fs"
+	"github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
@@ -149,7 +149,7 @@ func TestCallContext_Close(t *testing.T) {
 			nil, // stdout
 			nil, // stderr
 			nil, // randSource
-			map[uint32]*fs.FileEntry{ // openedFiles
+			map[uint32]*sys.FileEntry{ // openedFiles
 				3: {Path: "."},
 			},
 		)
diff --git a/wasi/wasi_bench_test.go b/wasi/wasi_bench_test.go
index 4fb34975..4879df45 100644
--- a/wasi/wasi_bench_test.go
+++ b/wasi/wasi_bench_test.go
@@ -25,7 +25,7 @@ func Test_EnvironGet(t *testing.T) {
 	require.NoError(t, err)
 
 	m := newModule(make([]byte, 20), sys)
-	environGet := newSnapshotPreview1(testCtx).EnvironGet
+	environGet := (&snapshotPreview1{}).EnvironGet
 
 	require.Equal(t, ErrnoSuccess, environGet(testCtx, m, 11, 1))
 	require.Equal(t, m.Memory(), testMem)
@@ -47,7 +47,7 @@ func Benchmark_EnvironGet(b *testing.B) {
 		0,
 	}, sys)
 
-	environGet := newSnapshotPreview1(testCtx).EnvironGet
+	environGet := (&snapshotPreview1{}).EnvironGet
 	b.Run("EnvironGet", func(b *testing.B) {
 		for i := 0; i < b.N; i++ {
 			if environGet(testCtx, m, 0, 4) != ErrnoSuccess {
diff --git a/wasi/wasi_test.go b/wasi/wasi_test.go
index a43ddc84..23c1543f 100644
--- a/wasi/wasi_test.go
+++ b/wasi/wasi_test.go
@@ -19,28 +19,19 @@ import (
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/experimental"
-	fs2 "github.com/tetratelabs/wazero/internal/fs"
+	internalsys "github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/sys"
 )
 
-// compile-time check to ensure fakeSys implements experimental.Sys.
-var _ experimental.Sys = fakeSys{}
-
-type fakeSys struct{}
-
 const (
 	epochNanos = uint64(1640995200000000000) // midnight UTC 2022-01-01
 	seed       = int64(42)                   // fixed seed value
 )
 
-func (d fakeSys) TimeNowUnixNano() uint64 {
-	return epochNanos
-}
-
-// testCtx ensures fakeSys is used for WASI functions.
-var testCtx = context.WithValue(context.Background(), experimental.SysKey{}, fakeSys{})
+// testCtx ensures the fake clock is used for WASI functions.
+var testCtx = experimental.WithTimeNowUnixNano(context.Background(), func() uint64 { return epochNanos })
 
 func TestSnapshotPreview1_ArgsGet(t *testing.T) {
 	sysCtx, err := newSysContext([]string{"a", "bc"}, nil, nil)
@@ -564,7 +555,7 @@ func TestSnapshotPreview1_FdClose(t *testing.T) {
 		entry2, errno := openFileEntry(testFs, path2)
 		require.Zero(t, errno, ErrnoName(errno))
 
-		sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{
+		sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
 			fdToClose: entry1,
 			fdToKeep:  entry2,
 		})
@@ -750,7 +741,7 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) {
 	fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
 
 	pathName := "/tmp"
-	sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{fd: {Path: pathName}})
+	sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{fd: {Path: pathName}})
 	require.NoError(t, err)
 
 	a, mod, fn := instantiateModule(testCtx, t, functionFdPrestatGet, importFdPrestatGet, sysCtx)
@@ -795,7 +786,7 @@ func TestSnapshotPreview1_FdPrestatGet_Errors(t *testing.T) {
 	fd := uint32(3)           // fd 3 will be opened for the "/tmp" directory after 0, 1, and 2, that are stdin/out/err
 	validAddress := uint32(0) // Arbitrary valid address as arguments to fd_prestat_get. We chose 0 here.
 
-	sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{fd: {Path: "/tmp"}})
+	sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{fd: {Path: "/tmp"}})
 	require.NoError(t, err)
 
 	a, mod, _ := instantiateModule(testCtx, t, functionFdPrestatGet, importFdPrestatGet, sysCtx)
@@ -837,7 +828,7 @@ func TestSnapshotPreview1_FdPrestatGet_Errors(t *testing.T) {
 func TestSnapshotPreview1_FdPrestatDirName(t *testing.T) {
 	fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
 
-	sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{fd: {Path: "/tmp"}})
+	sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{fd: {Path: "/tmp"}})
 	require.NoError(t, err)
 
 	a, mod, fn := instantiateModule(testCtx, t, functionFdPrestatDirName, importFdPrestatDirName, sysCtx)
@@ -878,7 +869,7 @@ func TestSnapshotPreview1_FdPrestatDirName(t *testing.T) {
 
 func TestSnapshotPreview1_FdPrestatDirName_Errors(t *testing.T) {
 	fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
-	sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{fd: {Path: "/tmp"}})
+	sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{fd: {Path: "/tmp"}})
 	require.NoError(t, err)
 
 	a, mod, _ := instantiateModule(testCtx, t, functionFdPrestatDirName, importFdPrestatDirName, sysCtx)
@@ -1000,7 +991,7 @@ func TestSnapshotPreview1_FdRead(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			// Create a fresh file to read the contents from
 			file, testFS := createFile(t, "test_path", []byte("wazero"))
-			sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{
+			sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
 				fd: {Path: "test_path", FS: testFS, File: file},
 			})
 			require.NoError(t, err)
@@ -1027,7 +1018,7 @@ func TestSnapshotPreview1_FdRead_Errors(t *testing.T) {
 	validFD := uint32(3)                                 // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
 	file, testFS := createFile(t, "test_path", []byte{}) // file with empty contents
 
-	sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{
+	sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
 		validFD: {Path: "test_path", FS: testFS, File: file},
 	})
 	require.NoError(t, err)
@@ -1156,7 +1147,7 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) {
 	resultNewoffset := uint32(1)                                 // arbitrary offset in `ctx.Memory` for the new offset value
 	file, testFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents
 
-	sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{
+	sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
 		fd: {Path: "test_path", FS: testFS, File: file},
 	})
 	require.NoError(t, err)
@@ -1264,7 +1255,7 @@ func TestSnapshotPreview1_FdSeek_Errors(t *testing.T) {
 	validFD := uint32(3)                                         // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
 	file, testFS := createFile(t, "test_path", []byte("wazero")) // arbitrary valid file with non-empty contents
 
-	sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{
+	sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
 		validFD: {Path: "test_path", FS: testFS, File: file},
 	})
 	require.NoError(t, err)
@@ -1395,7 +1386,7 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) {
 			// Create a fresh file to write the contents to
 			pathName := "test_path"
 			file, testFS := createWriteableFile(t, tmpDir, pathName, []byte{})
-			sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{
+			sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
 				fd: {Path: pathName, FS: testFS, File: file},
 			})
 			require.NoError(t, err)
@@ -1430,7 +1421,7 @@ func TestSnapshotPreview1_FdWrite_Errors(t *testing.T) {
 	pathName := "test_path"
 	file, testFS := createWriteableFile(t, tmpDir, pathName, []byte{})
 
-	sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{
+	sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
 		validFD: {Path: pathName, FS: testFS, File: file},
 	})
 	require.NoError(t, err)
@@ -1611,7 +1602,7 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) {
 		}
 
 		testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}}
-		sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{
+		sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
 			workdirFD: {Path: ".", FS: testFS},
 		})
 		require.NoError(t, err)
@@ -1687,7 +1678,7 @@ func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) {
 	pathName := "wazero"
 	testFS := fstest.MapFS{pathName: &fstest.MapFile{Mode: os.ModeDir}}
 
-	sysCtx, err := newSysContext(nil, nil, map[uint32]*fs2.FileEntry{
+	sysCtx, err := newSysContext(nil, nil, map[uint32]*internalsys.FileEntry{
 		validFD: {Path: ".", FS: testFS},
 	})
 	require.NoError(t, err)
@@ -2010,15 +2001,6 @@ func TestSnapshotPreview1_RandomGet_Errors(t *testing.T) {
 	}
 }
 
-// compile-time check to ensure fakeSysErr implements experimental.Sys.
-var _ experimental.Sys = &fakeSysErr{}
-
-type fakeSysErr struct{}
-
-func (d *fakeSysErr) TimeNowUnixNano() uint64 {
-	panic(errors.New("TimeNowUnixNano error"))
-}
-
 func TestSnapshotPreview1_RandomGet_SourceError(t *testing.T) {
 	tests := []struct {
 		name       string
@@ -2037,7 +2019,9 @@ func TestSnapshotPreview1_RandomGet_SourceError(t *testing.T) {
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
-			var errCtx = context.WithValue(context.Background(), experimental.SysKey{}, &fakeSysErr{})
+			var errCtx = experimental.WithTimeNowUnixNano(context.Background(), func() uint64 {
+				panic(errors.New("TimeNowUnixNano error"))
+			})
 
 			sysCtx, err := wasm.NewSysContext(math.MaxUint32, nil, nil, new(bytes.Buffer), nil, nil, tc.randSource, nil)
 			require.NoError(t, err)
@@ -2144,7 +2128,7 @@ func instantiateModule(ctx context.Context, t *testing.T, wasifunction, wasiimpo
 	return a, mod, fn
 }
 
-func newSysContext(args, environ []string, openedFiles map[uint32]*fs2.FileEntry) (sysCtx *wasm.SysContext, err error) {
+func newSysContext(args, environ []string, openedFiles map[uint32]*internalsys.FileEntry) (sysCtx *wasm.SysContext, err error) {
 	return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, nil, openedFiles)
 }