Repository: tetratelabs/wazero
Pull request #349 (2022-03-09 02:06:58)

Runtime.NewModule -> InstantiateModule and adds ModuleBuilder
This reverts `Runtime.NewModule` back to `InstantiateModule` as it calls
more attention to the registration aspect of it, and also makes a chain
of `NewXX` more clear. This is particularly helpful as this change
introduces `ModuleBuilder` which is created by `NewModuleBuilder`.

`ModuleBuilder` is a way to define a WebAssembly 1.0 (20191205) in Go.
The first iteration allows setting the module name and exported
functions. The next PR will add globals.

Ex. Below defines and instantiates a module named "env" with one function:

```go
hello := func() {
	fmt.Fprintln(stdout, "hello!")
}
_, err := r.NewModuleBuilder("env").ExportFunction("hello", hello).Instantiate()
```

If the same module may be instantiated multiple times, it is more efficient to separate steps. Ex.

```go
env, err := r.NewModuleBuilder("env").ExportFunction("get_random_string", getRandomString).Build()

_, err := r.InstantiateModule(env.WithName("env.1"))
_, err := r.InstantiateModule(env.WithName("env.2"))
```

Note: Builder methods do not return errors, to allow chaining. Any validation errors are deferred until Build.
Note: Insertion order is not retained. Anything defined by this builder is sorted lexicographically on Build.



Patch:
diff --git a/README.md b/README.md
index f42c7362..2478fced 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ func main() {
 	source, _ := os.ReadFile("./tests/engine/testdata/fac.wasm")
 
 	// Instantiate the module and return its exported functions
-	module, _ := wazero.NewRuntime().NewModuleFromSource(source)
+	module, _ := wazero.NewRuntime().InstantiateModuleFromSource(source)
 
 	// Discover 7! is 5040
 	fmt.Println(module.ExportedFunction("fac").Call(nil, 7))
diff --git a/builder.go b/builder.go
new file mode 100644
index 00000000..1836f90a
--- /dev/null
+++ b/builder.go
@@ -0,0 +1,124 @@
+package wazero
+
+import (
+	internalwasm "github.com/tetratelabs/wazero/internal/wasm"
+	"github.com/tetratelabs/wazero/wasm"
+)
+
+// ModuleBuilder is a way to define a WebAssembly 1.0 (20191205) in Go.
+//
+// Ex. Below defines and instantiates a module named "env" with one function:
+//
+//	hello := func() {
+//		fmt.Fprintln(stdout, "hello!")
+//	}
+//	_, err := r.NewModuleBuilder("env").ExportFunction("hello", hello).Instantiate()
+//
+// If the same module may be instantiated multiple times, it is more efficient to separate steps. Ex.
+//
+//	env, err := r.NewModuleBuilder("env").ExportFunction("get_random_string", getRandomString).Build()
+//
+//	_, err := r.InstantiateModule(env.WithName("env.1"))
+//	_, err := r.InstantiateModule(env.WithName("env.2"))
+//
+// Note: Builder methods do not return errors, to allow chaining. Any validation errors are deferred until Build.
+// Note: Insertion order is not retained. Anything defined by this builder is sorted lexicographically on Build.
+type ModuleBuilder interface {
+
+	// ExportFunction adds a function written in Go, which a WebAssembly Module can import.
+	//
+	// * name - the name to export. Ex "random_get"
+	// * goFunc - the `func` to export.
+	//
+	// Noting a context exception described later, all parameters or result types must match WebAssembly 1.0 (20191205) 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.Module.
+	//
+	// 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.Module, 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.Module, offset uint32) uint32 {
+	//		x, _ := ctx.Memory().ReadUint32Le(offset)
+	//		y, _ := ctx.Memory().ReadUint32Le(offset + 4) // 32 bits == 4 bytes!
+	//		return x + y
+	//	}
+	//
+	// Note: If a function is already exported with the same name, this overwrites it.
+	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#host-functions%E2%91%A2
+	ExportFunction(name string, goFunc interface{}) ModuleBuilder
+
+	// ExportFunctions is a convenience that calls ExportFunction for each key/value in the provided map.
+	ExportFunctions(nameToGoFunc map[string]interface{}) ModuleBuilder
+
+	// Build returns a Module to instantiate, or returns an error if any of the configuration is invalid.
+	Build() (*Module, error)
+
+	// Instantiate is a convenience that calls Build, then Runtime.InstantiateModule
+	Instantiate() (wasm.Module, error)
+}
+
+// moduleBuilder implements ModuleBuilder
+type moduleBuilder struct {
+	r            *runtime
+	moduleName   string
+	nameToGoFunc map[string]interface{}
+}
+
+// NewModuleBuilder implements Runtime.NewModuleBuilder
+func (r *runtime) NewModuleBuilder(moduleName string) ModuleBuilder {
+	return &moduleBuilder{
+		r:            r,
+		moduleName:   moduleName,
+		nameToGoFunc: map[string]interface{}{},
+	}
+}
+
+// ExportFunction implements ModuleBuilder.ExportFunction
+func (b *moduleBuilder) ExportFunction(name string, goFunc interface{}) ModuleBuilder {
+	b.nameToGoFunc[name] = goFunc
+	return b
+}
+
+// ExportFunctions implements ModuleBuilder.ExportFunctions
+func (b *moduleBuilder) ExportFunctions(nameToGoFunc map[string]interface{}) ModuleBuilder {
+	for k, v := range nameToGoFunc {
+		b.ExportFunction(k, v)
+	}
+	return b
+}
+
+// Build implements ModuleBuilder.Build
+func (b *moduleBuilder) Build() (*Module, error) {
+	// TODO: we can use r.enabledFeatures to fail early on things like mutable globals
+	if module, err := internalwasm.NewHostModule(b.moduleName, b.nameToGoFunc); err != nil {
+		return nil, err
+	} else {
+		return &Module{name: b.moduleName, module: module}, nil
+	}
+}
+
+// InstantiateModule implements ModuleBuilder.InstantiateModule
+func (b *moduleBuilder) Instantiate() (wasm.Module, error) {
+	if module, err := b.Build(); err != nil {
+		return nil, err
+	} else {
+		return b.r.InstantiateModule(module)
+	}
+}
diff --git a/config.go b/config.go
index b230463f..9a34bc6d 100644
--- a/config.go
+++ b/config.go
@@ -71,56 +71,13 @@ func (r *RuntimeConfig) WithFeatureSignExtensionOps(enabled bool) *RuntimeConfig
 	return &RuntimeConfig{engine: r.engine, ctx: r.ctx, enabledFeatures: enabledFeatures}
 }
 
