Repository: tetratelabs/wazero
Pull request #1301 (2023-03-29 02:11:49)

Stop using pointer of function pointers in sys.Context
This reduces the allocations.


Patch:
diff --git a/config.go b/config.go
index ccec69f8..259b7f50 100644
--- a/config.go
+++ b/config.go
@@ -609,12 +609,12 @@ type moduleConfig struct {
 	stdout             io.Writer
 	stderr             io.Writer
 	randSource         io.Reader
-	walltime           *sys.Walltime
+	walltime           sys.Walltime
 	walltimeResolution sys.ClockResolution
-	nanotime           *sys.Nanotime
+	nanotime           sys.Nanotime
 	nanotimeResolution sys.ClockResolution
-	nanosleep          *sys.Nanosleep
-	osyield            *sys.Osyield
+	nanosleep          sys.Nanosleep
+	osyield            sys.Osyield
 	args               [][]byte
 	// environ is pair-indexed to retain order similar to os.Environ.
 	environ [][]byte
@@ -728,7 +728,7 @@ func (c *moduleConfig) WithStdout(stdout io.Writer) ModuleConfig {
 // WithWalltime implements ModuleConfig.WithWalltime
 func (c *moduleConfig) WithWalltime(walltime sys.Walltime, resolution sys.ClockResolution) ModuleConfig {
 	ret := c.clone()
-	ret.walltime = &walltime
+	ret.walltime = walltime
 	ret.walltimeResolution = resolution
 	return ret
 }
@@ -745,7 +745,7 @@ func (c *moduleConfig) WithSysWalltime() ModuleConfig {
 // WithNanotime implements ModuleConfig.WithNanotime
 func (c *moduleConfig) WithNanotime(nanotime sys.Nanotime, resolution sys.ClockResolution) ModuleConfig {
 	ret := c.clone()
-	ret.nanotime = &nanotime
+	ret.nanotime = nanotime
 	ret.nanotimeResolution = resolution
 	return ret
 }
@@ -758,14 +758,14 @@ func (c *moduleConfig) WithSysNanotime() ModuleConfig {
 // WithNanosleep implements ModuleConfig.WithNanosleep
 func (c *moduleConfig) WithNanosleep(nanosleep sys.Nanosleep) ModuleConfig {
 	ret := *c // copy
-	ret.nanosleep = &nanosleep
+	ret.nanosleep = nanosleep
 	return &ret
 }
 
 // WithOsyield implements ModuleConfig.WithOsyield
 func (c *moduleConfig) WithOsyield(osyield sys.Osyield) ModuleConfig {
 	ret := *c // copy
-	ret.osyield = &osyield
+	ret.osyield = osyield
 	return &ret
 }
 
diff --git a/internal/platform/time.go b/internal/platform/time.go
index 5de23af1..fa9da1ac 100644
--- a/internal/platform/time.go
+++ b/internal/platform/time.go
@@ -15,32 +15,30 @@ const (
 
 // NewFakeWalltime implements sys.Walltime with FakeEpochNanos that increases by 1ms each reading.
 // See /RATIONALE.md
-func NewFakeWalltime() *sys.Walltime {
+func NewFakeWalltime() sys.Walltime {
 	// AddInt64 returns the new value. Adjust so the first reading will be FakeEpochNanos
 	t := FakeEpochNanos - ms
-	var wt sys.Walltime = func() (sec int64, nsec int32) {
+	return func() (sec int64, nsec int32) {
 		wt := atomic.AddInt64(&t, ms)
 		return wt / 1e9, int32(wt % 1e9)
 	}
-	return &wt
 }
 
 // NewFakeNanotime implements sys.Nanotime that increases by 1ms each reading.
 // See /RATIONALE.md
-func NewFakeNanotime() *sys.Nanotime {
+func NewFakeNanotime() sys.Nanotime {
 	// AddInt64 returns the new value. Adjust so the first reading will be zero.
 	t := int64(0) - ms
-	var nt sys.Nanotime = func() int64 {
+	return func() int64 {
 		return atomic.AddInt64(&t, ms)
 	}
-	return &nt
 }
 
 // FakeNanosleep implements sys.Nanosleep by returning without sleeping.
-func FakeNanosleep(int64) {}
+var FakeNanosleep = sys.Nanosleep(func(int64) {})
 
 // FakeOsyield implements sys.Osyield by returning without yielding.
-func FakeOsyield() {}
+var FakeOsyield = sys.Osyield(func() {})
 
 // Walltime implements sys.Walltime with time.Now.
 //
diff --git a/internal/sys/fs.go b/internal/sys/fs.go
index 29ed47b7..7070e15c 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -252,32 +252,32 @@ type FileTable = descriptor.Table[uint32, *FileEntry]
 //
 // If `preopened` is not sysfs.UnimplementedFS, it is inserted into
 // the file descriptor table as FdPreopen.
-func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, rootFS sysfs.FS) (fsc *FSContext, err error) {
-	fsc = &FSContext{rootFS: rootFS}
+func (c *Context) NewFSContext(stdin io.Reader, stdout, stderr io.Writer, rootFS sysfs.FS) (err error) {
+	c.fsc.rootFS = rootFS
 	inReader, err := stdinReader(stdin)
 	if err != nil {
-		return nil, err
+		return err
 	}
-	fsc.openedFiles.Insert(inReader)
+	c.fsc.openedFiles.Insert(inReader)
 	outWriter, err := stdioWriter(stdout, noopStdoutStat)
 	if err != nil {
-		return nil, err
+		return err
 	}
-	fsc.openedFiles.Insert(outWriter)
+	c.fsc.openedFiles.Insert(outWriter)
 	errWriter, err := stdioWriter(stderr, noopStderrStat)
 	if err != nil {
-		return nil, err
+		return err
 	}
-	fsc.openedFiles.Insert(errWriter)
+	c.fsc.openedFiles.Insert(errWriter)
 
 	if _, ok := rootFS.(sysfs.UnimplementedFS); ok {
-		return fsc, nil
+		return nil
 	}
 
 	if comp, ok := rootFS.(*sysfs.CompositeFS); ok {
 		preopens := comp.FS()
 		for i, p := range comp.GuestPaths() {
-			fsc.openedFiles.Insert(&FileEntry{
+			c.fsc.openedFiles.Insert(&FileEntry{
 				FS:        preopens[i],
 				Name:      p,
 				IsPreopen: true,
@@ -285,7 +285,7 @@ func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, rootFS sysfs.FS) (f
 			})
 		}
 	} else {
-		fsc.openedFiles.Insert(&FileEntry{
+		c.fsc.openedFiles.Insert(&FileEntry{
 			FS:        rootFS,
 			Name:      "/",
 			IsPreopen: true,
@@ -293,7 +293,7 @@ func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, rootFS sysfs.FS) (f
 		})
 	}
 
-	return fsc, nil
+	return nil
 }
 
 func stdinReader(r io.Reader) (*FileEntry, error) {
diff --git a/internal/sys/sys.go b/internal/sys/sys.go
index 1c86bce9..a0b5dbd5 100644
--- a/internal/sys/sys.go
+++ b/internal/sys/sys.go
@@ -17,16 +17,14 @@ type Context struct {
 	args, environ         [][]byte
 	argsSize, environSize uint32
 
-	// Note: Using function pointers here keeps them stable for tests.
-
-	walltime           *sys.Walltime
+	walltime           sys.Walltime
 	walltimeResolution sys.ClockResolution
-	nanotime           *sys.Nanotime
+	nanotime           sys.Nanotime
 	nanotimeResolution sys.ClockResolution
-	nanosleep          *sys.Nanosleep
-	osyield            *sys.Osyield
+	nanosleep          sys.Nanosleep
+	osyield            sys.Osyield
 	randSource         io.Reader
-	fsc                *FSContext
+	fsc                FSContext
 }
 
 // Args is like os.Args and defaults to nil.
@@ -65,7 +63,7 @@ func (c *Context) EnvironSize() uint32 {
 
 // Walltime implements platform.Walltime.
 func (c *Context) Walltime() (sec int64, nsec int32) {
-	return (*(c.walltime))()
+	return c.walltime()
 }
 
 // WalltimeNanos returns platform.Walltime as epoch nanoseconds.
@@ -81,7 +79,7 @@ func (c *Context) WalltimeResolution() sys.ClockResolution {
 
 // Nanotime implements sys.Nanotime.
 func (c *Context) Nanotime() int64 {
-	return (*(c.nanotime))()
+	return c.nanotime()
 }
 
 // NanotimeResolution returns resolution of Nanotime.
@@ -91,17 +89,17 @@ func (c *Context) NanotimeResolution() sys.ClockResolution {
 
 // Nanosleep implements sys.Nanosleep.
 func (c *Context) Nanosleep(ns int64) {
-	(*(c.nanosleep))(ns)
+	c.nanosleep(ns)
 }
 
 // Osyield implements sys.Osyield.
 func (c *Context) Osyield() {
-	(*(c.osyield))()
+	c.osyield()
 }
 
 // FS returns the possibly empty (sysfs.UnimplementedFS) file system context.
 func (c *Context) FS() *FSContext {
-	return c.fsc
+	return &c.fsc
 }
 
 // RandSource is a source of random bytes and defaults to a deterministic source.
@@ -120,6 +118,8 @@ func (eofReader) Read([]byte) (int, error) {
 }
 
 // DefaultContext returns Context with no values set except a possible nil fs.FS
+//
+// This is only used for testing.
 func DefaultContext(fs sysfs.FS) *Context {
 	if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, nil, fs); err != nil {
 		panic(fmt.Errorf("BUG: DefaultContext should never error: %w", err))
@@ -128,12 +128,6 @@ func DefaultContext(fs sysfs.FS) *Context {
 	}
 }
 
-var (
-	_                = DefaultContext(nil) // Force panic on bug.
-	ns sys.Nanosleep = platform.FakeNanosleep
-	oy sys.Osyield   = platform.FakeOsyield
-)
-
 // NewContext 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 NewContext(
@@ -142,12 +136,12 @@ func NewContext(
 	stdin io.Reader,
 	stdout, stderr io.Writer,
 	randSource io.Reader,
-	walltime *sys.Walltime,
+	walltime sys.Walltime,
 	walltimeResolution sys.ClockResolution,
-	nanotime *sys.Nanotime,
+	nanotime sys.Nanotime,
 	nanotimeResolution sys.ClockResolution,
-	nanosleep *sys.Nanosleep,
-	osyield *sys.Osyield,
+	nanosleep sys.Nanosleep,
+	osyield sys.Osyield,
 	rootFS sysfs.FS,
 ) (sysCtx *Context, err error) {
 	sysCtx = &Context{args: args, environ: environ}
@@ -191,19 +185,19 @@ func NewContext(
 	if nanosleep != nil {
 		sysCtx.nanosleep = nanosleep
 	} else {
-		sysCtx.nanosleep = &ns
+		sysCtx.nanosleep = platform.FakeNanosleep
 	}
 
 	if osyield != nil {
 		sysCtx.osyield = osyield
 	} else {
-		sysCtx.osyield = &oy
+		sysCtx.osyield = platform.FakeOsyield
 	}
 
 	if rootFS != nil {
-		sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, rootFS)
+		err = sysCtx.NewFSContext(stdin, stdout, stderr, rootFS)
 	} else {
-		sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, sysfs.UnimplementedFS{})
+		err = sysCtx.NewFSContext(stdin, stdout, stderr, sysfs.UnimplementedFS{})
 	}
 
 	return


Test patch:
diff --git a/config_test.go b/config_test.go
index c0824a41..c8f2419e 100644
--- a/config_test.go
+++ b/config_test.go
@@ -1,12 +1,11 @@
 package wazero
 
 import (
+	"bytes"
 	"context"
-	"crypto/rand"
 	_ "embed"
-	"io"
-	"math"
 	"testing"
+	"time"
 
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/fstest"
@@ -163,260 +162,214 @@ func TestModuleConfig(t *testing.T) {
 // TestModuleConfig_toSysContext only tests the cases that change the inputs to
 // sys.NewContext.
 func TestModuleConfig_toSysContext(t *testing.T) {
-	// Always assigns clocks so that pointers are constant.
-	var wt sys.Walltime = func() (int64, int32) {
-		return 0, 0
-	}
-	var nt sys.Nanotime = func() int64 {
-		return 0
-	}
 	base := NewModuleConfig()
-	base.(*moduleConfig).walltime = &wt
-	base.(*moduleConfig).walltimeResolution = 1
-	base.(*moduleConfig).nanotime = &nt
-	base.(*moduleConfig).nanotimeResolution = 1
-
-	testFS := testfs.FS{}
-	testFS2 := testfs.FS{"/": &testfs.File{}}
 
 	tests := []struct {
-		name     string
-		input    ModuleConfig
-		expected *internalsys.Context
+		name  string
+		input func() (mc ModuleConfig, verify func(t *testing.T, sys *internalsys.Context))
 	}{
 		{
-			name:  "empty",
-			input: base,
-			expected: requireSysContext(t,
-				math.MaxUint32, // max
-				nil,            // args
-				nil,            // environ
-				nil,            // stdin
-				nil,            // stdout
-				nil,            // stderr
-				nil,            // randSource
-				&wt, 1,         // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				nil, // fs
-			),
-		},
-		{
-			name:  "WithArgs",
-			input: base.WithArgs("a", "bc"),
-			expected: requireSysContext(t,
-				math.MaxUint32,      // max
-				[]string{"a", "bc"}, // args
-				nil,                 // environ
-				nil,                 // stdin
-				nil,                 // stdout
-				nil,                 // stderr
-				nil,                 // randSource
-				&wt, 1,              // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				nil, // fs
-			),
-		},
-		{
-			name:  "WithArgs empty ok", // Particularly argv[0] can be empty, and we have no rules about others.
-			input: base.WithArgs("", "bc"),
-			expected: requireSysContext(t,
-				math.MaxUint32,     // max
-				[]string{"", "bc"}, // args
-				nil,                // environ
-				nil,                // stdin
-				nil,                // stdout
-				nil,                // stderr
-				nil,                // randSource
-				&wt, 1,             // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				nil, // fs
-			),
-		},
-		{
-			name:  "WithArgs second call overwrites",
-			input: base.WithArgs("a", "bc").WithArgs("bc", "a"),
-			expected: requireSysContext(t,
-				math.MaxUint32,      // max
-				[]string{"bc", "a"}, // args
-				nil,                 // environ
-				nil,                 // stdin
-				nil,                 // stdout
-				nil,                 // stderr
-				nil,                 // randSource
-				&wt, 1,              // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				nil, // fs
-			),
-		},
-		{
-			name:  "WithEnv",
-			input: base.WithEnv("a", "b"),
-			expected: requireSysContext(t,
-				math.MaxUint32,  // max
-				nil,             // args
-				[]string{"a=b"}, // environ
-				nil,             // stdin
-				nil,             // stdout
-				nil,             // stderr
-				nil,             // randSource
-				&wt, 1,          // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				nil, // fs
-			),
-		},
-		{
-			name:  "WithEnv empty value",
-			input: base.WithEnv("a", ""),
-			expected: requireSysContext(t,
-				math.MaxUint32, // max
-				nil,            // args
-				[]string{"a="}, // environ
-				nil,            // stdin
-				nil,            // stdout
-				nil,            // stderr
-				nil,            // randSource
-				&wt, 1,         // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				nil, // fs
-			),
-		},
-		{
-			name:  "WithEnv twice",
-			input: base.WithEnv("a", "b").WithEnv("c", "de"),
-			expected: requireSysContext(t,
-				math.MaxUint32,          // max
-				nil,                     // args
-				[]string{"a=b", "c=de"}, // environ
-				nil,                     // stdin
-				nil,                     // stdout
-				nil,                     // stderr
-				nil,                     // randSource
-				&wt, 1,                  // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				nil, // fs
-			),
-		},
-		{
-			name:  "WithEnv overwrites",
-			input: base.WithEnv("a", "bc").WithEnv("c", "de").WithEnv("a", "de"),
-			expected: requireSysContext(t,
-				math.MaxUint32,           // max
-				nil,                      // args
-				[]string{"a=de", "c=de"}, // environ
-				nil,                      // stdin
-				nil,                      // stdout
-				nil,                      // stderr
-				nil,                      // randSource
-				&wt, 1,                   // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				nil, // fs
-			),
-		},
-		{
-			name:  "WithEnv twice",
-			input: base.WithEnv("a", "b").WithEnv("c", "de"),
-			expected: requireSysContext(t,
-				math.MaxUint32,          // max
-				nil,                     // args
-				[]string{"a=b", "c=de"}, // environ
-				nil,                     // stdin
-				nil,                     // stdout
-				nil,                     // stderr
-				nil,                     // randSource
-				&wt, 1,                  // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				nil, // fs
-			),
-		},
-		{
-			name:  "WithFS",
-			input: base.WithFS(testFS),
-			expected: requireSysContext(t,
-				math.MaxUint32, // max
-				nil,            // args
-				nil,            // environ
-				nil,            // stdin
-				nil,            // stdout
-				nil,            // stderr
-				nil,            // randSource
-				&wt, 1,         // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				sysfs.Adapt(testFS),
-			),
-		},
-		{
-			name:  "WithFS overwrites",
-			input: base.WithFS(testFS).WithFS(testFS2),
-			expected: requireSysContext(t,
-				math.MaxUint32, // max
-				nil,            // args
-				nil,            // environ
-				nil,            // stdin
-				nil,            // stdout
-				nil,            // stderr
-				nil,            // randSource
-				&wt, 1,         // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil,                  // nanosleep
-				nil,                  // osyield
-				sysfs.Adapt(testFS2), // fs
-			),
-		},
-		{
-			name:  "WithFS nil",
-			input: base.WithFS(nil),
-			expected: requireSysContext(t,
-				math.MaxUint32, // max
-				nil,            // args
-				nil,            // environ
-				nil,            // stdin
-				nil,            // stdout
-				nil,            // stderr
-				nil,            // randSource
-				&wt, 1,         // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				nil, // fs
-			),
-		},
-		{
-			name:  "WithRandSource",
-			input: base.WithRandSource(rand.Reader),
-			expected: requireSysContext(t,
-				math.MaxUint32, // max
-				nil,            // args
-				nil,            // environ
-				nil,            // stdin
-				nil,            // stdout
-				nil,            // stderr
-				rand.Reader,    // randSource
-				&wt, 1,         // walltime, walltimeResolution
-				&nt, 1, // nanotime, nanotimeResolution
-				nil, // nanosleep
-				nil, // osyield
-				nil, // fs
-			),
+			name: "empty",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				return base, func(t *testing.T, sys *internalsys.Context) { require.NotNil(t, sys) }
+			},
+		},
+		{
+			name: "WithNanotime",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithNanotime(func() int64 { return 1234567 }, 54321)
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					require.Equal(t, 1234567, int(sys.Nanotime()))
+					require.Equal(t, 54321, int(sys.NanotimeResolution()))
+				}
+			},
+		},
+		{
+			name: "WithSysNanotime",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithSysNanotime()
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					require.Equal(t, int(1), int(sys.NanotimeResolution()))
+				}
+			},
+		},
+		{
+			name: "WithWalltime",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithWalltime(func() (sec int64, nsec int32) { return 5, 10 }, 54321)
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					actualSec, actualNano := sys.Walltime()
+					require.Equal(t, 5, int(actualSec))
+					require.Equal(t, 10, int(actualNano))
+					require.Equal(t, 54321, int(sys.WalltimeResolution()))
+				}
+			},
+		},
+		{
+			name: "WithSysWalltime",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithSysWalltime()
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					require.Equal(t, int(time.Microsecond.Nanoseconds()), int(sys.WalltimeResolution()))
+				}
+			},
+		},
+		{
+			name: "WithArgs empty",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithArgs()
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					args := sys.Args()
+					require.Equal(t, 0, len(args))
+				}
+			},
+		},
+		{
+			name: "WithArgs",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithArgs("a", "bc")
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					args := sys.Args()
+					require.Equal(t, 2, len(args))
+					require.Equal(t, "a", string(args[0]))
+					require.Equal(t, "bc", string(args[1]))
+				}
+			},
+		},
+		{
+			name: "WithArgs empty ok", // Particularly argv[0] can be empty, and we have no rules about others.
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithArgs("", "bc")
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					args := sys.Args()
+					require.Equal(t, 2, len(args))
+					require.Equal(t, "", string(args[0]))
+					require.Equal(t, "bc", string(args[1]))
+				}
+			},
+		},
+		{
+			name: "WithArgs second call overwrites",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithArgs("a", "bc").WithArgs("bc", "a")
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					args := sys.Args()
+					require.Equal(t, 2, len(args))
+					require.Equal(t, "bc", string(args[0]))
+					require.Equal(t, "a", string(args[1]))
+				}
+			},
+		},
+		{
+			name: "WithEnv",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithEnv("a", "b")
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					envs := sys.Environ()
+					require.Equal(t, 1, len(envs))
+					require.Equal(t, "a=b", string(envs[0]))
+				}
+			},
+		},
+		{
+			name: "WithEnv empty value",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithEnv("a", "")
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					envs := sys.Environ()
+					require.Equal(t, 1, len(envs))
+					require.Equal(t, "a=", string(envs[0]))
+				}
+			},
+		},
+		{
+			name: "WithEnv twice",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithEnv("a", "b").WithEnv("c", "de")
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					envs := sys.Environ()
+					require.Equal(t, 2, len(envs))
+					require.Equal(t, "a=b", string(envs[0]))
+					require.Equal(t, "c=de", string(envs[1]))
+				}
+			},
+		},
+		{
+			name: "WithEnv overwrites",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithEnv("a", "bc").WithEnv("c", "de").WithEnv("a", "ff")
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					envs := sys.Environ()
+					require.Equal(t, 2, len(envs))
+					require.Equal(t, "a=ff", string(envs[0]))
+					require.Equal(t, "c=de", string(envs[1]))
+				}
+			},
+		},
+		{
+			name: "WithEnv twice",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithEnv("a", "b").WithEnv("c", "de")
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					envs := sys.Environ()
+					require.Equal(t, 2, len(envs))
+					require.Equal(t, "a=b", string(envs[0]))
+					require.Equal(t, "c=de", string(envs[1]))
+				}
+			},
+		},
+		{
+			name: "WithFS",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				testFS := &testfs.FS{}
+				config := base.WithFS(testFS)
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					rootfs := sys.FS().RootFS()
+					require.Equal(t, sysfs.Adapt(testFS), rootfs)
+				}
+			},
+		},
+		{
+			name: "WithFS overwrites",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				testFS, testFS2 := &testfs.FS{}, &testfs.FS{}
+				config := base.WithFS(testFS).WithFS(testFS2)
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					rootfs := sys.FS().RootFS()
+					require.Equal(t, sysfs.Adapt(testFS2), rootfs)
+				}
+			},
+		},
+		{
+			name: "WithFS nil",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithFS(nil)
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					rootfs := sys.FS().RootFS()
+					require.Equal(t, sysfs.Adapt(nil), rootfs)
+				}
+			},
+		},
+		{
+			name: "WithRandSource",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				r := bytes.NewReader([]byte{1, 2, 3, 4})
+				config := base.WithRandSource(r)
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					actual := sys.RandSource()
+					require.Equal(t, r, actual)
+				}
+			},
+		},
+		{
+			name: "WithRandSource nil",
+			input: func() (ModuleConfig, func(t *testing.T, sys *internalsys.Context)) {
+				config := base.WithRandSource(nil)
+				return config, func(t *testing.T, sys *internalsys.Context) {
+					actual := sys.RandSource()
+					require.Equal(t, platform.NewFakeRandSource(), actual)
+				}
+			},
 		},
 	}
 
