Repository: tetratelabs/wazero
Pull request #1240 (2023-03-14 23:50:58)

gojs: refactors out gojs.Config type
In order to support more configuration, we should stop using context as
it is getting gnarly.

codefromthecrypt: @mathetake I repurposed this to sort out config so that it is cleaner to add inheriting the current user (in a different change)
mathetake: lg

Patch:
diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go
index 03577f5b..4291da65 100644
--- a/cmd/wazero/wazero.go
+++ b/cmd/wazero/wazero.go
@@ -226,13 +226,6 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
 		exit(1)
 	}
 
-	if workdirInherit {
-		if ctx, err = gojs.WithOSWorkDir(ctx); err != nil {
-			fmt.Fprintf(stdErr, "cannot read current working directory: %v\n", err)
-			exit(1)
-		}
-	}
-
 	rt := wazero.NewRuntimeWithConfig(ctx, rtc)
 	defer rt.Close(ctx)
 
@@ -274,7 +267,13 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
 		}
 	case modeGo:
 		gojs.MustInstantiate(ctx, rt)
-		err = gojs.Run(ctx, rt, code, conf)
+
+		config := gojs.NewConfig(conf)
+		if workdirInherit {
+			config = config.WithOSWorkdir()
+		}
+
+		err = gojs.Run(ctx, rt, code, config)
 	case modeDefault:
 		_, err = rt.InstantiateModule(ctx, code, conf)
 	}