-// DecodedModule is a WebAssembly 1.0 (20191205) text or binary encoded module to instantiate.
-type DecodedModule struct {
+// Module is a WebAssembly 1.0 (20191205) module to instantiate.
+type Module struct {
 	name   string
 	module *internalwasm.Module
 }
 
 // WithName returns a new instance which overrides the name.
-func (m *DecodedModule) WithName(moduleName string) *DecodedModule {
-	return &DecodedModule{name: moduleName, module: m.module}
-}
-
-// HostModuleConfig are WebAssembly 1.0 (20191205) 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 (20191205) 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.Module.
-	//
-	// 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.Module, 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.Module, 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/2019/REC-wasm-core-1-20191205/#host-functions%E2%91%A2
-	Functions map[string]interface{}
+func (m *Module) WithName(moduleName string) *Module {
+	return &Module{name: moduleName, module: m.module}
 }
diff --git a/wasi.go b/wasi.go
index b09ef55c..f312f9e9 100644
--- a/wasi.go
+++ b/wasi.go
@@ -5,6 +5,7 @@ import (
 	"io"
 
 	internalwasi "github.com/tetratelabs/wazero/internal/wasi"
+	internalwasm "github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/wasi"
 	"github.com/tetratelabs/wazero/wasm"
 )
@@ -32,12 +33,12 @@ type WASIConfig struct {
 }
 
 // WASISnapshotPreview1 are functions importable as the module name wasi.ModuleSnapshotPreview1
-func WASISnapshotPreview1() *HostModuleConfig {
+func WASISnapshotPreview1() *Module {
 	return WASISnapshotPreview1WithConfig(&WASIConfig{})
 }
 
 // WASISnapshotPreview1WithConfig are functions importable as the module name wasi.ModuleSnapshotPreview1
-func WASISnapshotPreview1WithConfig(c *WASIConfig) *HostModuleConfig {
+func WASISnapshotPreview1WithConfig(c *WASIConfig) *Module {
 	// TODO: delete the internalwasi.Option types as they are not accessible as they are internal!
 	var opts []internalwasi.Option
 	if c.Stdin != nil {
@@ -72,10 +73,11 @@ func WASISnapshotPreview1WithConfig(c *WASIConfig) *HostModuleConfig {
 			opts = append(opts, internalwasi.Preopen(k, v))
 		}
 	}
-	return &HostModuleConfig{
-		Name:      wasi.ModuleSnapshotPreview1,
-		Functions: internalwasi.SnapshotPreview1Functions(opts...),
+	m, err := internalwasm.NewHostModule(wasi.ModuleSnapshotPreview1, internalwasi.SnapshotPreview1Functions(opts...))
+	if err != nil {
+		panic(fmt.Errorf("BUG: %w", err))
 	}
+	return &Module{name: wasi.ModuleSnapshotPreview1, module: m}
 }
 
 // StartWASICommandFromSource instantiates a module from the WebAssembly 1.0 (20191205) text or binary source or errs if
@@ -114,7 +116,7 @@ func StartWASICommandFromSource(r Runtime, source []byte) (wasm.Module, error) {
 // 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(r Runtime, module *DecodedModule) (wasm.Module, error) {
+func StartWASICommand(r Runtime, module *Module) (wasm.Module, error) {
 	if err := internalwasi.ValidateWASICommand(module.module, module.name); err != nil {
 		return nil, err
 	}
diff --git a/wasm.go b/wasm.go
index 1ac59443..92903302 100644
--- a/wasm.go
+++ b/wasm.go
@@ -15,10 +15,20 @@ import (
 // Ex.
 //	r := wazero.NewRuntime()
 //	decoded, _ := r.DecodeModule(source)
-//	module, _ := r.NewModule(decoded)
+//	module, _ := r.InstantiateModule(decoded)
 //
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/
 type Runtime interface {
+	// NewModuleBuilder lets you create modules out of functions defined in Go.
+	//
+	// Ex. Below defines and instantiates a module named "env" with one function:
+	//
+	//	hello := func() {
+	//		fmt.Fprintln(stdout, "hello!")
+	//	}
+	//	_, err := r.NewModuleBuilder("env").ExportFunction("hello", hello).Instantiate()
+	NewModuleBuilder(moduleName string) ModuleBuilder
+
 	// Module returns exports from an instantiated module or nil if there aren't any.
 	Module(moduleName string) wasm.Module
 
@@ -26,33 +36,27 @@ type Runtime interface {
 	//
 	// Note: the name defaults to what was decoded from the custom name section.
 	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0
-	DecodeModule(source []byte) (*DecodedModule, error)
+	DecodeModule(source []byte) (*Module, error)
 
-	// NewModuleFromSource instantiates a module from the WebAssembly 1.0 (20191205) text or binary source or errs if
-	// invalid.
+	// InstantiateModuleFromSource instantiates a module from the WebAssembly 1.0 (20191205) text or binary source or
+	// errs if invalid.
 	//
 	// Ex.
-	//	module, _ := wazero.NewRuntime().NewModuleFromSource(source)
+	//	module, _ := wazero.NewRuntime().InstantiateModuleFromSource(source)
 	//
-	// Note: This is a convenience utility that chains DecodeModule with NewModule. To instantiate the same source
-	// multiple times, use DecodeModule as NewModule avoids redundant decoding and/or compilation.
-	NewModuleFromSource(source []byte) (wasm.Module, error)
+	// Note: This is a convenience utility that chains DecodeModule with InstantiateModule. To instantiate the same source
+	// multiple times, use DecodeModule as InstantiateModule avoids redundant decoding and/or compilation.
+	InstantiateModuleFromSource(source []byte) (wasm.Module, error)
 
-	// NewModule instantiates the module namespace or errs if the configuration was invalid.
+	// InstantiateModule instantiates the module namespace or errs if the configuration was invalid.
 	//
 	// Ex.
 	//	r := wazero.NewRuntime()
 	//	decoded, _ := r.DecodeModule(source)
-	//	module, _ := r.NewModule(decoded)
+	//	module, _ := r.InstantiateModule(decoded)
 	//
 	// Note: The last value of RuntimeConfig.WithContext is used for any WebAssembly 1.0 (20191205) Start ExportedFunction.
-	NewModule(module *DecodedModule) (wasm.Module, error)
-
-	// NewHostModuleFromConfig instantiates the module namespace from the host or errs if the configuration was invalid.
-	//
-	// Ex.
-	//	module, _ := wazero.NewRuntime().NewHostModuleFromConfig(wazero.WASISnapshotPreview1())
-	NewHostModuleFromConfig(hostModule *HostModuleConfig) (wasm.Module, error)
+	InstantiateModule(module *Module) (wasm.Module, error)
 
 	// TODO: RemoveModule
 }
@@ -75,13 +79,13 @@ type runtime struct {
 	enabledFeatures internalwasm.Features
 }
 
-// Module implements wasm.Store Module
+// Module implements Runtime.Module
 func (r *runtime) Module(moduleName string) wasm.Module {
 	return r.store.Module(moduleName)
 }
 
 // DecodeModule implements Runtime.DecodeModule
-func (r *runtime) DecodeModule(source []byte) (*DecodedModule, error) {
+func (r *runtime) DecodeModule(source []byte) (*Module, error) {
 	if source == nil {
 		return nil, errors.New("source == nil")
 	}
@@ -107,7 +111,7 @@ func (r *runtime) DecodeModule(source []byte) (*DecodedModule, error) {
 		return nil, err
 	}
 
-	result := &DecodedModule{module: internal}
+	result := &Module{module: internal}
 	if internal.NameSection != nil {
 		result.name = internal.NameSection.ModuleName
 	}
@@ -115,25 +119,16 @@ func (r *runtime) DecodeModule(source []byte) (*DecodedModule, error) {
 	return result, nil
 }
 
-// NewModuleFromSource implements Runtime.NewModuleFromSource
-func (r *runtime) NewModuleFromSource(source []byte) (wasm.Module, error) {
+// InstantiateModuleFromSource implements Runtime.InstantiateModuleFromSource
+func (r *runtime) InstantiateModuleFromSource(source []byte) (wasm.Module, error) {
 	if decoded, err := r.DecodeModule(source); err != nil {
 		return nil, err
 	} else {
-		return r.NewModule(decoded)
+		return r.InstantiateModule(decoded)
 	}
 }
 
-// NewModule implements Runtime.NewModule
-func (r *runtime) NewModule(module *DecodedModule) (wasm.Module, error) {
+// InstantiateModule implements Runtime.InstantiateModule
+func (r *runtime) InstantiateModule(module *Module) (wasm.Module, error) {
 	return r.store.Instantiate(module.module, module.name)
 }
-
-// NewHostModuleFromConfig implements Runtime.NewHostModuleFromConfig
-func (r *runtime) NewHostModuleFromConfig(hostModule *HostModuleConfig) (wasm.Module, error) {
-	if m, err := internalwasm.NewHostModule(hostModule.Name, hostModule.Functions); err != nil {
-		return nil, err
-	} else {
-		return r.store.Instantiate(m, hostModule.Name)
-	}
-}
diff --git a/wasm/wasm.go b/wasm/wasm.go
index 92f49b8c..ab9ee4b4 100644
--- a/wasm/wasm.go
+++ b/wasm/wasm.go
@@ -89,7 +89,7 @@ type Module interface {
 	ExportedGlobal(name string) Global
 }
 
-// Function is a WebAssembly 1.0 (20191205) function exported from an instantiated module (wazero.Runtime NewModule).
+// Function is a WebAssembly 1.0 (20191205) function exported from an instantiated module (wazero.Runtime InstantiateModule).
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-func
 type Function interface {
 	// ParamTypes are the possibly empty sequence of value types accepted by a function with this signature.
@@ -122,7 +122,7 @@ type Function interface {
 	Call(ctx Module, params ...uint64) ([]uint64, error)
 }
 
-// Global is a WebAssembly 1.0 (20191205) global exported from an instantiated module (wazero.Runtime NewModule).
+// Global is a WebAssembly 1.0 (20191205) global exported from an instantiated module (wazero.Runtime InstantiateModule).
 //
 // Ex. If the value is not mutable, you can read it once:
 //


Test patch:
diff --git a/builder_test.go b/builder_test.go
new file mode 100644
index 00000000..e66a49d4
--- /dev/null
+++ b/builder_test.go
@@ -0,0 +1,208 @@
+package wazero
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	internalwasm "github.com/tetratelabs/wazero/internal/wasm"
+	"github.com/tetratelabs/wazero/wasm"
+)
+
+// TestNewModuleBuilder_Build only covers a few scenarios to avoid duplicating tests in internal/wasm/host_test.go
+func TestNewModuleBuilder_Build(t *testing.T) {
+	i32, i64 := wasm.ValueTypeI32, wasm.ValueTypeI64
+
+	uint32_uint32 := func(uint32) uint32 {
+		return 0
+	}
+	fnUint32_uint32 := reflect.ValueOf(uint32_uint32)
+	uint64_uint32 := func(uint64) uint32 {
+		return 0
+	}
+	fnUint64_uint32 := reflect.ValueOf(uint64_uint32)
+
+	tests := []struct {
+		name     string
+		input    func(Runtime) ModuleBuilder
+		expected *internalwasm.Module
+	}{
+		{
+			name: "empty",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("")
+			},
+			expected: &internalwasm.Module{},
+		},
+		{
+			name: "only name",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("env")
+			},
+			expected: &internalwasm.Module{NameSection: &internalwasm.NameSection{ModuleName: "env"}},
+		},
+		{
+			name: "ExportFunction",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportFunction("1", uint32_uint32)
+			},
+			expected: &internalwasm.Module{
+				TypeSection: []*internalwasm.FunctionType{
+					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}},
+				},
+				FunctionSection:     []internalwasm.Index{0},
+				HostFunctionSection: []*reflect.Value{&fnUint32_uint32},
+				ExportSection: map[string]*internalwasm.Export{
+					"1": {Name: "1", Type: internalwasm.ExternTypeFunc, Index: 0},
+				},
+				NameSection: &internalwasm.NameSection{
+					FunctionNames: internalwasm.NameMap{{Index: 0, Name: "1"}},
+				},
+			},
+		},
+		{
+			name: "ExportFunction overwrites existing",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportFunction("1", uint32_uint32).ExportFunction("1", uint64_uint32)
+			},
+			expected: &internalwasm.Module{
+				TypeSection: []*internalwasm.FunctionType{
+					{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32}},
+				},
+				FunctionSection:     []internalwasm.Index{0},
+				HostFunctionSection: []*reflect.Value{&fnUint64_uint32},
+				ExportSection: map[string]*internalwasm.Export{
+					"1": {Name: "1", Type: internalwasm.ExternTypeFunc, Index: 0},
+				},
+				NameSection: &internalwasm.NameSection{
+					FunctionNames: internalwasm.NameMap{{Index: 0, Name: "1"}},
+				},
+			},
+		},
+		{
+			name: "ExportFunction twice",
+			input: func(r Runtime) ModuleBuilder {
+				// Intentionally out of order
+				return r.NewModuleBuilder("").ExportFunction("2", uint64_uint32).ExportFunction("1", uint32_uint32)
+			},
+			expected: &internalwasm.Module{
+				TypeSection: []*internalwasm.FunctionType{
+					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}},
+					{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32}},
+				},
+				FunctionSection:     []internalwasm.Index{0, 1},
+				HostFunctionSection: []*reflect.Value{&fnUint32_uint32, &fnUint64_uint32},
+				ExportSection: map[string]*internalwasm.Export{
+					"1": {Name: "1", Type: internalwasm.ExternTypeFunc, Index: 0},
+					"2": {Name: "2", Type: internalwasm.ExternTypeFunc, Index: 1},
+				},
+				NameSection: &internalwasm.NameSection{
+					FunctionNames: internalwasm.NameMap{{Index: 0, Name: "1"}, {Index: 1, Name: "2"}},
+				},
+			},
+		},
+		{
+			name: "ExportFunctions",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportFunctions(map[string]interface{}{
+					"1": uint32_uint32,
+					"2": uint64_uint32,
+				})
+			},
+			expected: &internalwasm.Module{
+				TypeSection: []*internalwasm.FunctionType{
+					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}},
+					{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32}},
+				},
+				FunctionSection:     []internalwasm.Index{0, 1},
+				HostFunctionSection: []*reflect.Value{&fnUint32_uint32, &fnUint64_uint32},
+				ExportSection: map[string]*internalwasm.Export{
+					"1": {Name: "1", Type: internalwasm.ExternTypeFunc, Index: 0},
+					"2": {Name: "2", Type: internalwasm.ExternTypeFunc, Index: 1},
+				},
+				NameSection: &internalwasm.NameSection{
+					FunctionNames: internalwasm.NameMap{{Index: 0, Name: "1"}, {Index: 1, Name: "2"}},
+				},
+			},
+		},
+		{
+			name: "ExportFunctions overwrites",
+			input: func(r Runtime) ModuleBuilder {
+				b := r.NewModuleBuilder("").ExportFunction("1", uint64_uint32)
+				return b.ExportFunctions(map[string]interface{}{
+					"1": uint32_uint32,
+					"2": uint64_uint32,
+				})
+			},
+			expected: &internalwasm.Module{
+				TypeSection: []*internalwasm.FunctionType{
+					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}},
+					{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32}},
+				},
+				FunctionSection:     []internalwasm.Index{0, 1},
+				HostFunctionSection: []*reflect.Value{&fnUint32_uint32, &fnUint64_uint32},
+				ExportSection: map[string]*internalwasm.Export{
+					"1": {Name: "1", Type: internalwasm.ExternTypeFunc, Index: 0},
+					"2": {Name: "2", Type: internalwasm.ExternTypeFunc, Index: 1},
+				},
+				NameSection: &internalwasm.NameSection{
+					FunctionNames: internalwasm.NameMap{{Index: 0, Name: "1"}, {Index: 1, Name: "2"}},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			m, e := tc.input(NewRuntime()).Build()
+			require.NoError(t, e)
+			requireHostModuleEquals(t, tc.expected, m.module)
+		})
+	}
+}
+
+// TestNewModuleBuilder_InstantiateModule ensures Runtime.InstantiateModule is called on success.
+func TestNewModuleBuilder_InstantiateModule(t *testing.T) {
+	r := NewRuntime()
+	m, err := r.NewModuleBuilder("env").Instantiate()
+	require.NoError(t, err)
+
+	// If this was instantiated, it would be added to the store under the same name
+	require.Equal(t, r.(*runtime).store.Module("env"), m)
+}
+
+// TestNewModuleBuilder_InstantiateModule_Errors ensures errors propagate from Runtime.InstantiateModule
+func TestNewModuleBuilder_InstantiateModule_Errors(t *testing.T) {
+	r := NewRuntime()
+	_, err := r.NewModuleBuilder("env").Instantiate()
+	require.NoError(t, err)
+
+	_, err = r.NewModuleBuilder("env").Instantiate()
+	require.EqualError(t, err, "module env has already been instantiated")
+}
+
+// requireHostModuleEquals is redefined from internal/wasm/host_test.go to avoid an import cycle extracting it.
+func requireHostModuleEquals(t *testing.T, expected, actual *internalwasm.Module) {
+	// `require.Equal(t, expected, actual)` fails reflect pointers don't match, so brute compare:
+	require.Equal(t, expected.TypeSection, actual.TypeSection)
+	require.Equal(t, expected.ImportSection, actual.ImportSection)
+	require.Equal(t, expected.FunctionSection, actual.FunctionSection)
+	require.Equal(t, expected.TableSection, actual.TableSection)
+	require.Equal(t, expected.MemorySection, actual.MemorySection)
+	require.Equal(t, expected.GlobalSection, actual.GlobalSection)
+	require.Equal(t, expected.ExportSection, actual.ExportSection)
+	require.Equal(t, expected.StartSection, actual.StartSection)
+	require.Equal(t, expected.ElementSection, actual.ElementSection)
+	require.Nil(t, actual.CodeSection) // Host functions are implemented in Go, not Wasm!
+	require.Equal(t, expected.DataSection, actual.DataSection)
+	require.Equal(t, expected.NameSection, actual.NameSection)
+
+	// Special case because reflect.Value can't be compared with Equals
+	require.Equal(t, len(expected.HostFunctionSection), len(actual.HostFunctionSection))
+	for i := range expected.HostFunctionSection {
+		require.Equal(t, (*expected.HostFunctionSection[i]).Type(), (*actual.HostFunctionSection[i]).Type())
+	}
+}
diff --git a/examples/add_test.go b/examples/add_test.go
index 80fb2349..733ef775 100644
--- a/examples/add_test.go
+++ b/examples/add_test.go
@@ -11,7 +11,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) {
-	module, err := wazero.NewRuntime().NewModuleFromSource([]byte(`(module $test
+	module, err := wazero.NewRuntime().InstantiateModuleFromSource([]byte(`(module $test
     (func $addInt ;; TODO: function module (export "AddInt")
         (param $value_1 i32) (param $value_2 i32)
         (result i32)
diff --git a/examples/fibonacci_test.go b/examples/fibonacci_test.go
index a5a1c6bc..0bf5b78f 100644
--- a/examples/fibonacci_test.go
+++ b/examples/fibonacci_test.go
@@ -17,7 +17,7 @@ func Test_fibonacci(t *testing.T) {
 	r := wazero.NewRuntime()
 
 	// Note: fibonacci.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command.
-	_, err := r.NewHostModuleFromConfig(wazero.WASISnapshotPreview1())
+	_, err := r.InstantiateModule(wazero.WASISnapshotPreview1())
 	require.NoError(t, err)
 
 	module, err := wazero.StartWASICommandFromSource(r, fibWasm)
diff --git a/examples/file_system_test.go b/examples/file_system_test.go
index 125eda47..37ef87fe 100644
--- a/examples/file_system_test.go
+++ b/examples/file_system_test.go
@@ -52,7 +52,7 @@ func Test_file_system(t *testing.T) {
 	require.NoError(t, err)
 
 	wasiConfig := &wazero.WASIConfig{Preopens: map[string]wasi.FS{".": memFS}}
-	_, err = r.NewHostModuleFromConfig(wazero.WASISnapshotPreview1WithConfig(wasiConfig))
+	_, err = r.InstantiateModule(wazero.WASISnapshotPreview1WithConfig(wasiConfig))
 	require.NoError(t, err)
 
 	// Note: TinyGo binaries must be treated as WASI Commands to initialize memory.
diff --git a/examples/host_func_test.go b/examples/host_func_test.go
index eee7f79e..1dd60f31 100644
--- a/examples/host_func_test.go
+++ b/examples/host_func_test.go
@@ -56,13 +56,12 @@ func Test_hostFunc(t *testing.T) {
 
 	r := wazero.NewRuntime()
 
-	env := &wazero.HostModuleConfig{Name: "env", Functions: map[string]interface{}{"get_random_bytes": getRandomBytes}}
-	_, err := r.NewHostModuleFromConfig(env)
+	_, err := r.NewModuleBuilder("env").ExportFunction("get_random_bytes", getRandomBytes).Instantiate()
 	require.NoError(t, err)
 
 	// Note: host_func.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command.
 	stdout := bytes.NewBuffer(nil)
-	_, err = r.NewHostModuleFromConfig(wazero.WASISnapshotPreview1WithConfig(&wazero.WASIConfig{Stdout: stdout}))
+	_, err = r.InstantiateModule(wazero.WASISnapshotPreview1WithConfig(&wazero.WASIConfig{Stdout: stdout}))
 	require.NoError(t, err)
 
 	module, err := wazero.StartWASICommandFromSource(r, hostFuncWasm)
diff --git a/examples/simple_test.go b/examples/simple_test.go
index c824e065..20b7e6c9 100644
--- a/examples/simple_test.go
+++ b/examples/simple_test.go
@@ -13,20 +13,19 @@ import (
 // Test_Simple implements a basic function in go: hello. This is imported as the Wasm name "$hello" and run on start.
 func Test_Simple(t *testing.T) {
 	stdout := new(bytes.Buffer)
-	goFunc := func() {
+	hello := func() {
 		_, _ = fmt.Fprintln(stdout, "hello!")
 	}
 
 	r := wazero.NewRuntime()
 
 	// Host functions can be exported as any module name, including the empty string.
-	env := &wazero.HostModuleConfig{Name: "", Functions: map[string]interface{}{"hello": goFunc}}
-	_, err := r.NewHostModuleFromConfig(env)
+	_, err := r.NewModuleBuilder("").ExportFunction("hello", hello).Instantiate()
 	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 = r.NewModuleFromSource([]byte(`(module $test
+	_, err = r.InstantiateModuleFromSource([]byte(`(module $test
 	(import "" "hello" (func $hello))
 	(start $hello)
 )`))
diff --git a/examples/stdio_test.go b/examples/stdio_test.go
index 746ccfb2..22158ae2 100644
--- a/examples/stdio_test.go
+++ b/examples/stdio_test.go
@@ -24,7 +24,7 @@ func Test_stdio(t *testing.T) {
 
 	// Configure WASI host functions with the IO buffers
 	wasiConfig := &wazero.WASIConfig{Stdin: stdinBuf, Stdout: stdoutBuf, Stderr: stderrBuf}
-	_, err := r.NewHostModuleFromConfig(wazero.WASISnapshotPreview1WithConfig(wasiConfig))
+	_, err := r.InstantiateModule(wazero.WASISnapshotPreview1WithConfig(wasiConfig))
 	require.NoError(t, err)
 
 	// StartWASICommand runs the "_start" function which is what TinyGo compiles "main" to
diff --git a/examples/wasi_test.go b/examples/wasi_test.go
index 92c52041..2dbc2c95 100644
--- a/examples/wasi_test.go
+++ b/examples/wasi_test.go
@@ -19,7 +19,7 @@ func Test_WASI(t *testing.T) {
 	}
 
 	stdout := new(bytes.Buffer)
-	goFunc := func(ctx wasm.Module) {
+	random := func(ctx wasm.Module) {
 		// Write 8 random bytes to memory using WASI.
 		errno := randomGet(ctx, 0, 8)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
@@ -33,11 +33,11 @@ func Test_WASI(t *testing.T) {
 	r := wazero.NewRuntime()
 
 	// Host functions can be exported as any module name, including the empty string.
-	env := &wazero.HostModuleConfig{Name: "", Functions: map[string]interface{}{"random": goFunc}}
-	_, err := r.NewHostModuleFromConfig(env)
+	_, err := r.NewModuleBuilder("").ExportFunction("random", random).Instantiate()
+	require.NoError(t, err)
 
 	// Configure WASI and implement the function to use it
-	we, err := r.NewHostModuleFromConfig(wazero.WASISnapshotPreview1())
+	we, err := r.InstantiateModule(wazero.WASISnapshotPreview1())
 	require.NoError(t, err)
 	randomGetFn := we.ExportedFunction("random_get")
 
@@ -50,7 +50,7 @@ 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 = r.NewModuleFromSource([]byte(`(module $wasi
+	_, err = r.InstantiateModuleFromSource([]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))
diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go
index 029d3599..e1ee7ff4 100644
--- a/internal/wasm/host_test.go
+++ b/internal/wasm/host_test.go
@@ -80,30 +80,33 @@ func TestNewHostModule(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			m, e := NewHostModule(tc.moduleName, tc.goFuncs)
 			require.NoError(t, e)
-
-			// `require.Equal(t, tc.expected, m)` fails reflect pointers don't match, so brute compare:
-			require.Equal(t, tc.expected.TypeSection, m.TypeSection)
-			require.Equal(t, tc.expected.ImportSection, m.ImportSection)
-			require.Equal(t, tc.expected.FunctionSection, m.FunctionSection)
-			require.Equal(t, tc.expected.TableSection, m.TableSection)
-			require.Equal(t, tc.expected.MemorySection, m.MemorySection)
-			require.Equal(t, tc.expected.GlobalSection, m.GlobalSection)
-			require.Equal(t, tc.expected.ExportSection, m.ExportSection)
-			require.Equal(t, tc.expected.StartSection, m.StartSection)
-			require.Equal(t, tc.expected.ElementSection, m.ElementSection)
-			require.Nil(t, m.CodeSection) // Host functions are implemented in Go, not Wasm!
-			require.Equal(t, tc.expected.DataSection, m.DataSection)
-			require.Equal(t, tc.expected.NameSection, m.NameSection)
-
-			// Special case because reflect.Value can't be compared with Equals
-			require.Equal(t, len(tc.expected.HostFunctionSection), len(m.HostFunctionSection))
-			for i := range tc.expected.HostFunctionSection {
-				require.Equal(t, (*tc.expected.HostFunctionSection[i]).Type(), (*m.HostFunctionSection[i]).Type())
-			}
+			requireHostModuleEquals(t, tc.expected, m)
 		})
 	}
 }
 
+func requireHostModuleEquals(t *testing.T, expected, actual *Module) {
+	// `require.Equal(t, expected, actual)` fails reflect pointers don't match, so brute compare:
+	require.Equal(t, expected.TypeSection, actual.TypeSection)
+	require.Equal(t, expected.ImportSection, actual.ImportSection)
+	require.Equal(t, expected.FunctionSection, actual.FunctionSection)
+	require.Equal(t, expected.TableSection, actual.TableSection)
+	require.Equal(t, expected.MemorySection, actual.MemorySection)
+	require.Equal(t, expected.GlobalSection, actual.GlobalSection)
+	require.Equal(t, expected.ExportSection, actual.ExportSection)
+	require.Equal(t, expected.StartSection, actual.StartSection)
+	require.Equal(t, expected.ElementSection, actual.ElementSection)
+	require.Nil(t, actual.CodeSection) // Host functions are implemented in Go, not Wasm!
+	require.Equal(t, expected.DataSection, actual.DataSection)
+	require.Equal(t, expected.NameSection, actual.NameSection)
+
+	// Special case because reflect.Value can't be compared with Equals
+	require.Equal(t, len(expected.HostFunctionSection), len(actual.HostFunctionSection))
+	for i := range expected.HostFunctionSection {
+		require.Equal(t, (*expected.HostFunctionSection[i]).Type(), (*actual.HostFunctionSection[i]).Type())
+	}
+}
+
 func TestNewHostModule_Errors(t *testing.T) {
 	t.Run("Adds export name to error message", func(t *testing.T) {
 		_, err := NewHostModule("test", map[string]interface{}{"fn": "hello"})
diff --git a/tests/bench/bench_test.go b/tests/bench/bench_test.go
index cab9e787..d85f2483 100644
--- a/tests/bench/bench_test.go
+++ b/tests/bench/bench_test.go
@@ -131,15 +131,14 @@ func instantiateHostFunctionModuleWithEngine(b *testing.B, engine *wazero.Runtim
 
 	r := wazero.NewRuntimeWithConfig(engine)
 
-	env := &wazero.HostModuleConfig{Name: "env", Functions: map[string]interface{}{"get_random_string": getRandomString}}
-	_, err := r.NewHostModuleFromConfig(env)
+	_, err := r.NewModuleBuilder("env").ExportFunction("get_random_string", getRandomString).Instantiate()
 	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.
 	// Add WASI to satisfy import tests
-	_, err = r.NewHostModuleFromConfig(wazero.WASISnapshotPreview1())
+	_, err = r.InstantiateModule(wazero.WASISnapshotPreview1())
 	if err != nil {
 		b.Fatal(err)
 	}
diff --git a/tests/engine/adhoc_test.go b/tests/engine/adhoc_test.go
index 7f043af3..d4c5e02a 100644
--- a/tests/engine/adhoc_test.go
+++ b/tests/engine/adhoc_test.go
@@ -69,7 +69,7 @@ func runAdhocTests(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig)
 
 func testHugeStack(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig) {
 	r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
-	module, err := r.NewModuleFromSource(hugestackWasm)
+	module, err := r.InstantiateModuleFromSource(hugestackWasm)
 	require.NoError(t, err)
 
 	fn := module.ExportedFunction("main")
@@ -81,7 +81,7 @@ func testHugeStack(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig)
 
 func testFibonacci(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig) {
 	r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
-	module, err := r.NewModuleFromSource(fibWasm)
+	module, err := r.InstantiateModuleFromSource(fibWasm)
 	require.NoError(t, err)
 
 	fib := module.ExportedFunction("fib")
@@ -94,7 +94,7 @@ func testFibonacci(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig)
 
 func testFac(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig) {
 	r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
-	module, err := r.NewModuleFromSource(facWasm)
+	module, err := r.InstantiateModuleFromSource(facWasm)
 	require.NoError(t, err)
 	for _, name := range []string{
 		"fac-rec",
@@ -127,11 +127,10 @@ func testUnreachable(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig
 
 	r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
 
-	hostModule := &wazero.HostModuleConfig{Name: "host", Functions: map[string]interface{}{"cause_unreachable": callUnreachable}}
-	_, err := r.NewHostModuleFromConfig(hostModule)
+	_, err := r.NewModuleBuilder("host").ExportFunction("cause_unreachable", callUnreachable).Instantiate()
 	require.NoError(t, err)
 
-	module, err := r.NewModuleFromSource(unreachableWasm)
+	module, err := r.InstantiateModuleFromSource(unreachableWasm)
 	require.NoError(t, err)
 
 	_, err = module.ExportedFunction("main").Call(nil)
@@ -146,7 +145,7 @@ wasm backtrace:
 
 func testMemory(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig) {
 	r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
-	module, err := r.NewModuleFromSource(memoryWasm)
+	module, err := r.InstantiateModuleFromSource(memoryWasm)
 	require.NoError(t, err)
 
 	size := module.ExportedFunction("size")
@@ -185,11 +184,10 @@ func testRecursiveEntry(t *testing.T, newRuntimeConfig func() *wazero.RuntimeCon
 
 	r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
 
-	hostModule := &wazero.HostModuleConfig{Name: "env", Functions: map[string]interface{}{"host_func": hostfunc}}
-	_, err := r.NewHostModuleFromConfig(hostModule)
+	_, err := r.NewModuleBuilder("env").ExportFunction("host_func", hostfunc).Instantiate()
 	require.NoError(t, err)
 
-	module, err := r.NewModuleFromSource(recursiveWasm)
+	module, err := r.InstantiateModuleFromSource(recursiveWasm)
 	require.NoError(t, err)
 
 	_, err = module.ExportedFunction("main").Call(nil, 1)
@@ -211,11 +209,10 @@ func testImportedAndExportedFunc(t *testing.T, newRuntimeConfig func() *wazero.R
 
 	r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
 
-	hostModule := &wazero.HostModuleConfig{Name: "", Functions: map[string]interface{}{"store_int": storeInt}}
-	_, err := r.NewHostModuleFromConfig(hostModule)
+	_, err := r.NewModuleBuilder("").ExportFunction("store_int", storeInt).Instantiate()
 	require.NoError(t, err)
 
-	module, err := r.NewModuleFromSource([]byte(`(module $test
+	module, err := r.InstantiateModuleFromSource([]byte(`(module $test
 		(import "" "store_int"
 			(func $store_int (param $offset i32) (param $val i64) (result (;errno;) i32)))
 		(memory $memory 1 1)
@@ -279,11 +276,10 @@ func testHostFunctions(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConf
 	} {
 		r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
 
-		hostModule := &wazero.HostModuleConfig{Name: "host", Functions: v}
-		_, err := r.NewHostModuleFromConfig(hostModule)
+		_, err := r.NewModuleBuilder("host").ExportFunctions(v).Instantiate()
 		require.NoError(t, err)
 
-		m, err = r.NewModuleFromSource([]byte(`(module $test
+		m, err = r.InstantiateModuleFromSource([]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)))
diff --git a/tests/post1_0/post1_0_test.go b/tests/post1_0/post1_0_test.go
index 040d6be5..ea8fc547 100644
--- a/tests/post1_0/post1_0_test.go
+++ b/tests/post1_0/post1_0_test.go
@@ -56,12 +56,12 @@ func testSignExtensionOps(t *testing.T, newRuntimeConfig func() *wazero.RuntimeC
 	t.Run("disabled", func(t *testing.T) {
 		// Sign-extension is disabled by default.
 		r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
-		_, err := r.NewModuleFromSource(signExtend)
+		_, err := r.InstantiateModuleFromSource(signExtend)
 		require.Error(t, err)
 	})
 	t.Run("enabled", func(t *testing.T) {
 		r := wazero.NewRuntimeWithConfig(newRuntimeConfig().WithFeatureSignExtensionOps(true))
-		module, err := r.NewModuleFromSource(signExtend)
+		module, err := r.InstantiateModuleFromSource(signExtend)
 		require.NoError(t, err)
 
 		signExtend32from8Name, signExtend32from16Name := "i32.extend8_s", "i32.extend16_s"
diff --git a/vs/bench_fac_iter_test.go b/vs/bench_fac_iter_test.go
index f4565f91..cab83c19 100644
--- a/vs/bench_fac_iter_test.go
+++ b/vs/bench_fac_iter_test.go
@@ -243,7 +243,7 @@ func wasmtimeGoFacIterInvoke(b *testing.B) {
 func newWazeroFacIterBench(engine *wazero.RuntimeConfig) (wasm.Function, error) {
 	r := wazero.NewRuntimeWithConfig(engine)
 
-	m, err := r.NewModuleFromSource(facWasm)
+	m, err := r.InstantiateModuleFromSource(facWasm)
 	if err != nil {
 		return nil, err
 	}
diff --git a/vs/codec_test.go b/vs/codec_test.go
index ac6e5d33..21e5200e 100644
--- a/vs/codec_test.go
+++ b/vs/codec_test.go
@@ -108,11 +108,11 @@ func TestExampleUpToDate(t *testing.T) {
 		r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig().WithFeatureSignExtensionOps(true))
 
 		// Add WASI to satisfy import tests
-		_, err := r.NewHostModuleFromConfig(wazero.WASISnapshotPreview1())
+		_, err := r.InstantiateModule(wazero.WASISnapshotPreview1())
 		require.NoError(t, err)
 
 		// Decode and instantiate the module
-		module, err := r.NewModuleFromSource(exampleBinary)
+		module, err := r.InstantiateModuleFromSource(exampleBinary)
 		require.NoError(t, err)
 
 		// Call the add function as a smoke test
diff --git a/wasi_test.go b/wasi_test.go
index e5acbe9c..9b8096c1 100644
--- a/wasi_test.go
+++ b/wasi_test.go
@@ -21,10 +21,10 @@ func TestStartWASICommand_UsesStoreContext(t *testing.T) {
 		require.Equal(t, config.ctx, ctx.Context())
 	}
 
-	_, err := r.NewHostModuleFromConfig(&HostModuleConfig{Functions: map[string]interface{}{"start": start}})
+	_, err := r.NewModuleBuilder("").ExportFunction("start", start).Instantiate()
 	require.NoError(t, err)
 
-	_, err = r.NewHostModuleFromConfig(WASISnapshotPreview1())
+	_, err = r.InstantiateModule(WASISnapshotPreview1())
 	require.NoError(t, err)
 
 	decoded, err := r.DecodeModule([]byte(`(module $wasi_test.go
diff --git a/wasm_test.go b/wasm_test.go
index 7d2e7213..638410c5 100644
--- a/wasm_test.go
+++ b/wasm_test.go
@@ -103,12 +103,12 @@ func TestDecodedModule_WithName(t *testing.T) {
 
 	// Use the same runtime to instantiate multiple modules
 	internal := r.(*runtime).store
-	m1, err := r.NewModule(base.WithName("1"))
+	m1, err := r.InstantiateModule(base.WithName("1"))
 	require.NoError(t, err)
 	require.Nil(t, internal.Module("0"))
 	require.Equal(t, internal.Module("1"), m1)
 
-	m2, err := r.NewModule(base.WithName("2"))
+	m2, err := r.InstantiateModule(base.WithName("2"))
 	require.NoError(t, err)
 	require.Nil(t, internal.Module("0"))
 	require.Equal(t, internal.Module("2"), m2)
@@ -142,7 +142,7 @@ func TestModule_Memory(t *testing.T) {
 			require.NoError(t, err)
 
 			// Instantiate the module and get the export of the above hostFn
-			module, err := r.NewModule(decoded)
+			module, err := r.InstantiateModule(decoded)
 			require.NoError(t, err)
 
 			mem := module.ExportedMemory("memory")
@@ -216,7 +216,7 @@ func TestModule_Global(t *testing.T) {
 		r := NewRuntime()
 		t.Run(tc.name, func(t *testing.T) {
 			// Instantiate the module and get the export of the above global
-			module, err := r.NewModule(&DecodedModule{module: tc.module})
+			module, err := r.InstantiateModule(&Module{module: tc.module})
 			require.NoError(t, err)
 
 			global := module.ExportedGlobal("global")
@@ -279,7 +279,7 @@ func TestFunction_Context(t *testing.T) {
 			decoded, err := r.DecodeModule(source)
 			require.NoError(t, err)
 
-			module, err := r.NewModule(decoded)
+			module, err := r.InstantiateModule(decoded)
 			require.NoError(t, err)
 
 			// This fails if the function wasn't invoked, or had an unexpected context.
@@ -303,7 +303,7 @@ func TestRuntime_NewModule_UsesStoreContext(t *testing.T) {
 		require.Equal(t, runtimeCtx, ctx.Context())
 	}
 
-	_, err := r.NewHostModuleFromConfig(&HostModuleConfig{Functions: map[string]interface{}{"start": start}})
+	_, err := r.NewModuleBuilder("").ExportFunction("start", start).Instantiate()
 	require.NoError(t, err)
 
 	decoded, err := r.DecodeModule([]byte(`(module $runtime_test.go
@@ -313,16 +313,14 @@ func TestRuntime_NewModule_UsesStoreContext(t *testing.T) {
 	require.NoError(t, err)
 
 	// Instantiate the module, which calls the start function. This will fail if the context wasn't as intended.
-	_, err = r.NewModule(decoded)
+	_, err = r.InstantiateModule(decoded)
 	require.NoError(t, err)
 	require.True(t, calledStart)
 }
 
-// requireImportAndExportFunction re-module a host function because only host functions can see the propagated context.
+// requireImportAndExportFunction re-exports a host function because only host functions can see the propagated context.
 func requireImportAndExportFunction(t *testing.T, r Runtime, hostFn func(ctx wasm.Module) uint64, functionName string) []byte {
-	_, err := r.NewHostModuleFromConfig(&HostModuleConfig{
-		Name: "host", Functions: map[string]interface{}{functionName: hostFn},
-	})
+	_, err := r.NewModuleBuilder("host").ExportFunction(functionName, hostFn).Instantiate()
 	require.NoError(t, err)
 
 	return []byte(fmt.Sprintf(