@@ -424,9 +377,10 @@ func TestModuleConfig_toSysContext(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			sysCtx, err := tc.input.(*moduleConfig).toSysContext()
+			config, verify := tc.input()
+			actual, err := config.(*moduleConfig).toSysContext()
 			require.NoError(t, err)
-			require.Equal(t, tc.expected, sysCtx)
+			verify(t, actual)
 		})
 	}
 }
@@ -756,34 +710,3 @@ func TestNewRuntimeConfig(t *testing.T) {
 		require.Equal(t, engineKindInterpreter, c.engineKind)
 	}
 }
-
-// requireSysContext ensures wasm.NewContext 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,
-	walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
-	nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
-	nanosleep *sys.Nanosleep,
-	osyield *sys.Osyield,
-	fs sysfs.FS,
-) *internalsys.Context {
-	sysCtx, err := internalsys.NewContext(
-		max,
-		toByteSlices(args),
-		toByteSlices(environ),
-		stdin,
-		stdout,
-		stderr,
-		randSource,
-		walltime, walltimeResolution,
-		nanotime, nanotimeResolution,
-		nanosleep, osyield,
-		fs,
-	)
-	require.NoError(t, err)
-	return sysCtx
-}
diff --git a/internal/platform/time_test.go b/internal/platform/time_test.go
index 8c30fdea..8454a714 100644
--- a/internal/platform/time_test.go
+++ b/internal/platform/time_test.go
@@ -12,22 +12,22 @@ func Test_NewFakeWalltime(t *testing.T) {
 	wt := NewFakeWalltime()
 
 	// Base should be the same as FakeEpochNanos
-	sec, nsec := (*wt)()
+	sec, nsec := wt()
 	ft := time.UnixMicro(FakeEpochNanos / time.Microsecond.Nanoseconds()).UTC()
 	require.Equal(t, ft, time.Unix(sec, int64(nsec)).UTC())
 
 	// next reading should increase by 1ms
-	sec, nsec = (*wt)()
+	sec, nsec = wt()
 	require.Equal(t, ft.Add(time.Millisecond), time.Unix(sec, int64(nsec)).UTC())
 }
 
 func Test_NewFakeNanotime(t *testing.T) {
 	nt := NewFakeNanotime()
 
-	require.Equal(t, int64(0), (*nt)())
+	require.Equal(t, int64(0), nt())
 
 	// next reading should increase by 1ms
-	require.Equal(t, int64(time.Millisecond), (*nt)())
+	require.Equal(t, int64(time.Millisecond), nt())
 }
 
 func Test_Walltime(t *testing.T) {
diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go
index 26c58ff8..b7ec3514 100644
--- a/internal/sys/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -66,8 +66,10 @@ func TestNewFSContext(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(b *testing.T) {
-			fsc, err := NewFSContext(nil, nil, nil, tc.fs)
+			c := Context{}
+			err := c.NewFSContext(nil, nil, nil, tc.fs)
 			require.NoError(t, err)
+			fsc := c.fsc
 			defer fsc.Close(testCtx)
 
 			preopenedDir, _ := fsc.openedFiles.Lookup(FdPreopen)
@@ -102,8 +104,10 @@ func TestFSContext_CloseFile(t *testing.T) {
 	require.NoError(t, err)
 	testFS := sysfs.Adapt(embedFS)
 
-	fsc, err := NewFSContext(nil, nil, nil, testFS)
+	c := Context{}
+	err = c.NewFSContext(nil, nil, nil, testFS)
 	require.NoError(t, err)
+	fsc := c.fsc
 	defer fsc.Close(testCtx)
 
 	fdToClose, errno := fsc.OpenFile(testFS, "empty.txt", os.O_RDONLY, 0)
@@ -132,7 +136,10 @@ func TestFSContext_CloseFile(t *testing.T) {
 }
 
 func TestUnimplementedFSContext(t *testing.T) {
-	testFS, err := NewFSContext(nil, nil, nil, sysfs.UnimplementedFS{})
+	c := Context{}
+	err := c.NewFSContext(nil, nil, nil, sysfs.UnimplementedFS{})
+	require.NoError(t, err)
+	testFS := &c.fsc
 	require.NoError(t, err)
 
 	expected := &FSContext{rootFS: sysfs.UnimplementedFS{}}
@@ -159,8 +166,10 @@ func TestCompositeFSContext(t *testing.T) {
 	rootFS, err := sysfs.NewRootFS([]sysfs.FS{testFS2, testFS1}, []string{"/tmp", "/"})
 	require.NoError(t, err)
 
-	testFS, err := NewFSContext(nil, nil, nil, rootFS)
+	c := Context{}
+	err = c.NewFSContext(nil, nil, nil, rootFS)
 	require.NoError(t, err)
+	testFS := &c.fsc
 
 	// Ensure the pre-opens have exactly the name specified, and are in order.
 	preopen3, ok := testFS.openedFiles.Lookup(3)
@@ -182,8 +191,10 @@ func TestCompositeFSContext(t *testing.T) {
 func TestContext_Close(t *testing.T) {
 	testFS := sysfs.Adapt(testfs.FS{"foo": &testfs.File{}})
 
-	fsc, err := NewFSContext(nil, nil, nil, testFS)
+	c := Context{}
+	err := c.NewFSContext(nil, nil, nil, testFS)
 	require.NoError(t, err)
+	fsc := c.fsc
 
 	// Verify base case
 	require.Equal(t, 1+FdPreopen, uint32(fsc.openedFiles.Len()))
@@ -207,8 +218,10 @@ func TestContext_Close_Error(t *testing.T) {
 
 	testFS := sysfs.Adapt(testfs.FS{"foo": file})
 
-	fsc, err := NewFSContext(nil, nil, nil, testFS)
+	c := Context{}
+	err := c.NewFSContext(nil, nil, nil, testFS)
 	require.NoError(t, err)
+	fsc := c.fsc
 
 	// open another file
 	_, errno := fsc.OpenFile(testFS, "foo", os.O_RDONLY, 0)
@@ -228,7 +241,11 @@ func TestFSContext_ReOpenDir(t *testing.T) {
 	errno := dirFs.Mkdir(dirName, 0o700)
 	require.Zero(t, errno)
 
-	fsc, err := NewFSContext(nil, nil, nil, dirFs)
+	c := Context{}
+	err := c.NewFSContext(nil, nil, nil, dirFs)
+	require.NoError(t, err)
+	fsc := c.fsc
+
 	require.NoError(t, err)
 	defer func() {
 		require.NoError(t, fsc.Close(context.Background()))
@@ -275,45 +292,48 @@ func TestFSContext_Renumber(t *testing.T) {
 	errno := dirFs.Mkdir(dirName, 0o700)
 	require.Zero(t, errno)
 
-	c, err := NewFSContext(nil, nil, nil, dirFs)
+	c := Context{}
+	err := c.NewFSContext(nil, nil, nil, dirFs)
 	require.NoError(t, err)
+	fsc := c.fsc
+
 	defer func() {
-		require.NoError(t, c.Close(context.Background()))
+		require.NoError(t, fsc.Close(context.Background()))
 	}()
 
 	for _, toFd := range []uint32{10, 100, 100} {
-		fromFd, errno := c.OpenFile(dirFs, dirName, os.O_RDONLY, 0)
+		fromFd, errno := fsc.OpenFile(dirFs, dirName, os.O_RDONLY, 0)
 		require.Zero(t, errno)
 
-		prevDirFile, ok := c.LookupFile(fromFd)
+		prevDirFile, ok := fsc.LookupFile(fromFd)
 		require.True(t, ok)
 
-		require.Zero(t, c.Renumber(fromFd, toFd))
+		require.Zero(t, fsc.Renumber(fromFd, toFd))
 
-		renumberedDirFile, ok := c.LookupFile(toFd)
+		renumberedDirFile, ok := fsc.LookupFile(toFd)
 		require.True(t, ok)
 
 		require.Equal(t, prevDirFile, renumberedDirFile)
 
 		// Previous file descriptor shouldn't be used.
-		_, ok = c.LookupFile(fromFd)
+		_, ok = fsc.LookupFile(fromFd)
 		require.False(t, ok)
 	}
 
 	t.Run("errors", func(t *testing.T) {
 		// Sanity check for 3 being preopen.
-		preopen, ok := c.LookupFile(3)
+		preopen, ok := fsc.LookupFile(3)
 		require.True(t, ok)
 		require.True(t, preopen.IsPreopen)
 
 		// From is preopen.
-		require.Equal(t, syscall.ENOTSUP, c.Renumber(3, 100))
+		require.Equal(t, syscall.ENOTSUP, fsc.Renumber(3, 100))
 
 		// From does not exist.
-		require.Equal(t, syscall.EBADF, c.Renumber(12345, 3))
+		require.Equal(t, syscall.EBADF, fsc.Renumber(12345, 3))
 
 		// Both are preopen.
-		require.Equal(t, syscall.ENOTSUP, c.Renumber(3, 3))
+		require.Equal(t, syscall.ENOTSUP, fsc.Renumber(3, 3))
 	})
 }
 
@@ -324,36 +344,41 @@ func TestFSContext_ChangeOpenFlag(t *testing.T) {
 	const fileName = "dir"
 	require.NoError(t, os.WriteFile(path.Join(tmpDir, fileName), []byte("0123456789"), 0o600))
 
-	c, errno := NewFSContext(nil, nil, nil, dirFs)
-	require.NoError(t, errno)
+	c := Context{}
+	err := c.NewFSContext(nil, nil, nil, dirFs)
+	require.NoError(t, err)
+	fsc := c.fsc
+
 	defer func() {
-		require.NoError(t, c.Close(context.Background()))
+		require.NoError(t, fsc.Close(context.Background()))
 	}()
 
 	// Without APPEND.
-	fd, errno := c.OpenFile(dirFs, fileName, os.O_RDWR, 0o600)
+	fd, errno := fsc.OpenFile(dirFs, fileName, os.O_RDWR, 0o600)
 	require.Zero(t, errno)
 
-	f0, ok := c.openedFiles.Lookup(fd)
+	f0, ok := fsc.openedFiles.Lookup(fd)
 	require.True(t, ok)
 	require.Equal(t, f0.openFlag&syscall.O_APPEND, 0)
 
 	// Set the APPEND flag.
-	require.Zero(t, c.ChangeOpenFlag(fd, syscall.O_APPEND))
-	f1, ok := c.openedFiles.Lookup(fd)
+	require.Zero(t, fsc.ChangeOpenFlag(fd, syscall.O_APPEND))
+	f1, ok := fsc.openedFiles.Lookup(fd)
 	require.True(t, ok)
 	require.Equal(t, f1.openFlag&syscall.O_APPEND, syscall.O_APPEND)
 
 	// Remove the APPEND flag.
-	require.Zero(t, c.ChangeOpenFlag(fd, 0))
-	f2, ok := c.openedFiles.Lookup(fd)
+	require.Zero(t, fsc.ChangeOpenFlag(fd, 0))
+	f2, ok := fsc.openedFiles.Lookup(fd)
 	require.True(t, ok)
 	require.Equal(t, f2.openFlag&syscall.O_APPEND, 0)
 }
 
 func TestWriterForFile(t *testing.T) {
-	testFS, err := NewFSContext(nil, nil, nil, sysfs.UnimplementedFS{})
+	c := Context{}
+	err := c.NewFSContext(nil, nil, nil, sysfs.UnimplementedFS{})
 	require.NoError(t, err)
+	testFS := &c.fsc
 
 	require.Nil(t, WriterForFile(testFS, FdStdin))
 	require.Equal(t, noopStdout.File, WriterForFile(testFS, FdStdout))
diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go
index b8c4903e..d4755943 100644
--- a/internal/sys/sys_test.go
+++ b/internal/sys/sys_test.go
@@ -15,15 +15,6 @@ import (
 	"github.com/tetratelabs/wazero/sys"
 )
 
-func TestContext_FS(t *testing.T) {
-	sysCtx := DefaultContext(nil)
-
-	fsc, err := NewFSContext(nil, nil, nil, sysfs.UnimplementedFS{})
-	require.NoError(t, err)
-
-	require.Equal(t, fsc, sysCtx.FS())
-}
-
 func TestContext_WalltimeNanos(t *testing.T) {
 	sysCtx := DefaultContext(nil)
 
@@ -60,11 +51,9 @@ func TestDefaultSysContext(t *testing.T) {
 	require.Equal(t, sys.ClockResolution(1_000), sysCtx.WalltimeResolution())
 	require.Zero(t, sysCtx.Nanotime()) // See above on functions.
 	require.Equal(t, sys.ClockResolution(1), sysCtx.NanotimeResolution())
-	require.Equal(t, &ns, sysCtx.nanosleep)
+	require.Equal(t, platform.FakeNanosleep, sysCtx.nanosleep)
 	require.Equal(t, platform.NewFakeRandSource(), sysCtx.RandSource())
 
-	expectedFS, _ := NewFSContext(nil, nil, nil, testFS)
-
 	expectedOpenedFiles := FileTable{}
 	expectedOpenedFiles.Insert(noopStdin)
 	expectedOpenedFiles.Insert(noopStdout)
@@ -75,9 +64,7 @@ func TestDefaultSysContext(t *testing.T) {
 		FS:        testFS,
 		File:      &lazyDir{fs: testFS},
 	})
-
-	require.Equal(t, expectedOpenedFiles, expectedFS.openedFiles)
-	require.Equal(t, expectedFS, sysCtx.FS())
+	require.Equal(t, expectedOpenedFiles, sysCtx.FS().openedFiles)
 }
 
 func TestFileEntry_cachedStat(t *testing.T) {
@@ -104,7 +91,9 @@ func TestFileEntry_cachedStat(t *testing.T) {
 		tc := tc
 
 		t.Run(tc.name, func(t *testing.T) {
-			fsc, _ := NewFSContext(nil, nil, nil, tc.fs)
+			c := Context{}
+			_ = c.NewFSContext(nil, nil, nil, tc.fs)
+			fsc := c.fsc
 			defer fsc.Close(testCtx)
 
 			f, ok := fsc.LookupFile(FdPreopen)
@@ -258,7 +247,7 @@ func TestNewContext_Environ(t *testing.T) {
 func TestNewContext_Walltime(t *testing.T) {
 	tests := []struct {
 		name        string
-		time        *sys.Walltime
+		time        sys.Walltime
 		resolution  sys.ClockResolution
 		expectedErr string
 	}{
@@ -307,7 +296,7 @@ func TestNewContext_Walltime(t *testing.T) {
 func TestNewContext_Nanotime(t *testing.T) {
 	tests := []struct {
 		name        string
-		time        *sys.Nanotime
+		time        sys.Nanotime
 		resolution  sys.ClockResolution
 		expectedErr string
 	}{
@@ -396,12 +385,12 @@ func TestNewContext_Nanosleep(t *testing.T) {
 		nil,    // randSource
 		nil, 0, // Nanosleep, NanosleepResolution
 		nil, 0, // Nanosleep, NanosleepResolution
-		&aNs, // nanosleep
-		nil,  // osyield
-		nil,  // rootFS
+		aNs, // nanosleep
+		nil, // osyield
+		nil, // rootFS
 	)
 	require.Nil(t, err)
-	require.Equal(t, &aNs, sysCtx.nanosleep)
+	require.Equal(t, aNs, sysCtx.nanosleep)
 }
 
 func TestNewContext_Osyield(t *testing.T) {
@@ -417,9 +406,9 @@ func TestNewContext_Osyield(t *testing.T) {
 		nil, 0, // Nanosleep, NanosleepResolution
 		nil, 0, // Nanosleep, NanosleepResolution
 		nil, // nanosleep
-		&oy, // osyield
+		oy,  // osyield
 		nil, // rootFS
 	)
 	require.Nil(t, err)
-	require.Equal(t, &oy, sysCtx.osyield)
+	require.Equal(t, oy, sysCtx.osyield)
 }