diff --git a/experimental/gojs/example/stars.go b/experimental/gojs/example/stars.go
index 0acd4974..dca7a6ff 100644
--- a/experimental/gojs/example/stars.go
+++ b/experimental/gojs/example/stars.go
@@ -44,7 +44,7 @@ func main() {
 	log.Printf("gojs.MustInstantiate took %dms", goJsInstantiate)
 
 	// Combine the above into our baseline config, overriding defaults.
-	config := wazero.NewModuleConfig().
+	moduleConfig := wazero.NewModuleConfig().
 		// By default, I/O streams are discarded, so you won't see output.
 		WithStdout(os.Stdout).WithStderr(os.Stderr)
 
@@ -63,7 +63,7 @@ func main() {
 	log.Printf("CompileModule took %dms with %dKB cache", compilationTime, dirSize(compilationCacheDir)/1024)
 
 	// Instead of making real HTTP calls, return fake data.
-	ctx = gojs.WithRoundTripper(ctx, &fakeGitHub{})
+	config := gojs.NewConfig(moduleConfig).WithRoundTripper(&fakeGitHub{})
 
 	// Execute the "run" function, which corresponds to "main" in stars/main.go.
 	start = time.Now()
diff --git a/experimental/gojs/gojs.go b/experimental/gojs/gojs.go
index 358a78a5..c5353dcf 100644
--- a/experimental/gojs/gojs.go
+++ b/experimental/gojs/gojs.go
@@ -16,14 +16,12 @@ package gojs
 import (
 	"context"
 	"net/http"
-	"os"
-	"path/filepath"
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
 	. "github.com/tetratelabs/wazero/internal/gojs"
+	internalconfig "github.com/tetratelabs/wazero/internal/gojs/config"
 	. "github.com/tetratelabs/wazero/internal/gojs/run"
-	"github.com/tetratelabs/wazero/internal/platform"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -97,42 +95,60 @@ func (e *functionExporter) ExportFunctions(builder wazero.HostModuleBuilder) {
 	hfExporter.ExportHostFunc(Debug)
 }
 
-// WithRoundTripper sets the http.RoundTripper used to Run Wasm.
-//
-// For example, if the code compiled via `GOARCH=wasm GOOS=js` uses
-// http.RoundTripper, you can avoid failures by assigning an implementation
-// like so:
-//
-//	ctx = gojs.WithRoundTripper(ctx, http.DefaultTransport)
-//	err = gojs.Run(ctx, r, compiled, config)
-func WithRoundTripper(ctx context.Context, rt http.RoundTripper) context.Context {
-	return context.WithValue(ctx, RoundTripperKey{}, rt)
+// Config extends wazero.ModuleConfig with GOOS=js specific extensions.
+// Use NewConfig to create an instance.
+type Config interface {
+	// WithOSWorkdir sets the initial working directory used to Run Wasm to
+	// the value of os.Getwd instead of the default of root "/".
+	//
+	// Here's an example that overrides this to the current directory:
+	//
+	//	err = gojs.Run(ctx, r, compiled, gojs.NewConfig(moduleConfig).
+	//			WithOSWorkdir())
+	//
+	// Note: To use this feature requires mounting the real root directory via
+	// wazero.FSConfig `WithDirMount`. On windows, this root must be the same drive
+	// as the value of os.Getwd. For example, it would be an error to mount `C:\`
+	// as the guest path "", while the current directory is inside `D:\`.
+	WithOSWorkdir() Config
+
+	// WithRoundTripper sets the http.RoundTripper used to Run Wasm.
+	//
+	// For example, if the code compiled via `GOARCH=wasm GOOS=js` uses
+	// http.RoundTripper, you can avoid failures by assigning an implementation
+	// like so:
+	//
+	//	err = gojs.Run(ctx, r, compiled, gojs.NewConfig(moduleConfig).
+	//			WithRoundTripper(ctx, http.DefaultTransport))
+	WithRoundTripper(http.RoundTripper) Config
 }
 
-// WithOSWorkDir sets the initial working directory used to Run Wasm to
-// the value of os.Getwd instead of the default of root "/".
-//
-// Here's an example that overrides this to "/usr/local/go/src/os".
-//
-//	ctx = gojs.WithOSWorkDir(ctx)
-//	err = gojs.Run(ctx, r, compiled, config)
-//
-// Note: To use this feature requires mounting the real root directory via
-// wazero.FSConfig `WithDirMount`. On windows, this root must be the same drive
-// as the value of os.Getwd. For example, it would be an error to mount `C:\`
-// as the guest path "", while the current directory is inside `D:\`.
-func WithOSWorkDir(ctx context.Context) (context.Context, error) {
-	workdir, err := os.Getwd()
-	if err != nil {
-		return nil, err
-	}
+// NewConfig returns a Config that can be used for configuring module instantiation.
+func NewConfig(moduleConfig wazero.ModuleConfig) Config {
+	return &cfg{moduleConfig: moduleConfig, internal: internalconfig.NewConfig()}
+}
+
+type cfg struct {
+	moduleConfig wazero.ModuleConfig
+	internal     *internalconfig.Config
+}
 
-	// Ensure if used on windows, the input path is translated to a POSIX one.
-	workdir = platform.ToPosixPath(workdir)
-	// Strip the volume of the path, for example C:\
-	workdir = workdir[len(filepath.VolumeName(workdir)):]
+func (c *cfg) clone() *cfg {
+	return &cfg{moduleConfig: c.moduleConfig, internal: c.internal.Clone()}
+}
+
+// WithOSWorkdir implements Config.WithOSWorkdir
+func (c *cfg) WithOSWorkdir() Config {
+	ret := c.clone()
+	ret.internal.OsWorkdir = true
+	return ret
+}
 
-	return context.WithValue(ctx, WorkdirKey{}, workdir), nil
+// WithRoundTripper implements Config.WithRoundTripper
+func (c *cfg) WithRoundTripper(rt http.RoundTripper) Config {
+	ret := c.clone()
+	ret.internal.Rt = rt
+	return ret
 }
 
 // Run instantiates a new module and calls "run" with the given config.
@@ -142,20 +158,21 @@ func WithOSWorkDir(ctx context.Context) (context.Context, error) {
 //   - ctx: context to use when instantiating the module and calling "run".
 //   - r: runtime to instantiate both the host and guest (compiled) module in.
 //   - compiled: guest binary compiled with `GOARCH=wasm GOOS=js`
-//   - config: the configuration such as args, env or filesystem to use.
+//   - config: the Config to use including wazero.ModuleConfig or extensions of
+//     it.
 //
 // # Example
 //
 // After compiling your Wasm binary with wazero.Runtime's `CompileModule`, run
 // it like below:
 //
-//	// Use compilation cache to reduce performance penalty of multiple runs.
-//	ctx = experimental.WithCompilationCacheDirName(ctx, ".build")
+//	// Instantiate host functions needed by gojs
+//	gojs.MustInstantiate(ctx, r)
 //
-//	// Instantiate the host functions used for each call.
-//	gojs.MustInstantiate(r)
+//	// Assign any configuration relevant for your compiled wasm.
+//	config := gojs.NewConfig(wazero.NewConfig())
 //
-//	// Execute the "run" function, which corresponds to "main" in stars/main.go.
+//	// Run your wasm, notably handing any ExitError
 //	err = gojs.Run(ctx, r, compiled, config)
 //	if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
 //		log.Panicln(err)
@@ -165,12 +182,12 @@ func WithOSWorkDir(ctx context.Context) (context.Context, error) {
 //
 // # Notes
 //
-//   - Use wazero.RuntimeConfig `WithWasmCore2` to avoid needing to pick >1.0
-//     features set by `GOWASM` or used internally by Run.
-//   - Wasm generated by `GOARCH=wasm GOOS=js` is very slow to compile.
-//     Use experimental.WithCompilationCacheDirName to improve performance.
+//   - Wasm generated by `GOARCH=wasm GOOS=js` is very slow to compile: Use
+//     wazero.RuntimeConfig with wazero.CompilationCache when re-running the
+//     same binary.
 //   - The guest module is closed after being run.
-func Run(ctx context.Context, r wazero.Runtime, compiled wazero.CompiledModule, config wazero.ModuleConfig) error {
-	_, err := RunAndReturnState(ctx, r, compiled, config)
+func Run(ctx context.Context, r wazero.Runtime, compiled wazero.CompiledModule, moduleConfig Config) error {
+	c := moduleConfig.(*cfg)
+	_, err := RunAndReturnState(ctx, r, compiled, c.moduleConfig, c.internal)
 	return err
 }
diff --git a/internal/gojs/builtin.go b/internal/gojs/builtin.go
index 06fe4bd5..17cfbd37 100644
--- a/internal/gojs/builtin.go
+++ b/internal/gojs/builtin.go
@@ -26,7 +26,7 @@ func newJsGlobal(rt http.RoundTripper) *jsVal {
 			"fs":              jsfs,
 			"Date":            jsDateConstructor,
 		}).
-		addFunction("fetch", httpFetch{})
+		addFunction("fetch", &httpFetch{rt})
 }
 
 var (
diff --git a/internal/gojs/config/config.go b/internal/gojs/config/config.go
new file mode 100644
index 00000000..dab63bfe
--- /dev/null
+++ b/internal/gojs/config/config.go
@@ -0,0 +1,42 @@
+// Package config exists to avoid dependency cycles when keeping most of gojs
+// code internal.
+package config
+
+import (
+	"net/http"
+	"os"
+	"path/filepath"
+
+	"github.com/tetratelabs/wazero/internal/platform"
+)
+
+type Config struct {
+	OsWorkdir bool
+	Rt        http.RoundTripper
+
+	// Workdir is the actual working directory value.
+	Workdir string
+}
+
+func NewConfig() *Config {
+	return &Config{Workdir: "/"}
+}
+
+func (c *Config) Clone() *Config {
+	ret := *c // copy except maps which share a ref
+	return &ret
+}
+
+func (c *Config) Init() error {
+	if c.OsWorkdir {
+		workdir, err := os.Getwd()
+		if err != nil {
+			return err
+		}
+		// Ensure if used on windows, the input path is translated to a POSIX one.
+		workdir = platform.ToPosixPath(workdir)
+		// Strip the volume of the path, for example C:\
+		c.Workdir = workdir[len(filepath.VolumeName(workdir)):]
+	}
+	return nil
+}
diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go
index 35df7bba..bc60ecd1 100644
--- a/internal/gojs/fs.go
+++ b/internal/gojs/fs.go
@@ -539,8 +539,8 @@ type jsfsChown struct{}
 
 func (jsfsChown) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	path := resolvePath(ctx, args[0].(string))
-	uid := goos.ValueToUint32(args[1])
-	gid := goos.ValueToUint32(args[2])
+	uid := goos.ValueToInt32(args[1])
+	gid := goos.ValueToInt32(args[2])
 	callback := args[3].(funcWrapper)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
diff --git a/internal/gojs/goos/goos.go b/internal/gojs/goos/goos.go
index 3f7eb0ac..c56ab0d3 100644
--- a/internal/gojs/goos/goos.go
+++ b/internal/gojs/goos/goos.go
@@ -265,6 +265,15 @@ func ValueToUint32(arg interface{}) uint32 {
 	return uint32(arg.(float64))
 }
 
+func ValueToInt32(arg interface{}) int32 {
+	if arg == RefValueZero || arg == Undefined {
+		return 0
+	} else if u, ok := arg.(int); ok {
+		return int32(u)
+	}
+	return int32(uint32(arg.(float64)))
+}
+
 // GetFunction allows getting a JavaScript property by name.
 type GetFunction interface {
 	Get(ctx context.Context, propertyKey string) interface{}
diff --git a/internal/gojs/http.go b/internal/gojs/http.go
index 11434a76..1026d0d7 100644
--- a/internal/gojs/http.go
+++ b/internal/gojs/http.go
@@ -15,15 +15,6 @@ import (
 // headersConstructor = Get("Headers").New() // http.Roundtrip && "fetch"
 var headersConstructor = newJsVal(goos.RefHttpHeadersConstructor, "Headers")
 
-type RoundTripperKey struct{}
-
-func getRoundTripper(ctx context.Context) http.RoundTripper {
-	if rt, ok := ctx.Value(RoundTripperKey{}).(http.RoundTripper); ok {
-		return rt
-	}
-	return nil
-}
-
 // httpFetch implements jsFn for http.RoundTripper
 //
 // Reference in roundtrip_js.go init
@@ -33,10 +24,10 @@ func getRoundTripper(ctx context.Context) http.RoundTripper {
 // In http.Transport RoundTrip, this returns a promise
 //
 //	fetchPromise := js.Global().Call("fetch", req.URL.String(), opt)
-type httpFetch struct{}
+type httpFetch struct{ rt http.RoundTripper }
 
-func (httpFetch) invoke(ctx context.Context, _ api.Module, args ...interface{}) (interface{}, error) {
-	rt := getRoundTripper(ctx)
+func (h *httpFetch) invoke(ctx context.Context, _ api.Module, args ...interface{}) (interface{}, error) {
+	rt := h.rt
 	if rt == nil {
 		panic("unexpected to reach here without roundtripper as property is nil checked")
 	}
diff --git a/internal/gojs/logging/logging.go b/internal/gojs/logging/logging.go
index 9064ca26..744558d2 100644
--- a/internal/gojs/logging/logging.go
+++ b/internal/gojs/logging/logging.go
@@ -301,6 +301,11 @@ func writeVal(w logging.Writer, name string, val interface{}) {
 		return
 	}
 	switch name {
+	case "uid", "gid":
+		w.WriteString(name) //nolint
+		w.WriteByte('=')    //nolint
+		id := int(goos.ValueToInt32(val))
+		w.WriteString(strconv.Itoa(id)) //nolint
 	case "mask", "mode", "oldmask", "perm":
 		w.WriteString(name) //nolint
 		w.WriteByte('=')    //nolint
diff --git a/internal/gojs/run/gojs.go b/internal/gojs/run/gojs.go
index 06143187..1f9745a1 100644
--- a/internal/gojs/run/gojs.go
+++ b/internal/gojs/run/gojs.go
@@ -7,25 +7,35 @@ import (
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/internal/gojs"
+	"github.com/tetratelabs/wazero/internal/gojs/config"
 )
 
-func RunAndReturnState(ctx context.Context, r wazero.Runtime, compiled wazero.CompiledModule, config wazero.ModuleConfig) (*gojs.State, error) {
-	// Instantiate the module compiled by go, noting it has no init function.
+func RunAndReturnState(
+	ctx context.Context,
+	r wazero.Runtime,
+	compiled wazero.CompiledModule,
+	moduleConfig wazero.ModuleConfig,
+	config *config.Config,
+) (*gojs.State, error) {
+	if err := config.Init(); err != nil {
+		return nil, err
+	}
 
-	mod, err := r.InstantiateModule(ctx, compiled, config)
+	// Instantiate the module compiled by go, noting it has no init function.
+	mod, err := r.InstantiateModule(ctx, compiled, moduleConfig)
 	if err != nil {
 		return nil, err
 	}
 	defer mod.Close(ctx)
 
-	// Extract the args and env from the module config and write it to memory.
+	// Extract the args and env from the module Config and write it to memory.
 	argc, argv, err := gojs.WriteArgsAndEnviron(mod)
 	if err != nil {
 		return nil, err
 	}
 
 	// Create host-side state for JavaScript values and events.
-	s := gojs.NewState(ctx)
+	s := gojs.NewState(config)
 	ctx = context.WithValue(ctx, gojs.StateKey{}, s)
 
 	// Invoke the run function.
diff --git a/internal/gojs/state.go b/internal/gojs/state.go
index f089586f..e0380a53 100644
--- a/internal/gojs/state.go
+++ b/internal/gojs/state.go
@@ -6,24 +6,16 @@ import (
 	"math"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/gojs/config"
 	"github.com/tetratelabs/wazero/internal/gojs/goos"
 	"github.com/tetratelabs/wazero/internal/gojs/values"
 )
 
-type WorkdirKey struct{}
-
-func getWorkdir(ctx context.Context) string {
-	if wd, ok := ctx.Value(WorkdirKey{}).(string); ok {
-		return wd
-	}
-	return "/"
-}
-
-func NewState(ctx context.Context) *State {
+func NewState(config *config.Config) *State {
 	return &State{
 		values:                 values.NewValues(),
-		valueGlobal:            newJsGlobal(getRoundTripper(ctx)),
-		cwd:                    getWorkdir(ctx),
+		valueGlobal:            newJsGlobal(config.Rt),
+		cwd:                    config.Workdir,
 		umask:                  0o0022,
 		_nextCallbackTimeoutID: 1,
 		_scheduledTimeouts:     map[uint32]chan bool{},


Test patch:
diff --git a/experimental/gojs/example/stars_test.go b/experimental/gojs/example/stars_test.go
index e84e8c13..26fcf683 100644
--- a/experimental/gojs/example/stars_test.go
+++ b/experimental/gojs/example/stars_test.go
@@ -80,8 +80,8 @@ func Benchmark_main(b *testing.B) {
 	gojs.MustInstantiate(ctx, r)
 
 	// Instead of making real HTTP calls, return fake data.
-	ctx = gojs.WithRoundTripper(ctx, &fakeGitHub{})
-	cfg := wazero.NewModuleConfig()
+	cfg := gojs.NewConfig(wazero.NewModuleConfig()).
+		WithRoundTripper(&fakeGitHub{})
 
 	b.Run("gojs.Run", func(b *testing.B) {
 		b.ReportAllocs()
diff --git a/experimental/gojs/gojs_test.go b/experimental/gojs/gojs_test.go
deleted file mode 100644
index bb1ef995..00000000
--- a/experimental/gojs/gojs_test.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package gojs
-
-import (
-	"context"
-	"strings"
-	"testing"
-
-	"github.com/tetratelabs/wazero/internal/gojs"
-	"github.com/tetratelabs/wazero/internal/testing/require"
-)
-
-func TestWithOSWorkdir(t *testing.T) {
-	t.Parallel()
-
-	ctx, err := WithOSWorkDir(context.Background())
-	require.NoError(t, err)
-	actual := ctx.Value(gojs.WorkdirKey{}).(string)
-
-	// Check c:\ or d:\ aren't retained.
-	require.Equal(t, -1, strings.IndexByte(actual, '\\'))
-	require.Equal(t, -1, strings.IndexByte(actual, ':'))
-}
diff --git a/internal/gojs/argsenv_test.go b/internal/gojs/argsenv_test.go
index 63c28391..615837c1 100644
--- a/internal/gojs/argsenv_test.go
+++ b/internal/gojs/argsenv_test.go
@@ -4,14 +4,16 @@ import (
 	"testing"
 
 	"github.com/tetratelabs/wazero"
+	"github.com/tetratelabs/wazero/internal/gojs/config"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
 func Test_argsAndEnv(t *testing.T) {
 	t.Parallel()
 
-	stdout, stderr, err := compileAndRun(testCtx, "argsenv", wazero.NewModuleConfig().
-		WithEnv("c", "d").WithEnv("a", "b"))
+	stdout, stderr, err := compileAndRun(testCtx, "argsenv", func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) {
+		return moduleConfig.WithEnv("c", "d").WithEnv("a", "b"), config.NewConfig()
+	})
 
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
 	require.Zero(t, stderr)
diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go
index dbe5e81d..c7f46b95 100644
--- a/internal/gojs/compiler_test.go
+++ b/internal/gojs/compiler_test.go
@@ -20,13 +20,20 @@ import (
 	"github.com/tetratelabs/wazero/experimental/gojs"
 	"github.com/tetratelabs/wazero/internal/fstest"
 	internalgojs "github.com/tetratelabs/wazero/internal/gojs"
+	"github.com/tetratelabs/wazero/internal/gojs/config"
 	"github.com/tetratelabs/wazero/internal/gojs/run"
 	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary"
 )
 
-func compileAndRun(ctx context.Context, arg string, config wazero.ModuleConfig) (stdout, stderr string, err error) {
+type newConfig func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config)
+
+func defaultConfig(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) {
+	return moduleConfig, config.NewConfig()
+}
+
+func compileAndRun(ctx context.Context, arg string, config newConfig) (stdout, stderr string, err error) {
 	rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().
 		// https://github.com/tetratelabs/wazero/issues/992
 		WithMemoryCapacityFromMax(true).
@@ -34,7 +41,7 @@ func compileAndRun(ctx context.Context, arg string, config wazero.ModuleConfig)
 	return compileAndRunWithRuntime(ctx, rt, arg, config) // use global runtime
 }
 
-func compileAndRunWithRuntime(ctx context.Context, r wazero.Runtime, arg string, config wazero.ModuleConfig) (stdout, stderr string, err error) {
+func compileAndRunWithRuntime(ctx context.Context, r wazero.Runtime, arg string, config newConfig) (stdout, stderr string, err error) {
 	var stdoutBuf, stderrBuf bytes.Buffer
 
 	builder := r.NewHostModuleBuilder("go")
@@ -49,13 +56,15 @@ func compileAndRunWithRuntime(ctx context.Context, r wazero.Runtime, arg string,
 		log.Panicln(err)
 	}
 
-	var s *internalgojs.State
-	s, err = run.RunAndReturnState(ctx, r, compiled, config.
+	mc, c := config(wazero.NewModuleConfig().
 		WithStdout(&stdoutBuf).
 		WithStderr(&stderrBuf).
 		WithArgs("test", arg))
+
+	var s *internalgojs.State
+	s, err = run.RunAndReturnState(ctx, r, compiled, mc, c)
 	if err == nil {
-		if !reflect.DeepEqual(s, internalgojs.NewState(ctx)) {
+		if !reflect.DeepEqual(s, internalgojs.NewState(c)) {
 			log.Panicf("unexpected state: %v\n", s)
 		}
 	}
diff --git a/internal/gojs/config/config_test.go b/internal/gojs/config/config_test.go
new file mode 100644
index 00000000..870231b7
--- /dev/null
+++ b/internal/gojs/config/config_test.go
@@ -0,0 +1,22 @@
+package config
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func TestConfig_Init(t *testing.T) {
+	t.Parallel()
+
+	t.Run("OsWorkdir", func(t *testing.T) {
+		c := &Config{OsWorkdir: true}
+		require.NoError(t, c.Init())
+		actual := c.Workdir
+
+		// Check c:\ or d:\ aren't retained.
+		require.Equal(t, -1, strings.IndexByte(actual, '\\'))
+		require.Equal(t, -1, strings.IndexByte(actual, ':'))
+	})
+}
diff --git a/internal/gojs/crypto_test.go b/internal/gojs/crypto_test.go
index 23d9191e..c4c58194 100644
--- a/internal/gojs/crypto_test.go
+++ b/internal/gojs/crypto_test.go
@@ -5,7 +5,6 @@ import (
 	"context"
 	"testing"
 
-	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/experimental/logging"
 	"github.com/tetratelabs/wazero/internal/testing/require"
@@ -18,7 +17,7 @@ func Test_crypto(t *testing.T) {
 	loggingCtx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{},
 		logging.NewHostLoggingListenerFactory(&log, logging.LogScopeRandom))
 
-	stdout, stderr, err := compileAndRun(loggingCtx, "crypto", wazero.NewModuleConfig())
+	stdout, stderr, err := compileAndRun(loggingCtx, "crypto", defaultConfig)
 
 	require.Zero(t, stderr)
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
diff --git a/internal/gojs/fs_test.go b/internal/gojs/fs_test.go
index 19fb2581..b5dae811 100644
--- a/internal/gojs/fs_test.go
+++ b/internal/gojs/fs_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/internal/fstest"
+	"github.com/tetratelabs/wazero/internal/gojs/config"
 	"github.com/tetratelabs/wazero/internal/platform"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
@@ -14,7 +15,9 @@ import (
 func Test_fs(t *testing.T) {
 	t.Parallel()
 
-	stdout, stderr, err := compileAndRun(testCtx, "fs", wazero.NewModuleConfig().WithFS(testFS))
+	stdout, stderr, err := compileAndRun(testCtx, "fs", func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) {
+		return defaultConfig(moduleConfig.WithFS(testFS))
+	})
 
 	require.Zero(t, stderr)
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
@@ -44,7 +47,9 @@ func Test_testfs(t *testing.T) {
 	require.NoError(t, fstest.WriteTestFiles(testfsDir))
 
 	fsConfig := wazero.NewFSConfig().WithDirMount(tmpDir, "/")
-	stdout, stderr, err := compileAndRun(testCtx, "testfs", wazero.NewModuleConfig().WithFSConfig(fsConfig))
+	stdout, stderr, err := compileAndRun(testCtx, "testfs", func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) {
+		return defaultConfig(moduleConfig.WithFSConfig(fsConfig))
+	})
 
 	require.Zero(t, stderr)
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
@@ -59,7 +64,9 @@ func Test_writefs(t *testing.T) {
 	// test expects to write under /tmp
 	require.NoError(t, os.Mkdir(path.Join(tmpDir, "tmp"), 0o700))
 
-	stdout, stderr, err := compileAndRun(testCtx, "writefs", wazero.NewModuleConfig().WithFSConfig(fsConfig))
+	stdout, stderr, err := compileAndRun(testCtx, "writefs", func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) {
+		return defaultConfig(moduleConfig.WithFSConfig(fsConfig))
+	})
 
 	require.Zero(t, stderr)
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
diff --git a/internal/gojs/http_test.go b/internal/gojs/http_test.go
index 86bd2999..c8cc2dbe 100644
--- a/internal/gojs/http_test.go
+++ b/internal/gojs/http_test.go
@@ -8,7 +8,7 @@ import (
 	"testing"
 
 	"github.com/tetratelabs/wazero"
-	"github.com/tetratelabs/wazero/experimental/gojs"
+	"github.com/tetratelabs/wazero/internal/gojs/config"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
@@ -21,7 +21,7 @@ func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
 func Test_http(t *testing.T) {
 	t.Parallel()
 
-	ctx := gojs.WithRoundTripper(testCtx, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
+	rt := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
 		if req.URL.Path == "/error" {
 			return nil, errors.New("error")
 		}
@@ -38,10 +38,13 @@ func Test_http(t *testing.T) {
 			Body:          io.NopCloser(strings.NewReader("abcdef")),
 			ContentLength: 6,
 		}, nil
-	}))
+	})
 
-	stdout, stderr, err := compileAndRun(ctx, "http", wazero.NewModuleConfig().
-		WithEnv("BASE_URL", "http://host"))
+	stdout, stderr, err := compileAndRun(testCtx, "http", func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) {
+		return moduleConfig.WithEnv("BASE_URL", "http://host"), &config.Config{
+			Rt: rt,
+		}
+	})
 
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
 	require.Zero(t, stderr)
diff --git a/internal/gojs/misc_test.go b/internal/gojs/misc_test.go
index ae1bfbf1..e3ca36a6 100644
--- a/internal/gojs/misc_test.go
+++ b/internal/gojs/misc_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/experimental/logging"
+	"github.com/tetratelabs/wazero/internal/gojs/config"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
@@ -20,7 +21,7 @@ func Test_exit(t *testing.T) {
 	loggingCtx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{},
 		logging.NewHostLoggingListenerFactory(&log, logging.LogScopeProc))
 
-	stdout, stderr, err := compileAndRun(loggingCtx, "exit", wazero.NewModuleConfig())
+	stdout, stderr, err := compileAndRun(loggingCtx, "exit", defaultConfig)
 
 	require.EqualError(t, err, `module "" closed with exit_code(255)`)
 	require.Zero(t, stderr)
@@ -33,7 +34,7 @@ func Test_exit(t *testing.T) {
 func Test_goroutine(t *testing.T) {
 	t.Parallel()
 
-	stdout, stderr, err := compileAndRun(testCtx, "goroutine", wazero.NewModuleConfig())
+	stdout, stderr, err := compileAndRun(testCtx, "goroutine", defaultConfig)
 
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
 	require.Zero(t, stderr)
@@ -49,7 +50,7 @@ func Test_mem(t *testing.T) {
 	loggingCtx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{},
 		logging.NewHostLoggingListenerFactory(&log, logging.LogScopeMemory))
 
-	stdout, stderr, err := compileAndRun(loggingCtx, "mem", wazero.NewModuleConfig())
+	stdout, stderr, err := compileAndRun(loggingCtx, "mem", defaultConfig)
 
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
 	require.Zero(t, stderr)
@@ -65,8 +66,9 @@ func Test_stdio(t *testing.T) {
 	t.Parallel()
 
 	input := "stdin\n"
-	stdout, stderr, err := compileAndRun(testCtx, "stdio", wazero.NewModuleConfig().
-		WithStdin(strings.NewReader(input)))
+	stdout, stderr, err := compileAndRun(testCtx, "stdio", func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) {
+		return defaultConfig(moduleConfig.WithStdin(strings.NewReader(input)))
+	})
 
 	require.Equal(t, "stderr 6\n", stderr)
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
@@ -83,8 +85,9 @@ func Test_stdio_large(t *testing.T) {
 
 	size := 2 * 1024 * 1024 // 2MB
 	input := make([]byte, size)
-	stdout, stderr, err := compileAndRun(loggingCtx, "stdio", wazero.NewModuleConfig().
-		WithStdin(bytes.NewReader(input)))
+	stdout, stderr, err := compileAndRun(loggingCtx, "stdio", func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) {
+		return defaultConfig(moduleConfig.WithStdin(bytes.NewReader(input)))
+	})
 
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
 	require.Equal(t, fmt.Sprintf("stderr %d\n", size), stderr)
@@ -102,7 +105,7 @@ func Test_stdio_large(t *testing.T) {
 func Test_gc(t *testing.T) {
 	t.Parallel()
 
-	stdout, stderr, err := compileAndRun(testCtx, "gc", wazero.NewModuleConfig())
+	stdout, stderr, err := compileAndRun(testCtx, "gc", defaultConfig)
 
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
 	require.Equal(t, "", stderr)
diff --git a/internal/gojs/syscall_test.go b/internal/gojs/syscall_test.go
index 19d01738..e2b81df7 100644
--- a/internal/gojs/syscall_test.go
+++ b/internal/gojs/syscall_test.go
@@ -3,14 +3,13 @@ package gojs_test
 import (
 	"testing"
 
-	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
 func Test_syscall(t *testing.T) {
 	t.Parallel()
 
-	stdout, stderr, err := compileAndRun(testCtx, "syscall", wazero.NewModuleConfig())
+	stdout, stderr, err := compileAndRun(testCtx, "syscall", defaultConfig)
 
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
 	require.Zero(t, stderr)
diff --git a/internal/gojs/time_test.go b/internal/gojs/time_test.go
index be7fec58..3e83fbca 100644
--- a/internal/gojs/time_test.go
+++ b/internal/gojs/time_test.go
@@ -5,7 +5,6 @@ import (
 	"context"
 	"testing"
 
-	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/experimental/logging"
 	"github.com/tetratelabs/wazero/internal/testing/require"
@@ -18,7 +17,7 @@ func Test_time(t *testing.T) {
 	loggingCtx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{},
 		logging.NewHostLoggingListenerFactory(&log, logging.LogScopeClock))
 
-	stdout, stderr, err := compileAndRun(loggingCtx, "time", wazero.NewModuleConfig())
+	stdout, stderr, err := compileAndRun(loggingCtx, "time", defaultConfig)
 
 	require.EqualError(t, err, `module "" closed with exit_code(0)`)
 	require.Zero(t, stderr)