Repository: tetratelabs/wazero
Pull request #280 (2022-02-23 07:43:46)

Consolidates API to config input types
This makes instantiating host or wasm-defined modules similar, by using
XXXConfig types. Doing so also allows configuration engines to seed
properties.

Since decoded modules are only usable during instantiation, this pushes
decoding inside those functions. By doing so, the API is easier to use
as it has less choices and less errors to catch. Detection is done
internally by peeking at the magic number.

See https://github.com/tetratelabs/wazero/issues/279


Patch:
diff --git a/README.md b/README.md
index 2c3d0254..d99d2343 100644
--- a/README.md
+++ b/README.md
@@ -17,11 +17,8 @@ func main() {
 	// * Ex. (func (export "fac") (param i64) (result i64) ...
 	source, _ := os.ReadFile("./tests/engine/testdata/fac.wasm")
 
-	// Decode the binary as WebAssembly module.
-	mod, _ := wazero.DecodeModuleBinary(source)
-
 	// Instantiate the module with a Wasm Interpreter, to return its exported functions
-	exports, _ := wazero.InstantiateModule(wazero.NewStore(), mod)
+	exports, _ := wazero.InstantiateModule(wazero.NewStore(), &wazero.ModuleConfig{Source: source})
 
 	// Discover 7! is 5040
 	fmt.Println(exports.Function("fac")(context.Background(), 7))
diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go
index 6cb8faa0..c07b61a9 100644
--- a/internal/wasm/binary/decoder.go
+++ b/internal/wasm/binary/decoder.go
@@ -16,7 +16,7 @@ func DecodeModule(binary []byte) (*wasm.Module, error) {
 
 	// Magic number.
 	buf := make([]byte, 4)
-	if _, err := io.ReadFull(r, buf); err != nil || !bytes.Equal(buf, magic) {
+	if _, err := io.ReadFull(r, buf); err != nil || !bytes.Equal(buf, Magic) {
 		return nil, ErrInvalidMagicNumber
 	}
 
diff --git a/internal/wasm/binary/encoder.go b/internal/wasm/binary/encoder.go
index 0a232eaf..6b24ccc7 100644
--- a/internal/wasm/binary/encoder.go
+++ b/internal/wasm/binary/encoder.go
@@ -10,7 +10,7 @@ var sizePrefixedName = []byte{4, 'n', 'a', 'm', 'e'}
 // Note: If saving to a file, the conventional extension is wasm
 // See https://www.w3.org/TR/wasm-core-1/#binary-format%E2%91%A0
 func EncodeModule(m *wasm.Module) (bytes []byte) {
-	bytes = append(magic, version...)
+	bytes = append(Magic, version...)
 	if m.SectionElementCount(wasm.SectionIDType) > 0 {
 		bytes = append(bytes, encodeTypeSection(m.TypeSection)...)
 	}
diff --git a/internal/wasm/binary/header.go b/internal/wasm/binary/header.go
index e290c446..9abffe4a 100644
--- a/internal/wasm/binary/header.go
+++ b/internal/wasm/binary/header.go
@@ -1,8 +1,8 @@
 package binary
 
-// magic is the 4 byte preamble (literally "\0asm") of the binary format
+// Magic is the 4 byte preamble (literally "\0asm") of the binary format
 // See https://www.w3.org/TR/wasm-core-1/#binary-magic
-var magic = []byte{0x00, 0x61, 0x73, 0x6D}
+var Magic = []byte{0x00, 0x61, 0x73, 0x6D}
 
 // version is format version and doesn't change between known specification versions
 // See https://www.w3.org/TR/wasm-core-1/#binary-version
diff --git a/store.go b/store.go
deleted file mode 100644
index d75819f9..00000000
--- a/store.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package wazero
-
-import (
-	"context"
-	"fmt"
-
-	internalwasm "github.com/tetratelabs/wazero/internal/wasm"
-	"github.com/tetratelabs/wazero/internal/wasm/interpreter"
-	"github.com/tetratelabs/wazero/internal/wasm/jit"
-	"github.com/tetratelabs/wazero/wasm"
-)
-
-type Engine struct {
-	e internalwasm.Engine
-}
-
-func NewEngineInterpreter() *Engine {
-	return &Engine{e: interpreter.NewEngine()}
-}
-
-func NewEngineJIT() *Engine { // TODO: compiler?
-	return &Engine{e: jit.NewEngine()}
-}
-
-// StoreConfig allows customization of a Store via NewStoreWithConfig
-type StoreConfig struct {
-	// Context is the default context used to initialize the module. Defaults to context.Background.
-	//
-	// Notes:
-	// * If the Module defines a start function, this is used to invoke it.
-	// * This is the outer-most ancestor of wasm.ModuleContext Context() during wasm.HostFunction invocations.
-	// * This is the default context of wasm.Function when callers pass nil.
-	//
-	// See https://www.w3.org/TR/wasm-core-1/#start-function%E2%91%A0
-	Context context.Context
-	// Engine defaults to NewEngineInterpreter
-	Engine *Engine
-}
-
-func NewStore() wasm.Store {
-	return internalwasm.NewStore(context.Background(), interpreter.NewEngine())
-}
-
-// NewStoreWithConfig returns a store with the given configuration.
-func NewStoreWithConfig(config *StoreConfig) wasm.Store {
-	ctx := config.Context
-	if ctx == nil {
-		ctx = context.Background()
-	}
-	engine := config.Engine
-	if engine == nil {
-		engine = NewEngineInterpreter()
-	}
-	return internalwasm.NewStore(ctx, engine.e)
-}
-
-// InstantiateModule instantiates the module namespace or errs if the configuration was invalid.
-//
-// Ex.
-//	exports, _ := wazero.InstantiateModule(wazero.NewStore(), mod)
-//
-// Note: StoreConfig.Context is used for any WebAssembly 1.0 (MVP) Start Function.
-func InstantiateModule(store wasm.Store, module *Module) (wasm.ModuleExports, error) {
-	internal, ok := store.(*internalwasm.Store)
-	if !ok {
-		return nil, fmt.Errorf("unsupported Store implementation: %s", store)
-	}
-	return internal.Instantiate(module.wasm, module.name)
-}
-
-// ExportHostFunctions adds functions written in Go, which a WebAssembly Module can import.
-//
-// Here is a description of parameters:
-// * moduleName is the module name that these functions are imported with. Ex. wasi.ModuleSnapshotPreview1
-// * nameToGoFunc map key is the name to export and the value is the func. Ex. WASISnapshotPreview1
-//
-// Noting a context exception described later, all parameters or result types must match WebAssembly 1.0 (MVP) value
-// types. This means uint32, uint64, float32 or float64. Up to one result can be returned.
-//
-// Ex. This is a valid host function:
-//
-//	addInts := func(x uint32, uint32) uint32 {
-//		return x + y
-//	}
-//
-// Host functions may also have an initial parameter (param[0]) of type context.Context or wasm.ModuleContext.
-//
-// Ex. This uses a Go Context:
-//
-//	addInts := func(ctx context.Context, x uint32, uint32) uint32 {
-//		// add a little extra if we put some in the context!
-//		return x + y + ctx.Value(extraKey).(uint32)
-//	}
-//
-// The most sophisticated context is wasm.ModuleContext, which allows access to the Go context, but also
-// allows writing to memory. This is important because there are only numeric types in Wasm. The only way to share other
-// data is via writing memory and sharing offsets.
-//
-// Ex. This reads the parameters from!
-//
-//	addInts := func(ctx wasm.ModuleContext, offset uint32) uint32 {
-//		x, _ := ctx.Memory().ReadUint32Le(offset)
-//		y, _ := ctx.Memory().ReadUint32Le(offset + 4) // 32 bits == 4 bytes!
-//		return x + y
-//	}
-//
-// See https://www.w3.org/TR/wasm-core-1/#host-functions%E2%91%A2
-func ExportHostFunctions(store wasm.Store, moduleName string, nameToGoFunc map[string]interface{}) (wasm.HostExports, error) {
-	internal, ok := store.(*internalwasm.Store)
-	if !ok {
-		return nil, fmt.Errorf("unsupported Store implementation: %s", store)
-	}
-	return internal.ExportHostFunctions(moduleName, nameToGoFunc)
-}
diff --git a/wasi.go b/wasi.go
index 570946f0..67f9a504 100644
--- a/wasi.go
+++ b/wasi.go
@@ -32,13 +32,13 @@ type WASIConfig struct {
 	Preopens map[string]wasi.FS
 }
 
-// WASISnapshotPreview1 are functions to export as wasi.ModuleSnapshotPreview1
-func WASISnapshotPreview1() map[string]interface{} {
+// WASISnapshotPreview1 are functions importable as the module name wasi.ModuleSnapshotPreview1
+func WASISnapshotPreview1() *HostModuleConfig {
 	return WASISnapshotPreview1WithConfig(&WASIConfig{})
 }
 
-// WASISnapshotPreview1WithConfig are functions to export as wasi.ModuleSnapshotPreview1
-func WASISnapshotPreview1WithConfig(c *WASIConfig) map[string]interface{} {
+// WASISnapshotPreview1WithConfig are functions importable as the module name wasi.ModuleSnapshotPreview1
+func WASISnapshotPreview1WithConfig(c *WASIConfig) *HostModuleConfig {
 	// TODO: delete the internalwasi.Option types as they are not accessible as they are internal!
 	var opts []internalwasi.Option
 	if c.Stdin != nil {
@@ -73,7 +73,7 @@ func WASISnapshotPreview1WithConfig(c *WASIConfig) map[string]interface{} {
 			opts = append(opts, internalwasi.Preopen(k, v))
 		}
 	}
-	return internalwasi.SnapshotPreview1Functions(opts...)
+	return &HostModuleConfig{Name: wasi.ModuleSnapshotPreview1, Functions: internalwasi.SnapshotPreview1Functions(opts...)}
 }
 
 // StartWASICommand instantiates the module and starts its WASI Command function ("_start"). The return value are all
@@ -89,16 +89,20 @@ func WASISnapshotPreview1WithConfig(c *WASIConfig) map[string]interface{} {
 // Note: The wasm.Functions return value does not restrict exports after "_start" as allowed in the specification.
 // Note: All TinyGo Wasm are WASI commands. They initialize memory on "_start" and import "fd_write" to implement panic.
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi
-func StartWASICommand(store wasm.Store, module *Module) (wasm.ModuleExports, error) {
+func StartWASICommand(store wasm.Store, module *ModuleConfig) (wasm.ModuleExports, error) {
 	internal, ok := store.(*internalwasm.Store)
 	if !ok {
 		return nil, fmt.Errorf("unsupported Store implementation: %s", store)
 	}
-	if err := internalwasi.ValidateWASICommand(module.wasm, module.name); err != nil {
+	m, name, err := decodeModule(module)
+	if err != nil {
+		return nil, err
+	}
+	if err := internalwasi.ValidateWASICommand(m, name); err != nil {
 		return nil, err
 	}
 
-	instantiated, err := internal.Instantiate(module.wasm, module.name)
+	instantiated, err := internal.Instantiate(m, name)
 	if err != nil {
 		return nil, err
 	}
@@ -106,7 +110,7 @@ func StartWASICommand(store wasm.Store, module *Module) (wasm.ModuleExports, err
 
 	start := ctx.Function(internalwasi.FunctionStart)
 	if _, err = start(ctx.Context()); err != nil {
-		return nil, fmt.Errorf("module[%s] function[%s] failed: %w", module.name, internalwasi.FunctionStart, err)
+		return nil, fmt.Errorf("module[%s] function[%s] failed: %w", name, internalwasi.FunctionStart, err)
 	}
 	return instantiated, nil
 }
diff --git a/wasm.go b/wasm.go
index 65266545..49763832 100644
--- a/wasm.go
+++ b/wasm.go
@@ -1,66 +1,171 @@
 package wazero
 
 import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+
 	internalwasm "github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wasm/binary"
+	"github.com/tetratelabs/wazero/internal/wasm/interpreter"
+	"github.com/tetratelabs/wazero/internal/wasm/jit"
 	"github.com/tetratelabs/wazero/internal/wasm/text"
+	"github.com/tetratelabs/wazero/wasm"
 )
 
-// DecodeModule parses the configured source into a Module. This function returns when the source is exhausted or
-// an error occurs. The result can be initialized for use via InstantiateModule.
-//
-// Here's a description of the return values:
-// * result is the module parsed or nil on error
-// * err is a FormatError invoking the parser, dangling block comments or unexpected characters.
-// See binary.DecodeModule and text.DecodeModule
-type DecodeModule func(source []byte) (result *Module, err error)
-
-var DecodeModuleBinary DecodeModule = func(source []byte) (*Module, error) {
-	return decodeModule(binary.DecodeModule, source)
+type Engine struct {
+	e internalwasm.Engine
 }
 
-var DecodeModuleText DecodeModule = func(source []byte) (*Module, error) {
-	return decodeModule(text.DecodeModule, source)
+func NewEngineInterpreter() *Engine {
+	return &Engine{e: interpreter.NewEngine()}
 }
 
-func decodeModule(decoder internalwasm.DecodeModule, source []byte) (*Module, error) {
-	m, err := decoder(source)
-	if err != nil {
-		return nil, err
+func NewEngineJIT() *Engine { // TODO: compiler?
+	return &Engine{e: jit.NewEngine()}
+}
+
+// StoreConfig allows customization of a Store via NewStoreWithConfig
+type StoreConfig struct {
+	// Context is the default context used to initialize the module. Defaults to context.Background.
+	//
+	// Notes:
+	// * If the Module defines a start function, this is used to invoke it.
+	// * This is the outer-most ancestor of wasm.ModuleContext Context() during wasm.HostFunction invocations.
+	// * This is the default context of wasm.Function when callers pass nil.
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#start-function%E2%91%A0
+	Context context.Context
+	// Engine defaults to NewEngineInterpreter
+	Engine *Engine
+}
+
+func NewStore() wasm.Store {
+	return internalwasm.NewStore(context.Background(), interpreter.NewEngine())
+}
+
+// NewStoreWithConfig returns a store with the given configuration.
+func NewStoreWithConfig(config *StoreConfig) wasm.Store {
+	ctx := config.Context
+	if ctx == nil {
+		ctx = context.Background()
 	}
-	var name string
-	if m.NameSection != nil {
-		name = m.NameSection.ModuleName
+	engine := config.Engine
+	if engine == nil {
+		engine = NewEngineInterpreter()
 	}
-	return &Module{wasm: m, name: name}, nil
+	return internalwasm.NewStore(ctx, engine.e)
 }
 
-// EncodeModule encodes the given module into a byte slice depending on the format of the implementation.
-// See binary.EncodeModule
-type EncodeModule func(m *Module) []byte
-
-var EncodeModuleBinary EncodeModule = func(m *Module) []byte {
-	return encodeModule(binary.EncodeModule, m)
+// ModuleConfig defines the WebAssembly 1.0 (MVP) module to instantiate.
+type ModuleConfig struct {
+	// Name defaults to what's decoded from the custom name section and can be overridden WithName.
+	// See https://www.w3.org/TR/wasm-core-1/#name-section%E2%91%A0
+	Name string
+	// Source is the WebAssembly 1.0 (MVP) text or binary encoding of the module.
+	Source []byte
 }
 
-func encodeModule(encoder internalwasm.EncodeModule, m *Module) []byte {
-	return encoder(m.wasm)
+// InstantiateModule instantiates the module namespace or errs if the configuration was invalid.
+//
+// Ex.
+//	exports, _ := wazero.InstantiateModule(wazero.NewStore(), &wazero.ModuleConfig{Source: wasm})
+//
+// Note: StoreConfig.Context is used for any WebAssembly 1.0 (MVP) Start Function.
+func InstantiateModule(store wasm.Store, module *ModuleConfig) (wasm.ModuleExports, error) {
+	internal, ok := store.(*internalwasm.Store)
+	if !ok {
+		return nil, fmt.Errorf("unsupported Store implementation: %s", store)
+	}
+	m, name, err := decodeModule(module)
+	if err != nil {
+		return nil, err
+	}
+	return internal.Instantiate(m, name)
 }
 
-// Module is a WebAssembly 1.0 (MVP) module decoded (DecodeModule) from a valid source.
-type Module struct {
-	name string
-	wasm *internalwasm.Module
+func decodeModule(module *ModuleConfig) (m *internalwasm.Module, name string, err error) {
+	if module.Source == nil {
+		err = errors.New("source == nil")
+		return
+	}
+
+	if len(module.Source) < 8 { // Ex. less than magic+version in binary or '(module)' in text
+		err = errors.New("invalid source")
+		return
+	}
+
+	// Peek to see if this is a binary or text format
+	if bytes.Equal(module.Source[0:4], binary.Magic) {
+		m, err = binary.DecodeModule(module.Source)
+	} else {
+		m, err = text.DecodeModule(module.Source)
+	}
+	if err != nil {
+		return
+	}
+
+	name = module.Name
+	if name == "" && m.NameSection != nil {
+		name = m.NameSection.ModuleName
+	}
+	return
 }
 
-// Name defaults to what's decoded from the custom name section and can be overridden WithName.
-// See https://www.w3.org/TR/wasm-core-1/#name-section%E2%91%A0
-func (m *Module) Name() string {
-	return m.name
+// HostModuleConfig are WebAssembly 1.0 (MVP) exports from the host bound to a module name used by InstantiateHostModule.
+type HostModuleConfig struct {
+	// Name is the module name that these exports can be imported with. Ex. wasi.ModuleSnapshotPreview1
+	Name string
+
+	// Functions adds functions written in Go, which a WebAssembly Module can import.
+	//
+	// The key is the name to export and the value is the func. Ex. WASISnapshotPreview1
+	//
+	// Noting a context exception described later, all parameters or result types must match WebAssembly 1.0 (MVP) value
+	// types. This means uint32, uint64, float32 or float64. Up to one result can be returned.
+	//
+	// Ex. This is a valid host function:
+	//
+	//	addInts := func(x uint32, uint32) uint32 {
+	//		return x + y
+	//	}
+	//
+	// Host functions may also have an initial parameter (param[0]) of type context.Context or wasm.ModuleContext.
+	//
+	// Ex. This uses a Go Context:
+	//
+	//	addInts := func(ctx context.Context, x uint32, uint32) uint32 {
+	//		// add a little extra if we put some in the context!
+	//		return x + y + ctx.Value(extraKey).(uint32)
+	//	}
+	//
+	// The most sophisticated context is wasm.ModuleContext, which allows access to the Go context, but also
+	// allows writing to memory. This is important because there are only numeric types in Wasm. The only way to share other
+	// data is via writing memory and sharing offsets.
+	//
+	// Ex. This reads the parameters from!
+	//
+	//	addInts := func(ctx wasm.ModuleContext, offset uint32) uint32 {
+	//		x, _ := ctx.Memory().ReadUint32Le(offset)
+	//		y, _ := ctx.Memory().ReadUint32Le(offset + 4) // 32 bits == 4 bytes!
+	//		return x + y
+	//	}
+	//
+	// See https://www.w3.org/TR/wasm-core-1/#host-functions%E2%91%A2
+	Functions map[string]interface{}
 }
 
-// WithName overwrites Name
-func (m *Module) WithName(name string) *Module {
-	m.name = name
-	return m
+// InstantiateHostModule instantiates the module namespace from the host or errs if the configuration was invalid.
+//
+// Ex.
+//	store := wazero.NewStore()
+//	wasiExports, _ := wazero.InstantiateHostModule(store, wazero.WASISnapshotPreview1())
+//
+func InstantiateHostModule(store wasm.Store, hostModule *HostModuleConfig) (wasm.HostExports, error) {
+	internal, ok := store.(*internalwasm.Store)
+	if !ok {
+		return nil, fmt.Errorf("unsupported Store implementation: %s", store)
+	}
+	return internal.ExportHostFunctions(hostModule.Name, hostModule.Functions)
 }


Test patch:
diff --git a/examples/add_test.go b/examples/add_test.go
index 34a6a6bb..5c409f92 100644
--- a/examples/add_test.go
+++ b/examples/add_test.go
@@ -12,7 +12,7 @@ import (
 // Test_AddInt shows how you can define a function in text format and have it compiled inline.
 // See https://github.com/summerwind/the-art-of-webassembly-go/blob/main/chapter1/addint/addint.wat
 func Test_AddInt(t *testing.T) {
-	mod, err := wazero.DecodeModuleText([]byte(`(module $test
+	exports, err := wazero.InstantiateModule(wazero.NewStore(), &wazero.ModuleConfig{Source: []byte(`(module $test
     (func $addInt ;; TODO: function exports (export "AddInt")
         (param $value_1 i32) (param $value_2 i32)
         (result i32)
@@ -21,13 +21,7 @@ func Test_AddInt(t *testing.T) {
         i32.add
     )
     (export "AddInt" (func $addInt))
-)`))
-	require.NoError(t, err)
-
-	store := wazero.NewStore()
-	require.NoError(t, err)
-
-	exports, err := wazero.InstantiateModule(store, mod)
+)`)})
 	require.NoError(t, err)
 
 	addInt := exports.Function("AddInt")
diff --git a/examples/fibonacci_test.go b/examples/fibonacci_test.go
index 3bdff1be..df2d5af6 100644
--- a/examples/fibonacci_test.go
+++ b/examples/fibonacci_test.go
@@ -8,7 +8,6 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/wazero"
-	"github.com/tetratelabs/wazero/wasi"
 )
 
 // fibWasm was compiled from TinyGo testdata/fibonacci.go
@@ -16,16 +15,13 @@ import (
 var fibWasm []byte // TODO: implement this in text format as it is less distracting setup
 
 func Test_fibonacci(t *testing.T) {
-	mod, err := wazero.DecodeModuleBinary(fibWasm)
-	require.NoError(t, err)
-
 	store := wazero.NewStore()
 
 	// Note: fibonacci.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command.
-	_, err = wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1())
+	_, err := wazero.InstantiateHostModule(store, wazero.WASISnapshotPreview1())
 	require.NoError(t, err)
 
-	exports, err := wazero.StartWASICommand(store, mod)
+	exports, err := wazero.StartWASICommand(store, &wazero.ModuleConfig{Source: fibWasm})
 	require.NoError(t, err)
 
 	fibonacci := exports.Function("fibonacci")
diff --git a/examples/file_system_test.go b/examples/file_system_test.go
index fe50dee9..a41bd268 100644
--- a/examples/file_system_test.go
+++ b/examples/file_system_test.go
@@ -45,22 +45,18 @@ func readFile(fs wasi.FS, path string) ([]byte, error) {
 }
 
 func Test_file_system(t *testing.T) {
-	mod, err := wazero.DecodeModuleBinary(filesystemWasm)
-	require.NoError(t, err)
-
 	store := wazero.NewStore()
 
 	memFS := wazero.WASIMemFS()
-	err = writeFile(memFS, "input.txt", []byte("Hello, file system!"))
+	err := writeFile(memFS, "input.txt", []byte("Hello, file system!"))
 	require.NoError(t, err)
 
-	_, err = wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1WithConfig(
-		&wazero.WASIConfig{Preopens: map[string]wasi.FS{".": memFS}},
-	))
+	wasiConfig := &wazero.WASIConfig{Preopens: map[string]wasi.FS{".": memFS}}
+	_, err = wazero.InstantiateHostModule(store, wazero.WASISnapshotPreview1WithConfig(wasiConfig))
 	require.NoError(t, err)
 
 	// Note: TinyGo binaries must be treated as WASI Commands to initialize memory.
-	_, err = wazero.StartWASICommand(store, mod)
+	_, err = wazero.StartWASICommand(store, &wazero.ModuleConfig{Source: filesystemWasm})
 	require.NoError(t, err)
 
 	out, err := readFile(memFS, "output.txt")
diff --git a/examples/host_func_test.go b/examples/host_func_test.go
index 54de954e..593a1ffc 100644
--- a/examples/host_func_test.go
+++ b/examples/host_func_test.go
@@ -9,7 +9,6 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/wazero"
-	"github.com/tetratelabs/wazero/wasi"
 	"github.com/tetratelabs/wazero/wasm"
 )
 
@@ -50,19 +49,17 @@ func Test_hostFunc(t *testing.T) {
 		require.Equal(t, bufferSize, n)
 	}
 
-	mod, err := wazero.DecodeModuleBinary(hostFuncWasm)
-	require.NoError(t, err)
-
 	store := wazero.NewStore()
 
-	_, err = wazero.ExportHostFunctions(store, "env", map[string]interface{}{"get_random_string": getRandomString})
+	env := &wazero.HostModuleConfig{Name: "env", Functions: map[string]interface{}{"get_random_string": getRandomString}}
+	_, err := wazero.InstantiateHostModule(store, env)
 	require.NoError(t, err)
 
 	// Note: host_func.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command.
-	_, err = wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1())
+	_, err = wazero.InstantiateHostModule(store, wazero.WASISnapshotPreview1())
 	require.NoError(t, err)
 
-	exports, err := wazero.StartWASICommand(store, mod)
+	exports, err := wazero.StartWASICommand(store, &wazero.ModuleConfig{Source: hostFuncWasm})
 	require.NoError(t, err)
 
 	allocateBufferFn := exports.Function("allocate_buffer")
diff --git a/examples/simple_test.go b/examples/simple_test.go
index 3f7b9291..4db02117 100644
--- a/examples/simple_test.go
+++ b/examples/simple_test.go
@@ -17,20 +17,19 @@ func Test_Simple(t *testing.T) {
 		_, _ = fmt.Fprintln(stdout, "hello!")
 	}
 
-	mod, err := wazero.DecodeModuleText([]byte(`(module $test
-	(import "" "hello" (func $hello))
-	(start $hello)
-)`))
-	require.NoError(t, err)
 	store := wazero.NewStore()
 
 	// Host functions can be exported as any module name, including the empty string.
-	_, err = wazero.ExportHostFunctions(store, "", map[string]interface{}{"hello": goFunc})
+	env := &wazero.HostModuleConfig{Name: "", Functions: map[string]interface{}{"hello": goFunc}}
+	_, err := wazero.InstantiateHostModule(store, env)
 	require.NoError(t, err)
 
 	// The "hello" function was imported as $hello in Wasm. Since it was marked as the start
 	// function, it is invoked on instantiation. Ensure that worked: "hello" was called!
-	_, err = wazero.InstantiateModule(store, mod)
+	_, err = wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: []byte(`(module $test
+	(import "" "hello" (func $hello))
+	(start $hello)
+)`)})
 	require.NoError(t, err)
 	require.Equal(t, "hello!\n", stdout.String())
 }
diff --git a/examples/stdio_test.go b/examples/stdio_test.go
index 2c257c1e..42bc2b7c 100644
--- a/examples/stdio_test.go
+++ b/examples/stdio_test.go
@@ -9,7 +9,6 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/wazero"
-	"github.com/tetratelabs/wazero/wasi"
 )
 
 // stdioWasm was compiled from TinyGo testdata/stdio.go
@@ -17,8 +16,6 @@ import (
 var stdioWasm []byte
 
 func Test_stdio(t *testing.T) {
-	mod, err := wazero.DecodeModuleBinary(stdioWasm)
-	require.NoError(t, err)
 	store := wazero.NewStore()
 
 	stdinBuf := bytes.NewBuffer([]byte("WASI\n"))
@@ -27,11 +24,11 @@ func Test_stdio(t *testing.T) {
 
 	// Configure WASI host functions with the IO buffers
 	wasiConfig := &wazero.WASIConfig{Stdin: stdinBuf, Stdout: stdoutBuf, Stderr: stderrBuf}
-	_, err = wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1WithConfig(wasiConfig))
+	_, err := wazero.InstantiateHostModule(store, wazero.WASISnapshotPreview1WithConfig(wasiConfig))
 	require.NoError(t, err)
 
 	// StartWASICommand runs the "_start" function which is what TinyGo compiles "main" to
-	_, err = wazero.StartWASICommand(store, mod)
+	_, err = wazero.StartWASICommand(store, &wazero.ModuleConfig{Source: stdioWasm})
 	require.NoError(t, err)
 
 	require.Equal(t, "Hello, WASI!", strings.TrimSpace(stdoutBuf.String()))
diff --git a/examples/wasi_test.go b/examples/wasi_test.go
index 08092fb1..7ee877a7 100644
--- a/examples/wasi_test.go
+++ b/examples/wasi_test.go
@@ -30,22 +30,14 @@ func Test_WASI(t *testing.T) {
 		_, _ = fmt.Fprintf(stdout, "random: %x\n", random)
 	}
 
-	mod, err := wazero.DecodeModuleText([]byte(`(module $wasi
-	(import "wasi_snapshot_preview1" "random_get"
-		(func $wasi.random_get (param $buf i32) (param $buf_len i32) (result (;errno;) i32)))
-	(import "" "random" (func $random))
-	(memory 1)
-	(start $random)
-)`))
-	require.NoError(t, err)
 	store := wazero.NewStore()
 
 	// Host functions can be exported as any module name, including the empty string.
-	_, err = wazero.ExportHostFunctions(store, "", map[string]interface{}{"random": goFunc})
-	require.NoError(t, err)
+	env := &wazero.HostModuleConfig{Name: "", Functions: map[string]interface{}{"random": goFunc}}
+	_, err := wazero.InstantiateHostModule(store, env)
 
 	// Configure WASI and implement the function to use it
-	we, err := wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1())
+	we, err := wazero.InstantiateHostModule(store, wazero.WASISnapshotPreview1())
 	require.NoError(t, err)
 	randomGetFn := we.Function("random_get")
 
@@ -58,7 +50,13 @@ func Test_WASI(t *testing.T) {
 
 	// The "random" function was imported as $random in Wasm. Since it was marked as the start
 	// function, it is invoked on instantiation. Ensure that worked: "random" was called!
-	_, err = wazero.InstantiateModule(store, mod)
+	_, err = wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: []byte(`(module $wasi
+	(import "wasi_snapshot_preview1" "random_get"
+		(func $wasi.random_get (param $buf i32) (param $buf_len i32) (result (;errno;) i32)))
+	(import "" "random" (func $random))
+	(memory 1)
+	(start $random)
+)`)})
 	require.NoError(t, err)
 	require.Contains(t, stdout.String(), "random: ")
 }
diff --git a/internal/wasm/binary/decoder_test.go b/internal/wasm/binary/decoder_test.go
index 34d326d1..1e20027e 100644
--- a/internal/wasm/binary/decoder_test.go
+++ b/internal/wasm/binary/decoder_test.go
@@ -93,7 +93,7 @@ func TestDecodeModule(t *testing.T) {
 		})
 	}
 	t.Run("skips custom section", func(t *testing.T) {
-		input := append(append(magic, version...),
+		input := append(append(Magic, version...),
 			wasm.SectionIDCustom, 0xf, // 15 bytes in this section
 			0x04, 'm', 'e', 'm', 'e',
 			1, 2, 3, 4, 5, 6, 7, 8, 9, 0)
@@ -102,7 +102,7 @@ func TestDecodeModule(t *testing.T) {
 		require.Equal(t, &wasm.Module{}, m)
 	})
 	t.Run("skips custom section, but not name", func(t *testing.T) {
-		input := append(append(magic, version...),
+		input := append(append(Magic, version...),
 			wasm.SectionIDCustom, 0xf, // 15 bytes in this section
 			0x04, 'm', 'e', 'm', 'e',
 			1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
@@ -135,7 +135,7 @@ func TestDecodeModule_Errors(t *testing.T) {
 		},
 		{
 			name: "redundant name section",
-			input: append(append(magic, version...),
+			input: append(append(Magic, version...),
 				wasm.SectionIDCustom, 0x09, // 9 bytes in this section
 				0x04, 'n', 'a', 'm', 'e',
 				subsectionIDModuleName, 0x02, 0x01, 'x',
diff --git a/internal/wasm/binary/encoder_test.go b/internal/wasm/binary/encoder_test.go
index d1ee8949..a65dc06f 100644
--- a/internal/wasm/binary/encoder_test.go
+++ b/internal/wasm/binary/encoder_test.go
@@ -20,12 +20,12 @@ func TestModule_Encode(t *testing.T) {
 		{
 			name:     "empty",
 			input:    &wasm.Module{},
-			expected: append(magic, version...),
+			expected: append(Magic, version...),
 		},
 		{
 			name:  "only name section",
 			input: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "simple"}},
-			expected: append(append(magic, version...),
+			expected: append(append(Magic, version...),
 				wasm.SectionIDCustom, 0x0e, // 14 bytes in this section
 				0x04, 'n', 'a', 'm', 'e',
 				subsectionIDModuleName, 0x07, // 7 bytes in this subsection
@@ -41,7 +41,7 @@ func TestModule_Encode(t *testing.T) {
 					{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}},
 				},
 			},
-			expected: append(append(magic, version...),
+			expected: append(append(Magic, version...),
 				wasm.SectionIDType, 0x12, // 18 bytes in this section
 				0x03,             // 3 types
 				0x60, 0x00, 0x00, // func=0x60 no param no result
@@ -68,7 +68,7 @@ func TestModule_Encode(t *testing.T) {
 					},
 				},
 			},
-			expected: append(append(magic, version...),
+			expected: append(append(Magic, version...),
 				wasm.SectionIDType, 0x0d, // 13 bytes in this section
 				0x02,                            // 2 types
 				0x60, 0x02, i32, i32, 0x01, i32, // func=0x60 2 params and 1 result
@@ -92,7 +92,7 @@ func TestModule_Encode(t *testing.T) {
 				}},
 				StartSection: &zero,
 			},
-			expected: append(append(magic, version...),
+			expected: append(append(Magic, version...),
 				wasm.SectionIDType, 0x04, // 4 bytes in this section
 				0x01,           // 1 type
 				0x60, 0x0, 0x0, // func=0x60 0 params and 0 result
@@ -127,7 +127,7 @@ func TestModule_Encode(t *testing.T) {
 					},
 				},
 			},
-			expected: append(append(magic, version...),
+			expected: append(append(Magic, version...),
 				wasm.SectionIDType, 0x07, // 7 bytes in this section
 				0x01,                            // 1 type
 				0x60, 0x02, i32, i32, 0x01, i32, // func=0x60 2 params and 1 result
diff --git a/tests/bench/bench_fac_iter_test.go b/tests/bench/bench_fac_iter_test.go
index 64e8ff63..42a33995 100644
--- a/tests/bench/bench_fac_iter_test.go
+++ b/tests/bench/bench_fac_iter_test.go
@@ -175,14 +175,9 @@ func BenchmarkFacIter_Invoke(b *testing.B) {
 }
 
 func newWazeroFacIterBench(engine *wazero.Engine) (wasm.Function, error) {
-	mod, err := wazero.DecodeModuleBinary(facWasm)
-	if err != nil {
-		return nil, err
-	}
-
 	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: engine})
 
-	m, err := wazero.InstantiateModule(store, mod)
+	m, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: facWasm})
 	if err != nil {
 		return nil, err
 	}
diff --git a/tests/bench/bench_test.go b/tests/bench/bench_test.go
index 54397223..84804b81 100644
--- a/tests/bench/bench_test.go
+++ b/tests/bench/bench_test.go
@@ -8,7 +8,6 @@ import (
 	"testing"
 
 	"github.com/tetratelabs/wazero"
-	"github.com/tetratelabs/wazero/wasi"
 	"github.com/tetratelabs/wazero/wasm"
 )
 
@@ -132,23 +131,20 @@ func instantiateHostFunctionModuleWithEngine(b *testing.B, engine *wazero.Engine
 
 	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: engine})
 
-	_, err := wazero.ExportHostFunctions(store, "env", map[string]interface{}{"get_random_string": getRandomString})
+	env := &wazero.HostModuleConfig{Name: "env", Functions: map[string]interface{}{"get_random_string": getRandomString}}
+	_, err := wazero.InstantiateHostModule(store, env)
 	if err != nil {
 		b.Fatal(err)
 	}
 
 	// Note: host_func.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command.
-	_, err = wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1())
+	// Add WASI to satisfy import tests
+	_, err = wazero.InstantiateHostModule(store, wazero.WASISnapshotPreview1())
 	if err != nil {
 		b.Fatal(err)
 	}
 
-	mod, err := wazero.DecodeModuleBinary(caseWasm)
-	if err != nil {
-		b.Fatal(err)
-	}
-
-	m, err := wazero.StartWASICommand(store, mod)
+	m, err := wazero.StartWASICommand(store, &wazero.ModuleConfig{Source: caseWasm})
 	if err != nil {
 		b.Fatal(err)
 	}
diff --git a/tests/codec/codec_test.go b/tests/codec/codec_test.go
index af6e5613..df4fca5a 100644
--- a/tests/codec/codec_test.go
+++ b/tests/codec/codec_test.go
@@ -18,7 +18,6 @@ import (
 	wasm "github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wasm/binary"
 	"github.com/tetratelabs/wazero/internal/wasm/text"
-	publicwasi "github.com/tetratelabs/wazero/wasi"
 )
 
 // example holds the latest supported features as described in the comments of exampleText
@@ -106,13 +105,11 @@ func TestExampleUpToDate(t *testing.T) {
 		store := wazero.NewStore()
 
 		// Add WASI to satisfy import tests
-		_, err := wazero.ExportHostFunctions(store, publicwasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1())
+		_, err := wazero.InstantiateHostModule(store, wazero.WASISnapshotPreview1())
 		require.NoError(t, err)
 
 		// Decode and instantiate the module
-		mod, err := wazero.DecodeModuleBinary(exampleBinary)
-		require.NoError(t, err)
-		exports, err := wazero.InstantiateModule(store, mod)
+		exports, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: exampleBinary})
 		require.NoError(t, err)
 
 		// Call the add function as a smoke test
diff --git a/tests/engine/adhoc_test.go b/tests/engine/adhoc_test.go
index 4d43e30c..dd7e2e85 100644
--- a/tests/engine/adhoc_test.go
+++ b/tests/engine/adhoc_test.go
@@ -65,9 +65,6 @@ func runTests(t *testing.T, newEngine func() *wazero.Engine) {
 }
 
 func testFibonacci(t *testing.T, newEngine func() *wazero.Engine) {
-	mod, err := wazero.DecodeModuleBinary(fibWasm)
-	require.NoError(t, err)
-
 	// We execute 1000 times in order to ensure the JIT engine is stable under high concurrency
 	// and we have no conflict with Go's runtime.
 	const goroutines = 1000
@@ -77,7 +74,7 @@ func testFibonacci(t *testing.T, newEngine func() *wazero.Engine) {
 		go func() {
 			defer wg.Done()
 			store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
-			exports, err := wazero.InstantiateModule(store, mod)
+			exports, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: fibWasm})
 			require.NoError(t, err)
 
 			results, err := exports.Function("fib")(nil, 20)
@@ -90,10 +87,8 @@ func testFibonacci(t *testing.T, newEngine func() *wazero.Engine) {
 }
 
 func testFac(t *testing.T, newEngine func() *wazero.Engine) {
-	mod, err := wazero.DecodeModuleBinary(facWasm)
-	require.NoError(t, err)
 	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
-	exports, err := wazero.InstantiateModule(store, mod)
+	exports, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: facWasm})
 	require.NoError(t, err)
 	for _, name := range []string{
 		"fac-rec",
@@ -120,9 +115,6 @@ func testFac(t *testing.T, newEngine func() *wazero.Engine) {
 }
 
 func testUnreachable(t *testing.T, newEngine func() *wazero.Engine) {
-	mod, err := wazero.DecodeModuleBinary(unreachableWasm)
-	require.NoError(t, err)
-
 	callUnreachable := func(ctx publicwasm.ModuleContext) {
 		_, err := ctx.Function("unreachable_func")(ctx.Context())
 		require.NoError(t, err)
@@ -130,10 +122,11 @@ func testUnreachable(t *testing.T, newEngine func() *wazero.Engine) {
 
 	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
 
-	_, err = wazero.ExportHostFunctions(store, "host", map[string]interface{}{"cause_unreachable": callUnreachable})
+	hostModule := &wazero.HostModuleConfig{Name: "host", Functions: map[string]interface{}{"cause_unreachable": callUnreachable}}
+	_, err := wazero.InstantiateHostModule(store, hostModule)
 	require.NoError(t, err)
 
-	exports, err := wazero.InstantiateModule(store, mod)
+	exports, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: unreachableWasm})
 	require.NoError(t, err)
 
 	_, err = exports.Function("main")(nil)
@@ -149,10 +142,8 @@ wasm backtrace:
 }
 
 func testMemory(t *testing.T, newEngine func() *wazero.Engine) {
-	mod, err := wazero.DecodeModuleBinary(memoryWasm)
-	require.NoError(t, err)
 	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
-	exports, err := wazero.InstantiateModule(store, mod)
+	exports, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: memoryWasm})
 	require.NoError(t, err)
 
 	size := exports.Function("size")
@@ -184,20 +175,18 @@ func testMemory(t *testing.T, newEngine func() *wazero.Engine) {
 }
 
 func testRecursiveEntry(t *testing.T, newEngine func() *wazero.Engine) {
-	mod, err := wazero.DecodeModuleBinary(recursiveWasm)
-	require.NoError(t, err)
-
 	hostfunc := func(ctx publicwasm.ModuleContext) {
-		_, err = ctx.Function("called_by_host_func")(nil)
+		_, err := ctx.Function("called_by_host_func")(nil)
 		require.NoError(t, err)
 	}
 
 	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
 
-	_, err = wazero.ExportHostFunctions(store, "env", map[string]interface{}{"host_func": hostfunc})
+	hostModule := &wazero.HostModuleConfig{Name: "env", Functions: map[string]interface{}{"host_func": hostfunc}}
+	_, err := wazero.InstantiateHostModule(store, hostModule)
 	require.NoError(t, err)
 
-	exports, err := wazero.InstantiateModule(store, mod)
+	exports, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: recursiveWasm})
 	require.NoError(t, err)
 
 	_, err = exports.Function("main")(nil, 1)
@@ -207,16 +196,6 @@ func testRecursiveEntry(t *testing.T, newEngine func() *wazero.Engine) {
 // testImportedAndExportedFunc fails if the engine cannot call an "imported-and-then-exported-back" function
 // Notably, this uses memory, which ensures wasm.ModuleContext is valid in both interpreter and JIT engines.
 func testImportedAndExportedFunc(t *testing.T, newEngine func() *wazero.Engine) {
-	mod, err := wazero.DecodeModuleText([]byte(`(module $test
-		(import "" "store_int"
-			(func $store_int (param $offset i32) (param $val i64) (result (;errno;) i32)))
-		(memory $memory 1 1)
-		(export "memory" (memory $memory))
-		;; store_int is imported from the environment, but it's also exported back to the environment
-		(export "store_int" (func $store_int))
-		)`))
-	require.NoError(t, err)
-
 	var memory *wasm.MemoryInstance
 	storeInt := func(ctx publicwasm.ModuleContext, offset uint32, val uint64) uint32 {
 		if !ctx.Memory().WriteUint64Le(offset, val) {
@@ -229,10 +208,18 @@ func testImportedAndExportedFunc(t *testing.T, newEngine func() *wazero.Engine)
 
 	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
 
-	_, err = wazero.ExportHostFunctions(store, "", map[string]interface{}{"store_int": storeInt})
+	hostModule := &wazero.HostModuleConfig{Name: "", Functions: map[string]interface{}{"store_int": storeInt}}
+	_, err := wazero.InstantiateHostModule(store, hostModule)
 	require.NoError(t, err)
 
-	exports, err := wazero.InstantiateModule(store, mod)
+	exports, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: []byte(`(module $test
+		(import "" "store_int"
+			(func $store_int (param $offset i32) (param $val i64) (result (;errno;) i32)))
+		(memory $memory 1 1)
+		(export "memory" (memory $memory))
+		;; store_int is imported from the environment, but it's also exported back to the environment
+		(export "store_int" (func $store_int))
+		)`)})
 	require.NoError(t, err)
 
 	// Call store_int and ensure it didn't return an error code.
@@ -247,24 +234,6 @@ func testImportedAndExportedFunc(t *testing.T, newEngine func() *wazero.Engine)
 //  testHostFunctions ensures arg0 is optionally a context, and fails if a float parameter corrupts a host function value
 func testHostFunctions(t *testing.T, newEngine func() *wazero.Engine) {
 	ctx := context.Background()
-	mod, err := wazero.DecodeModuleText([]byte(`(module $test
-	;; these imports return the input param
-	(import "host" "identity_f32" (func $test.identity_f32 (param f32) (result f32)))
-	(import "host" "identity_f64" (func $test.identity_f64 (param f64) (result f64)))
-
-	;; 'call->test.identity_fXX' proxies 'test.identity_fXX' to test floats aren't corrupted through OpCodeCall
-	(func $call->test.identity_f32 (param f32) (result f32)
-		local.get 0
-		call $test.identity_f32
-	)
-	(export "call->test.identity_f32" (func $call->test.identity_f32))
-	(func $call->test.identity_f64 (param f64) (result f64)
-		local.get 0
-		call $test.identity_f64
-	)
-	(export "call->test.identity_f64" (func $call->test.identity_f64))
-)`))
-	require.NoError(t, err)
 
 	floatFuncs := map[string]interface{}{
 		"identity_f32": func(value float32) float32 {
@@ -301,10 +270,27 @@ func testHostFunctions(t *testing.T, newEngine func() *wazero.Engine) {
 	} {
 		store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
 
-		_, err = wazero.ExportHostFunctions(store, "host", v)
+		hostModule := &wazero.HostModuleConfig{Name: "host", Functions: v}
+		_, err := wazero.InstantiateHostModule(store, hostModule)
 		require.NoError(t, err)
 
-		m, err := wazero.InstantiateModule(store, mod)
+		m, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: []byte(`(module $test
+	;; these imports return the input param
+	(import "host" "identity_f32" (func $test.identity_f32 (param f32) (result f32)))
+	(import "host" "identity_f64" (func $test.identity_f64 (param f64) (result f64)))
+
+	;; 'call->test.identity_fXX' proxies 'test.identity_fXX' to test floats aren't corrupted through OpCodeCall
+	(func $call->test.identity_f32 (param f32) (result f32)
+		local.get 0
+		call $test.identity_f32
+	)
+	(export "call->test.identity_f32" (func $call->test.identity_f32))
+	(func $call->test.identity_f64 (param f64) (result f64)
+		local.get 0
+		call $test.identity_f64
+	)
+	(export "call->test.identity_f64" (func $call->test.identity_f64))
+)`)})
 		require.NoError(t, err)
 
 		t.Run(fmt.Sprintf("host function with f32 param%s", k), func(t *testing.T) {
diff --git a/wasi_test.go b/wasi_test.go
index b2b8c2a0..4d14111d 100644
--- a/wasi_test.go
+++ b/wasi_test.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/tetratelabs/wazero/wasi"
 	"github.com/tetratelabs/wazero/wasm"
 )
 
@@ -21,22 +20,20 @@ func TestStartWASICommand_UsesStoreContext(t *testing.T) {
 		calledStart = true
 		require.Equal(t, config.Context, ctx.Context())
 	}
-	_, err := ExportHostFunctions(store, "", map[string]interface{}{"start": start})
-	require.NoError(t, err)
 
-	mod, err := DecodeModuleText([]byte(`(module $wasi_test.go
-	(import "" "start" (func $start))
-	(memory 1)
-	(export "_start" (func $start))
-	(export "memory" (memory 0))
-)`))
+	_, err := InstantiateHostModule(store, &HostModuleConfig{Functions: map[string]interface{}{"start": start}})
 	require.NoError(t, err)
 
-	_, err = ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, WASISnapshotPreview1())
+	_, err = InstantiateHostModule(store, WASISnapshotPreview1())
 	require.NoError(t, err)
 
 	// Start the module as a WASI command. This will fail if the context wasn't as intended.
-	_, err = StartWASICommand(store, mod)
+	_, err = StartWASICommand(store, &ModuleConfig{Source: []byte(`(module $wasi_test.go
+	(import "" "start" (func $start))
+	(memory 1)
+	(export "_start" (func $start))
+	(export "memory" (memory 0))
+)`)})
 	require.NoError(t, err)
 	require.True(t, calledStart)
 }
diff --git a/store_test.go b/wasm_test.go
similarity index 58%
rename from store_test.go
rename to wasm_test.go
index 556d1c65..4875e4dc 100644
--- a/store_test.go
+++ b/wasm_test.go
@@ -8,9 +8,75 @@ import (
 
 	"github.com/stretchr/testify/require"
 
+	"github.com/tetratelabs/wazero/internal/wasm/binary"
+	"github.com/tetratelabs/wazero/internal/wasm/text"
 	"github.com/tetratelabs/wazero/wasm"
 )
 
+func TestDecodeModule(t *testing.T) {
+	wat := []byte(`(module $test)`)
+	m, err := text.DecodeModule(wat)
+	require.NoError(t, err)
+	wasm := binary.EncodeModule(m)
+
+	tests := []struct {
+		name       string
+		moduleName string
+		source     []byte
+	}{
+		{name: "binary", source: wasm},
+		{name: "binary - override name", source: wasm, moduleName: "override"},
+		{name: "text", source: wat},
+		{name: "text - override name", source: wat, moduleName: "override"},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			_, name, err := decodeModule(&ModuleConfig{Name: tc.moduleName, Source: tc.source})
+			require.NoError(t, err)
+			if tc.moduleName == "" {
+				require.Equal(t, "test" /* from the text format */, name)
+			} else {
+				require.Equal(t, tc.moduleName, name)
+			}
+		})
+	}
+}
+
+func TestDecodeModule_Errors(t *testing.T) {
+	tests := []struct {
+		name        string
+		source      []byte
+		expectedErr string
+	}{
+		{
+			name:        "nil",
+			expectedErr: "source == nil",
+		},
+		{
+			name:        "invalid binary",
+			source:      append(binary.Magic, []byte("yolo")...),
+			expectedErr: "invalid version header",
+		},
+		{
+			name:        "invalid text",
+			source:      []byte(`(modular)`),
+			expectedErr: "1:2: unexpected field: modular",
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			_, _, err := decodeModule(&ModuleConfig{Source: tc.source})
+			require.EqualError(t, err, tc.expectedErr)
+		})
+	}
+}
+
 // TestModuleExports_Memory only covers a couple cases to avoid duplication of internal/wasm/store_test.go
 func TestModuleExports_Memory(t *testing.T) {
 	tests := []struct {
@@ -31,15 +97,11 @@ func TestModuleExports_Memory(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			mod, err := DecodeModuleText([]byte(tc.wat))
-			require.NoError(t, err)
-
 			// Instantiate the module and get the export of the above hostFn
-			exports, err := InstantiateModule(NewStore(), mod)
+			exports, err := InstantiateModule(NewStore(), &ModuleConfig{Source: []byte(tc.wat)})
 			require.NoError(t, err)
 
 			mem := exports.Memory("memory")
@@ -89,10 +151,10 @@ func TestFunction_Context(t *testing.T) {
 				require.Equal(t, tc.expected, ctx.Context())
 				return expectedResult
 			}
-			mod := requireImportAndExportFunction(t, store, hostFn, functionName)
+			source := requireImportAndExportFunction(t, store, hostFn, functionName)
 
 			// Instantiate the module and get the export of the above hostFn
-			exports, err := InstantiateModule(store, mod)
+			exports, err := InstantiateModule(store, &ModuleConfig{Source: source})
 			require.NoError(t, err)
 
 			// This fails if the function wasn't invoked, or had an unexpected context.
@@ -114,27 +176,27 @@ func TestInstantiateModule_UsesStoreContext(t *testing.T) {
 		calledStart = true
 		require.Equal(t, config.Context, ctx.Context())
 	}
-	_, err := ExportHostFunctions(store, "", map[string]interface{}{"start": start})
-	require.NoError(t, err)
 
-	mod, err := DecodeModuleText([]byte(`(module $store_test.go
-	(import "" "start" (func $start))
-	(start $start)
-)`))
+	_, err := InstantiateHostModule(store, &HostModuleConfig{Functions: map[string]interface{}{"start": start}})
 	require.NoError(t, err)
 
 	// Instantiate the module, which calls the start function. This will fail if the context wasn't as intended.
-	_, err = InstantiateModule(store, mod)
+	_, err = InstantiateModule(store, &ModuleConfig{Source: []byte(`(module $store_test.go
+	(import "" "start" (func $start))
+	(start $start)
+)`)})
 	require.NoError(t, err)
 	require.True(t, calledStart)
 }
 
 // requireImportAndExportFunction re-exports a host function because only host functions can see the propagated context.
-func requireImportAndExportFunction(t *testing.T, store wasm.Store, hostFn func(ctx wasm.ModuleContext) uint64, functionName string) *Module {
-	_, err := ExportHostFunctions(store, "host", map[string]interface{}{functionName: hostFn})
+func requireImportAndExportFunction(t *testing.T, store wasm.Store, hostFn func(ctx wasm.ModuleContext) uint64, functionName string) []byte {
+	_, err := InstantiateHostModule(store, &HostModuleConfig{
+		Name: "host", Functions: map[string]interface{}{functionName: hostFn},
+	})
 	require.NoError(t, err)
-	wat := fmt.Sprintf(`(module (import "host" "%[1]s" (func (result i64))) (export "%[1]s" (func 0)))`, functionName)
-	mod, err := DecodeModuleText([]byte(wat))
-	require.NoError(t, err)
-	return mod
+
+	return []byte(fmt.Sprintf(
+		`(module (import "host" "%[1]s" (func (result i64))) (export "%[1]s" (func 0)))`, functionName,
+	))
 }