Repository: tetratelabs/wazero
Pull request #1370 (2023-04-17 16:00:26)

Makes host function index insertion order
This makes host functions index consistently on insertion order, rather than lexicographic order. This helps with ABI such as Emscripten, which need an expected order.

This also constrains the internal code around host functions to only one export name. More than one was never used. By restricting this, logic is simpler and smaller.


Patch:
diff --git a/builder.go b/builder.go
index 82eecd30..caf08d3c 100644
--- a/builder.go
+++ b/builder.go
@@ -146,7 +146,7 @@ type HostFunctionBuilder interface {
 //	defer r.Close(ctx) // This closes everything this Runtime created.
 //
 //	hello := func() {
-//		fmt.Fprintln(stdout, "hello!")
+//		println("hello!")
 //	}
 //	env, _ := r.NewHostModuleBuilder("env").
 //		NewFunctionBuilder().WithFunc(hello).Export("hello").
@@ -170,8 +170,8 @@ type HostFunctionBuilder interface {
 //     chaining.
 //   - methods do not return errors, to allow chaining. Any validation errors
 //     are deferred until Compile.
-//   - Insertion order is not retained. Anything defined by this builder is
-//     sorted lexicographically on Compile.
+//   - Functions are indexed in order of calls to NewFunctionBuilder as
+//     insertion ordering is needed by ABI such as Emscripten (invoke_*).
 type HostModuleBuilder interface {
 	// Note: until golang/go#5860, we can't use example tests to embed code in interface godocs.
 
@@ -191,7 +191,7 @@ type HostModuleBuilder interface {
 	//	defer r.Close(ctx) // This closes everything this Runtime created.
 	//
 	//	hello := func() {
-	//		fmt.Fprintln(stdout, "hello!")
+	//		println("hello!")
 	//	}
 	//	env, _ := r.NewHostModuleBuilder("env").
 	//		NewFunctionBuilder().WithFunc(hello).Export("hello").
@@ -207,19 +207,18 @@ type HostModuleBuilder interface {
 
 // hostModuleBuilder implements HostModuleBuilder
 type hostModuleBuilder struct {
-	r            *runtime
-	moduleName   string
-	nameToGoFunc map[string]interface{}
-	funcToNames  map[string]*wasm.HostFuncNames
+	r              *runtime
+	moduleName     string
+	exportNames    []string
+	nameToHostFunc map[string]*wasm.HostFunc
 }
 
 // NewHostModuleBuilder implements Runtime.NewHostModuleBuilder
 func (r *runtime) NewHostModuleBuilder(moduleName string) HostModuleBuilder {
 	return &hostModuleBuilder{
-		r:            r,
-		moduleName:   moduleName,
-		nameToGoFunc: map[string]interface{}{},
-		funcToNames:  map[string]*wasm.HostFuncNames{},
+		r:              r,
+		moduleName:     moduleName,
+		nameToHostFunc: map[string]*wasm.HostFunc{},
 	}
 }
 
@@ -234,21 +233,13 @@ type hostFunctionBuilder struct {
 
 // WithGoFunction implements HostFunctionBuilder.WithGoFunction
 func (h *hostFunctionBuilder) WithGoFunction(fn api.GoFunction, params, results []api.ValueType) HostFunctionBuilder {
-	h.fn = &wasm.HostFunc{
-		ParamTypes:  params,
-		ResultTypes: results,
-		Code:        wasm.Code{GoFunc: fn},
-	}
+	h.fn = &wasm.HostFunc{ParamTypes: params, ResultTypes: results, Code: wasm.Code{GoFunc: fn}}
 	return h
 }
 
 // WithGoModuleFunction implements HostFunctionBuilder.WithGoModuleFunction
 func (h *hostFunctionBuilder) WithGoModuleFunction(fn api.GoModuleFunction, params, results []api.ValueType) HostFunctionBuilder {
-	h.fn = &wasm.HostFunc{
-		ParamTypes:  params,
-		ResultTypes: results,
-		Code:        wasm.Code{GoFunc: fn},
-	}
+	h.fn = &wasm.HostFunc{ParamTypes: params, ResultTypes: results, Code: wasm.Code{GoFunc: fn}}
 	return h
 }
 
@@ -278,30 +269,35 @@ func (h *hostFunctionBuilder) WithResultNames(names ...string) HostFunctionBuild
 
 // Export implements HostFunctionBuilder.Export
 func (h *hostFunctionBuilder) Export(exportName string) HostModuleBuilder {
-	if h.name == "" {
-		h.name = exportName
+	var hostFn *wasm.HostFunc
+	if fn, ok := h.fn.(*wasm.HostFunc); ok {
+		hostFn = fn
+	} else {
+		hostFn = &wasm.HostFunc{Code: wasm.Code{GoFunc: h.fn}}
 	}
-	names := &wasm.HostFuncNames{
-		Name:        h.name,
-		ParamNames:  h.paramNames,
-		ResultNames: h.resultNames,
+
+	// Assign any names from the builder
+	hostFn.ExportName = exportName
+	if h.name != "" {
+		hostFn.Name = h.name
 	}
-	if fn, ok := h.fn.(*wasm.HostFunc); ok {
-		if fn.Name == "" {
-			fn.Name = names.Name
-		}
-		fn.ParamNames = names.ParamNames
-		fn.ResultNames = names.ResultNames
-		fn.ExportNames = []string{exportName}
+	if len(h.paramNames) != 0 {
+		hostFn.ParamNames = h.paramNames
+	}
+	if len(h.resultNames) != 0 {
+		hostFn.ResultNames = h.resultNames
 	}
-	h.b.nameToGoFunc[exportName] = h.fn
-	h.b.funcToNames[exportName] = names
+
+	h.b.ExportHostFunc(hostFn)
 	return h.b
 }
 
 // ExportHostFunc implements wasm.HostFuncExporter
 func (b *hostModuleBuilder) ExportHostFunc(fn *wasm.HostFunc) {
-	b.nameToGoFunc[fn.ExportNames[0]] = fn
+	if _, ok := b.nameToHostFunc[fn.ExportName]; !ok { // add a new name
+		b.exportNames = append(b.exportNames, fn.ExportName)
+	}
+	b.nameToHostFunc[fn.ExportName] = fn
 }
 
 // NewFunctionBuilder implements HostModuleBuilder.NewFunctionBuilder
@@ -311,7 +307,7 @@ func (b *hostModuleBuilder) NewFunctionBuilder() HostFunctionBuilder {
 
 // Compile implements HostModuleBuilder.Compile
 func (b *hostModuleBuilder) Compile(ctx context.Context) (CompiledModule, error) {
-	module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.funcToNames, b.r.enabledFeatures)
+	module, err := wasm.NewHostModule(b.moduleName, b.exportNames, b.nameToHostFunc, b.r.enabledFeatures)
 	if err != nil {
 		return nil, err
 	} else if err = module.Validate(b.r.enabledFeatures); err != nil {
diff --git a/imports/assemblyscript/assemblyscript.go b/imports/assemblyscript/assemblyscript.go
index 92226ce6..19b465b8 100644
--- a/imports/assemblyscript/assemblyscript.go
+++ b/imports/assemblyscript/assemblyscript.go
@@ -138,11 +138,11 @@ func (e *functionExporter) ExportFunctions(builder wazero.HostModuleBuilder) {
 //
 // See https://github.com/AssemblyScript/assemblyscript/blob/v0.26.7/std/assembly/builtins.ts#L2508
 var abortMessageEnabled = &wasm.HostFunc{
-	ExportNames: []string{AbortName},
-	Name:        "~lib/builtins/abort",
-	ParamTypes:  []api.ValueType{i32, i32, i32, i32},
-	ParamNames:  []string{"message", "fileName", "lineNumber", "columnNumber"},
-	Code:        wasm.Code{GoFunc: api.GoModuleFunc(abortWithMessage)},
+	ExportName: AbortName,
+	Name:       "~lib/builtins/abort",
+	ParamTypes: []api.ValueType{i32, i32, i32, i32},
+	ParamNames: []string{"message", "fileName", "lineNumber", "columnNumber"},
+	Code:       wasm.Code{GoFunc: api.GoModuleFunc(abortWithMessage)},
 }
 
 var abortMessageDisabled = abortMessageEnabled.WithGoModuleFunc(abort)
@@ -185,10 +185,10 @@ var traceDisabled = traceStdout.WithGoModuleFunc(func(context.Context, api.Modul
 
 // traceStdout implements trace to the configured Stdout.
 var traceStdout = &wasm.HostFunc{
-	ExportNames: []string{TraceName},
-	Name:        "~lib/builtins/trace",
-	ParamTypes:  []api.ValueType{i32, i32, f64, f64, f64, f64, f64},
-	ParamNames:  []string{"message", "nArgs", "arg0", "arg1", "arg2", "arg3", "arg4"},
+	ExportName: TraceName,
+	Name:       "~lib/builtins/trace",
+	ParamTypes: []api.ValueType{i32, i32, f64, f64, f64, f64, f64},
+	ParamNames: []string{"message", "nArgs", "arg0", "arg1", "arg2", "arg3", "arg4"},
 	Code: wasm.Code{
 		GoFunc: api.GoModuleFunc(func(_ context.Context, mod api.Module, stack []uint64) {
 			fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -270,7 +270,7 @@ func formatFloat(f float64) string {
 //
 // See https://github.com/AssemblyScript/assemblyscript/blob/v0.26.7/std/assembly/builtins.ts#L2531
 var seed = &wasm.HostFunc{
-	ExportNames: []string{SeedName},
+	ExportName:  SeedName,
 	Name:        "~lib/builtins/seed",
 	ResultTypes: []api.ValueType{f64},
 	ResultNames: []string{"rand"},
diff --git a/imports/emscripten/emscripten.go b/imports/emscripten/emscripten.go
index b013572c..a74034b9 100644
--- a/imports/emscripten/emscripten.go
+++ b/imports/emscripten/emscripten.go
@@ -94,11 +94,10 @@ func (functionExporter) ExportFunctions(builder wazero.HostModuleBuilder) {
 const functionNotifyMemoryGrowth = "emscripten_notify_memory_growth"
 
 var notifyMemoryGrowth = &wasm.HostFunc{
-	ExportNames: []string{functionNotifyMemoryGrowth},
-	Name:        functionNotifyMemoryGrowth,
-	ParamTypes:  []wasm.ValueType{wasm.ValueTypeI32},
-	ParamNames:  []string{"memory_index"},
-	Code:        wasm.Code{GoFunc: api.GoModuleFunc(func(context.Context, api.Module, []uint64) {})},
+	ExportName: functionNotifyMemoryGrowth,
+	ParamTypes: []wasm.ValueType{wasm.ValueTypeI32},
+	ParamNames: []string{"memory_index"},
+	Code:       wasm.Code{GoFunc: api.GoModuleFunc(func(context.Context, api.Module, []uint64) {})},
 }
 
 // All `invoke_` functions have an initial "index" parameter of
@@ -134,8 +133,7 @@ const (
 )
 
 var invokeI = &wasm.HostFunc{
-	ExportNames: []string{functionInvokeI},
-	Name:        functionInvokeI,
+	ExportName:  functionInvokeI,
 	ParamTypes:  []api.ValueType{i32},
 	ParamNames:  []string{"index"},
 	ResultTypes: []api.ValueType{i32},
@@ -151,8 +149,7 @@ func invokeIFn(ctx context.Context, mod api.Module, stack []uint64) {
 }
 
 var invokeIi = &wasm.HostFunc{
-	ExportNames: []string{functionInvokeIi},
-	Name:        functionInvokeIi,
+	ExportName:  functionInvokeIi,
 	ParamTypes:  []api.ValueType{i32, i32},
 	ParamNames:  []string{"index", "a1"},
 	ResultTypes: []api.ValueType{i32},
@@ -168,8 +165,7 @@ func invokeIiFn(ctx context.Context, mod api.Module, stack []uint64) {
 }
 
 var invokeIii = &wasm.HostFunc{
-	ExportNames: []string{functionInvokeIii},
-	Name:        functionInvokeIii,
+	ExportName:  functionInvokeIii,
 	ParamTypes:  []api.ValueType{i32, i32, i32},
 	ParamNames:  []string{"index", "a1", "a2"},
 	ResultTypes: []api.ValueType{i32},
@@ -185,8 +181,7 @@ func invokeIiiFn(ctx context.Context, mod api.Module, stack []uint64) {
 }
 
 var invokeIiii = &wasm.HostFunc{
-	ExportNames: []string{functionInvokeIiii},
-	Name:        functionInvokeIiii,
+	ExportName:  functionInvokeIiii,
 	ParamTypes:  []api.ValueType{i32, i32, i32, i32},
 	ParamNames:  []string{"index", "a1", "a2", "a3"},
 	ResultTypes: []api.ValueType{i32},
@@ -202,8 +197,7 @@ func invokeIiiiFn(ctx context.Context, mod api.Module, stack []uint64) {
 }
 
 var invokeIiiii = &wasm.HostFunc{
-	ExportNames: []string{functionInvokeIiiii},
-	Name:        functionInvokeIiiii,
+	ExportName:  functionInvokeIiiii,
 	ParamTypes:  []api.ValueType{i32, i32, i32, i32, i32},
 	ParamNames:  []string{"index", "a1", "a2", "a3", "a4"},
 	ResultTypes: []api.ValueType{i32},
@@ -219,8 +213,7 @@ func invokeIiiiiFn(ctx context.Context, mod api.Module, stack []uint64) {
 }
 
 var invokeV = &wasm.HostFunc{
-	ExportNames: []string{functionInvokeV},
-	Name:        functionInvokeV,
+	ExportName:  functionInvokeV,
 	ParamTypes:  []api.ValueType{i32},
 	ParamNames:  []string{"index"},
 	ResultTypes: []api.ValueType{},
@@ -235,8 +228,7 @@ func invokeVFn(ctx context.Context, mod api.Module, stack []uint64) {
 }
 
 var invokeVi = &wasm.HostFunc{
-	ExportNames: []string{functionInvokeVi},
-	Name:        functionInvokeVi,
+	ExportName:  functionInvokeVi,
 	ParamTypes:  []api.ValueType{i32, i32},
 	ParamNames:  []string{"index", "a1"},
 	ResultTypes: []api.ValueType{},
@@ -251,8 +243,7 @@ func invokeViFn(ctx context.Context, mod api.Module, stack []uint64) {
 }
 
 var invokeVii = &wasm.HostFunc{
-	ExportNames: []string{functionInvokeVii},
-	Name:        functionInvokeVii,
+	ExportName:  functionInvokeVii,
 	ParamTypes:  []api.ValueType{i32, i32, i32},
 	ParamNames:  []string{"index", "a1", "a2"},
 	ResultTypes: []api.ValueType{},
@@ -267,8 +258,7 @@ func invokeViiFn(ctx context.Context, mod api.Module, stack []uint64) {
 }
 
 var invokeViii = &wasm.HostFunc{
-	ExportNames: []string{functionInvokeViii},
-	Name:        functionInvokeViii,
+	ExportName:  functionInvokeViii,
 	ParamTypes:  []api.ValueType{i32, i32, i32, i32},
 	ParamNames:  []string{"index", "a1", "a2", "a3"},
 	ResultTypes: []api.ValueType{},
@@ -283,8 +273,7 @@ func invokeViiiFn(ctx context.Context, mod api.Module, stack []uint64) {
 }
 
 var invokeViiii = &wasm.HostFunc{
-	ExportNames: []string{functionInvokeViiii},
-	Name:        functionInvokeViiii,
+	ExportName:  functionInvokeViiii,
 	ParamTypes:  []api.ValueType{i32, i32, i32, i32, i32},
 	ParamNames:  []string{"index", "a1", "a2", "a3", "a4"},
 	ResultTypes: []api.ValueType{},
diff --git a/imports/wasi_snapshot_preview1/proc.go b/imports/wasi_snapshot_preview1/proc.go
index 07c963ef..d1b5fced 100644
--- a/imports/wasi_snapshot_preview1/proc.go
+++ b/imports/wasi_snapshot_preview1/proc.go
@@ -19,13 +19,10 @@ import (
 //
 // See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#proc_exit
 var procExit = &wasm.HostFunc{
-	ExportNames: []string{wasip1.ProcExitName},
-	Name:        wasip1.ProcExitName,
-	ParamTypes:  []api.ValueType{i32},
-	ParamNames:  []string{"rval"},
-	Code: wasm.Code{
-		GoFunc: api.GoModuleFunc(procExitFn),
-	},
+	ExportName: wasip1.ProcExitName,
+	ParamTypes: []api.ValueType{i32},
+	ParamNames: []string{"rval"},
+	Code:       wasm.Code{GoFunc: api.GoModuleFunc(procExitFn)},
 }
 
 func procExitFn(ctx context.Context, mod api.Module, params []uint64) {
diff --git a/imports/wasi_snapshot_preview1/wasi.go b/imports/wasi_snapshot_preview1/wasi.go
index f8498e07..04177908 100644
--- a/imports/wasi_snapshot_preview1/wasi.go
+++ b/imports/wasi_snapshot_preview1/wasi.go
@@ -263,8 +263,7 @@ func newHostFunc(
 	paramNames ...string,
 ) *wasm.HostFunc {
 	return &wasm.HostFunc{
-		ExportNames: []string{name},
-		Name:        name,
+		ExportName:  name,
 		ParamTypes:  paramTypes,
 		ParamNames:  paramNames,
 		ResultTypes: []api.ValueType{i32},
@@ -291,8 +290,7 @@ func (f wasiFunc) Call(ctx context.Context, mod api.Module, stack []uint64) {
 // stubFunction stubs for GrainLang per #271.
 func stubFunction(name string, paramTypes []wasm.ValueType, paramNames ...string) *wasm.HostFunc {
 	return &wasm.HostFunc{
-		Name:        name,
-		ExportNames: []string{name},
+		ExportName:  name,
 		ParamTypes:  paramTypes,
 		ParamNames:  paramNames,
 		ResultTypes: []api.ValueType{i32},
diff --git a/internal/gojs/goarch/wasm.go b/internal/gojs/goarch/wasm.go
index dab0a333..82b52abe 100644
--- a/internal/gojs/goarch/wasm.go
+++ b/internal/gojs/goarch/wasm.go
@@ -16,11 +16,10 @@ import (
 // This traps (unreachable opcode) to ensure the function is never called.
 func StubFunction(name string) *wasm.HostFunc {
 	return &wasm.HostFunc{
-		ExportNames: []string{name},
-		Name:        name,
-		ParamTypes:  []wasm.ValueType{wasm.ValueTypeI32},
-		ParamNames:  []string{"sp"},
-		Code:        wasm.Code{GoFunc: api.GoModuleFunc(func(ctx context.Context, _ api.Module, stack []uint64) {})},
+		ExportName: name,
+		ParamTypes: []wasm.ValueType{wasm.ValueTypeI32},
+		ParamNames: []string{"sp"},
+		Code:       wasm.Code{GoFunc: api.GoModuleFunc(func(ctx context.Context, _ api.Module, stack []uint64) {})},
 	}
 }
 
diff --git a/internal/gojs/util/util.go b/internal/gojs/util/util.go
index 21db4c92..a8bfba0d 100644
--- a/internal/gojs/util/util.go
+++ b/internal/gojs/util/util.go
@@ -38,11 +38,10 @@ func MustRead(mem api.Memory, funcName string, paramIdx int, offset, byteCount u
 
 func NewFunc(name string, goFunc api.GoModuleFunc) *wasm.HostFunc {
 	return &wasm.HostFunc{
-		ExportNames: []string{name},
-		Name:        name,
-		ParamTypes:  []api.ValueType{api.ValueTypeI32},
-		ParamNames:  []string{"sp"},
-		Code:        wasm.Code{GoFunc: goFunc},
+		ExportName: name,
+		ParamTypes: []api.ValueType{api.ValueTypeI32},
+		ParamNames: []string{"sp"},
+		Code:       wasm.Code{GoFunc: goFunc},
 	}
 }
 
diff --git a/internal/wasm/host.go b/internal/wasm/host.go
index e0a1c190..cb4ca65e 100644
--- a/internal/wasm/host.go
+++ b/internal/wasm/host.go
@@ -3,7 +3,6 @@ package wasm
 import (
 	"errors"
 	"fmt"
-	"sort"
 
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/wasmdebug"
@@ -16,8 +15,8 @@ type HostFuncExporter interface {
 // HostFunc is a function with an inlined type, used for NewHostModule.
 // Any corresponding FunctionType will be reused or added to the Module.
 type HostFunc struct {
-	// ExportNames is equivalent to the same method on api.FunctionDefinition.
-	ExportNames []string
+	// ExportName is the only value returned by api.FunctionDefinition.
+	ExportName string
 
 	// Name is equivalent to the same method on api.FunctionDefinition.
 	Name string
@@ -45,17 +44,11 @@ func (f *HostFunc) WithGoModuleFunc(fn api.GoModuleFunc) *HostFunc {
 	return &ret
 }
 
-type HostFuncNames struct {
-	Name        string
-	ParamNames  []string
-	ResultNames []string
-}
-
 // NewHostModule is defined internally for use in WASI tests and to keep the code size in the root directory small.
 func NewHostModule(
 	moduleName string,
-	nameToGoFunc map[string]interface{},
-	funcToNames map[string]*HostFuncNames,
+	exportNames []string,
+	nameToHostFunc map[string]*HostFunc,
 	enabledFeatures api.CoreFeatures,
 ) (m *Module, err error) {
 	if moduleName != "" {
@@ -64,10 +57,10 @@ func NewHostModule(
 		return nil, errors.New("a module name must not be empty")
 	}
 
-	if exportCount := uint32(len(nameToGoFunc)); exportCount > 0 {
+	if exportCount := uint32(len(nameToHostFunc)); exportCount > 0 {
 		m.ExportSection = make([]Export, 0, exportCount)
 		m.Exports = make(map[string]*Export, exportCount)
-		if err = addFuncs(m, nameToGoFunc, funcToNames, enabledFeatures); err != nil {
+		if err = addFuncs(m, exportNames, nameToHostFunc, enabledFeatures); err != nil {
 			return
 		}
 	}
@@ -85,73 +78,55 @@ func NewHostModule(
 
 func addFuncs(
 	m *Module,
-	nameToGoFunc map[string]interface{},
-	funcToNames map[string]*HostFuncNames,
+	exportNames []string,
+	nameToHostFunc map[string]*HostFunc,
 	enabledFeatures api.CoreFeatures,
 ) (err error) {
 	if m.NameSection == nil {
 		m.NameSection = &NameSection{}
 	}
 	moduleName := m.NameSection.ModuleName
-	nameToFunc := make(map[string]*HostFunc, len(nameToGoFunc))
-	sortedExportNames := make([]string, len(nameToFunc))
-	for k := range nameToGoFunc {
-		sortedExportNames = append(sortedExportNames, k)
-	}
 
-	// Sort names for consistent iteration
-	sort.Strings(sortedExportNames)
-
-	funcNames := make([]string, len(nameToFunc))
-	for _, k := range sortedExportNames {
-		v := nameToGoFunc[k]
-		if hf, ok := v.(*HostFunc); ok {
-			nameToFunc[hf.Name] = hf
-			funcNames = append(funcNames, hf.Name)
-		} else { // reflection
-			params, results, code, ftErr := parseGoReflectFunc(v)
-			if ftErr != nil {
-				return fmt.Errorf("func[%s.%s] %w", moduleName, k, ftErr)
-			}
-			hf = &HostFunc{
-				ExportNames: []string{k},
-				Name:        k,
-				ParamTypes:  params,
-				ResultTypes: results,
-				Code:        code,
-			}
+	for _, k := range exportNames {
+		hf := nameToHostFunc[k]
+		if hf.Name == "" {
+			hf.Name = k // default name to export name
+		}
+		switch hf.Code.GoFunc.(type) {
+		case api.GoModuleFunction, api.GoFunction:
+			continue // already parsed
+		}
 
-			// Assign names to the function, if they exist.
-			ns := funcToNames[k]
-			if name := ns.Name; name != "" {
-				hf.Name = ns.Name
-			}
-			if paramNames := ns.ParamNames; paramNames != nil {
-				if paramNamesLen := len(paramNames); paramNamesLen != len(params) {
-					return fmt.Errorf("func[%s.%s] has %d params, but %d params names", moduleName, k, paramNamesLen, len(params))
-				}
-				hf.ParamNames = paramNames
-			}
-			if resultNames := ns.ResultNames; resultNames != nil {
-				if resultNamesLen := len(resultNames); resultNamesLen != len(results) {
-					return fmt.Errorf("func[%s.%s] has %d results, but %d results names", moduleName, k, resultNamesLen, len(results))
-				}
-				hf.ResultNames = resultNames
+		// Resolve the code using reflection
+		hf.ParamTypes, hf.ResultTypes, hf.Code, err = parseGoReflectFunc(hf.Code.GoFunc)
+		if err != nil {
+			return fmt.Errorf("func[%s.%s] %w", moduleName, k, err)
+		}
+
+		// Assign names to the function, if they exist.
+		params := hf.ParamTypes
+		if paramNames := hf.ParamNames; paramNames != nil {
+			if paramNamesLen := len(paramNames); paramNamesLen != len(params) {
+				return fmt.Errorf("func[%s.%s] has %d params, but %d params names", moduleName, k, paramNamesLen, len(params))
 			}
+		}
 
-			nameToFunc[k] = hf
-			funcNames = append(funcNames, k)
+		results := hf.ResultTypes
+		if resultNames := hf.ResultNames; resultNames != nil {
+			if resultNamesLen := len(resultNames); resultNamesLen != len(results) {
+				return fmt.Errorf("func[%s.%s] has %d results, but %d results names", moduleName, k, resultNamesLen, len(results))
+			}
 		}
 	}
 
-	funcCount := uint32(len(nameToFunc))
+	funcCount := uint32(len(exportNames))
 	m.NameSection.FunctionNames = make([]NameAssoc, 0, funcCount)
 	m.FunctionSection = make([]Index, 0, funcCount)
 	m.CodeSection = make([]Code, 0, funcCount)
 
 	idx := Index(0)
-	for _, name := range funcNames {
-		hf := nameToFunc[name]
+	for _, name := range exportNames {
+		hf := nameToHostFunc[name]
 		debugName := wasmdebug.FuncName(moduleName, name, idx)
 		typeIdx, typeErr := m.maybeAddType(hf.ParamTypes, hf.ResultTypes, enabledFeatures)
 		if typeErr != nil {
@@ -159,10 +134,10 @@ func addFuncs(
 		}
 		m.FunctionSection = append(m.FunctionSection, typeIdx)
 		m.CodeSection = append(m.CodeSection, hf.Code)
-		for _, export := range hf.ExportNames {
-			m.ExportSection = append(m.ExportSection, Export{Type: ExternTypeFunc, Name: export, Index: idx})
-			m.Exports[export] = &m.ExportSection[len(m.ExportSection)-1]
-		}
+
+		export := hf.ExportName
+		m.ExportSection = append(m.ExportSection, Export{Type: ExternTypeFunc, Name: export, Index: idx})
+		m.Exports[export] = &m.ExportSection[len(m.ExportSection)-1]
 		m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, NameAssoc{Index: idx, Name: hf.Name})
 
 		if len(hf.ParamNames) > 0 {


Test patch:
diff --git a/builder_test.go b/builder_test.go
index 7deee604..2e80fe48 100644
--- a/builder_test.go
+++ b/builder_test.go
@@ -159,21 +159,21 @@ func TestNewHostModuleBuilder_Compile(t *testing.T) {
 			},
 			expected: &wasm.Module{
 				TypeSection: []wasm.FunctionType{
-					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
 					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
+					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
 				},
 				FunctionSection: []wasm.Index{0, 1},
-				CodeSection:     []wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32), wasm.MustParseGoReflectFuncCode(uint64_uint32)},
+				CodeSection:     []wasm.Code{wasm.MustParseGoReflectFuncCode(uint64_uint32), wasm.MustParseGoReflectFuncCode(uint32_uint32)},
 				ExportSection: []wasm.Export{
-					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
-					{Name: "2", Type: wasm.ExternTypeFunc, Index: 1},
+					{Name: "2", Type: wasm.ExternTypeFunc, Index: 0},
+					{Name: "1", Type: wasm.ExternTypeFunc, Index: 1},
 				},
 				Exports: map[string]*wasm.Export{
-					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
-					"2": {Name: "2", Type: wasm.ExternTypeFunc, Index: 1},
+					"2": {Name: "2", Type: wasm.ExternTypeFunc, Index: 0},
+					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 1},
 				},
 				NameSection: &wasm.NameSection{
-					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}, {Index: 1, Name: "2"}},
+					FunctionNames: wasm.NameMap{{Index: 0, Name: "2"}, {Index: 1, Name: "1"}},
 					ModuleName:    "host",
 				},
 			},
@@ -269,7 +269,7 @@ func TestNewHostModuleBuilder_Compile(t *testing.T) {
 		{
 			name: "WithGoFunction twice",
 			input: func(r Runtime) HostModuleBuilder {
-				// Intentionally out of order
+				// Intentionally not in lexicographic order
 				return r.NewHostModuleBuilder("host").
 					NewFunctionBuilder().
 					WithGoFunction(gofunc2, []api.ValueType{i64}, []api.ValueType{i32}).
@@ -280,24 +280,24 @@ func TestNewHostModuleBuilder_Compile(t *testing.T) {
 			},
 			expected: &wasm.Module{
 				TypeSection: []wasm.FunctionType{
-					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
 					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
+					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
 				},
 				FunctionSection: []wasm.Index{0, 1},
 				CodeSection: []wasm.Code{
-					{GoFunc: gofunc1},
 					{GoFunc: gofunc2},
+					{GoFunc: gofunc1},
 				},
 				ExportSection: []wasm.Export{
-					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
-					{Name: "2", Type: wasm.ExternTypeFunc, Index: 1},
+					{Name: "2", Type: wasm.ExternTypeFunc, Index: 0},
+					{Name: "1", Type: wasm.ExternTypeFunc, Index: 1},
 				},
 				Exports: map[string]*wasm.Export{
-					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
-					"2": {Name: "2", Type: wasm.ExternTypeFunc, Index: 1},
+					"2": {Name: "2", Type: wasm.ExternTypeFunc, Index: 0},
+					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 1},
 				},
 				NameSection: &wasm.NameSection{
-					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}, {Index: 1, Name: "2"}},
+					FunctionNames: wasm.NameMap{{Index: 0, Name: "2"}, {Index: 1, Name: "1"}},
 					ModuleName:    "host",
 				},
 			},
@@ -342,18 +342,13 @@ func TestNewHostModuleBuilder_Compile_Errors(t *testing.T) {
 		expectedErr string
 	}{
 		{
-			name: "error compiling", // should fail due to missing result.
+			name: "error compiling", // should fail due to invalid param.
 			input: func(rt Runtime) HostModuleBuilder {
 				return rt.NewHostModuleBuilder("host").NewFunctionBuilder().
-					WithFunc(&wasm.HostFunc{
-						ExportNames: []string{"fn"},
-						ResultTypes: []wasm.ValueType{wasm.ValueTypeI32},
-						Code:        wasm.Code{Body: []byte{wasm.OpcodeEnd}},
-					}).Export("fn")
+					WithFunc(&wasm.HostFunc{ExportName: "fn", Code: wasm.Code{GoFunc: func(string) {}}}).
+					Export("fn")
 			},
-			expectedErr: `invalid function[0] export["fn"]: not enough results
-	have ()
-	want (i32)`,
+			expectedErr: `func[host.fn] param[0] is unsupported: string`,
 		},
 	}
 
diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go
index 093fa84a..74d51342 100644
--- a/internal/engine/compiler/engine_test.go
+++ b/internal/engine/compiler/engine_test.go
@@ -231,7 +231,7 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
 
 	const hostModuleName = "env"
 	const hostFnName = "grow_and_shrink_goroutine_stack"
-	hm, err := wasm.NewHostModule(hostModuleName, map[string]interface{}{hostFnName: func() {
+	hostFn := func() {
 		// This function aggressively grow the goroutine stack by recursively
 		// calling the function many times.
 		callNum := 1000
@@ -247,7 +247,13 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
 		// Trigger relocation of goroutine stack because at this point we have the majority of
 		// goroutine stack unused after recursive call.
 		runtime.GC()
-	}}, map[string]*wasm.HostFuncNames{hostFnName: {}}, enabledFeatures)
+	}
+	hm, err := wasm.NewHostModule(
+		hostModuleName,
+		[]string{hostFnName},
+		map[string]*wasm.HostFunc{hostFnName: {ExportName: hostFnName, Code: wasm.Code{GoFunc: hostFn}}},
+		enabledFeatures,
+	)
 	require.NoError(t, err)
 
 	err = s.Engine.CompileModule(testCtx, hm, nil, false)
diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go
index f02faba3..b736a962 100644
--- a/internal/testing/enginetest/enginetest.go
+++ b/internal/testing/enginetest/enginetest.go
@@ -58,11 +58,21 @@ func RunTestEngine_MemoryGrowInRecursiveCall(t *testing.T, et EngineTester) {
 	const hostModuleName = "env"
 	const hostFnName = "grow_memory"
 	var growFn api.Function
-	hm, err := wasm.NewHostModule(hostModuleName, map[string]interface{}{hostFnName: func() {
-		// Does the recursive call into Wasm, which grows memory.
-		_, err := growFn.Call(context.Background())
-		require.NoError(t, err)
-	}}, map[string]*wasm.HostFuncNames{hostFnName: {}}, enabledFeatures)
+	hm, err := wasm.NewHostModule(
+		hostModuleName,
+		[]string{hostFnName},
+		map[string]*wasm.HostFunc{
+			hostFnName: {
+				ExportName: hostFnName,
+				Code: wasm.Code{GoFunc: func() {
+					// Does the recursive call into Wasm, which grows memory.
+					_, err := growFn.Call(context.Background())
+					require.NoError(t, err)
+				}},
+			},
+		},
+		enabledFeatures,
+	)
 	require.NoError(t, err)
 
 	err = s.Engine.CompileModule(testCtx, hm, nil, false)
diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go
index cde01651..55c881cd 100644
--- a/internal/wasm/host_test.go
+++ b/internal/wasm/host_test.go
@@ -30,8 +30,8 @@ func TestNewHostModule(t *testing.T) {
 	swapName := "swap"
 	tests := []struct {
 		name, moduleName string
-		nameToGoFunc     map[string]interface{}
-		funcToNames      map[string]*HostFuncNames
+		exportNames      []string
+		nameToHostFunc   map[string]*HostFunc
 		expected         *Module
 	}{
 		{
@@ -40,22 +40,21 @@ func TestNewHostModule(t *testing.T) {
 			expected:   &Module{NameSection: &NameSection{ModuleName: "test"}},
 		},
 		{
-			name:       "funcs",
-			moduleName: InternalModuleName,
-			nameToGoFunc: map[string]interface{}{
-				ArgsSizesGetName: argsSizesGet,
-				FdWriteName:      fdWrite,
-			},
-			funcToNames: map[string]*HostFuncNames{
+			name:        "funcs",
+			moduleName:  InternalModuleName,
+			exportNames: []string{ArgsSizesGetName, FdWriteName},
+			nameToHostFunc: map[string]*HostFunc{
 				ArgsSizesGetName: {
-					Name:        ArgsSizesGetName,
+					ExportName:  ArgsSizesGetName,
 					ParamNames:  []string{"result.argc", "result.argv_len"},
 					ResultNames: []string{"errno"},
+					Code:        Code{GoFunc: argsSizesGet},
 				},
 				FdWriteName: {
-					Name:        FdWriteName,
+					ExportName:  FdWriteName,
 					ParamNames:  []string{"fd", "iovs", "iovs_len", "result.size"},
 					ResultNames: []string{"errno"},
+					Code:        Code{GoFunc: fdWrite},
 				},
 			},
 			expected: &Module{
@@ -99,12 +98,10 @@ func TestNewHostModule(t *testing.T) {
 			},
 		},
 		{
-			name:       "multi-value",
-			moduleName: "swapper",
-			nameToGoFunc: map[string]interface{}{
-				swapName: swap,
-			},
-			funcToNames: map[string]*HostFuncNames{swapName: {}},
+			name:           "multi-value",
+			moduleName:     "swapper",
+			exportNames:    []string{swapName},
+			nameToHostFunc: map[string]*HostFunc{swapName: {ExportName: swapName, Code: Code{GoFunc: swap}}},
 			expected: &Module{
 				TypeSection:     []FunctionType{{Params: []ValueType{i32, i32}, Results: []ValueType{i32, i32}}},
 				FunctionSection: []Index{0},
@@ -120,7 +117,7 @@ func TestNewHostModule(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			m, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.funcToNames, api.CoreFeaturesV2)
+			m, e := NewHostModule(tc.moduleName, tc.exportNames, tc.nameToHostFunc, api.CoreFeaturesV2)
 			require.NoError(t, e)
 			requireHostModuleEquals(t, tc.expected, m)
 			require.True(t, m.IsHostModule)
@@ -158,23 +155,23 @@ func requireHostModuleEquals(t *testing.T, expected, actual *Module) {
 func TestNewHostModule_Errors(t *testing.T) {
 	tests := []struct {
 		name, moduleName string
-		nameToGoFunc     map[string]interface{}
-		funcToNames      map[string]*HostFuncNames
+		exportNames      []string
+		nameToHostFunc   map[string]*HostFunc
 		expectedErr      string
 	}{
 		{
-			name:         "not a function",
-			moduleName:   "modname",
-			nameToGoFunc: map[string]interface{}{"fn": t},
-			funcToNames:  map[string]*HostFuncNames{"fn": {}},
-			expectedErr:  "func[modname.fn] kind != func: ptr",
+			name:           "not a function",
+			moduleName:     "modname",
+			exportNames:    []string{"fn"},
+			nameToHostFunc: map[string]*HostFunc{"fn": {ExportName: "fn", Code: Code{GoFunc: t}}},
+			expectedErr:    "func[modname.fn] kind != func: ptr",
 		},
 		{
-			name:         "function has multiple results",
-			moduleName:   "yetanother",
-			nameToGoFunc: map[string]interface{}{"fn": func() (uint32, uint32) { return 0, 0 }},
-			funcToNames:  map[string]*HostFuncNames{"fn": {}},
-			expectedErr:  "func[yetanother.fn] multiple result types invalid as feature \"multi-value\" is disabled",
+			name:           "function has multiple results",
+			moduleName:     "yetanother",
+			exportNames:    []string{"fn"},
+			nameToHostFunc: map[string]*HostFunc{"fn": {ExportName: "fn", Code: Code{GoFunc: func() (uint32, uint32) { return 0, 0 }}}},
+			expectedErr:    "func[yetanother.fn] multiple result types invalid as feature \"multi-value\" is disabled",
 		},
 	}
 
@@ -182,7 +179,7 @@ func TestNewHostModule_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			_, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.funcToNames, api.CoreFeaturesV1)
+			_, e := NewHostModule(tc.moduleName, tc.exportNames, tc.nameToHostFunc, api.CoreFeaturesV1)
 			require.EqualError(t, e, tc.expectedErr)
 		})
 	}
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index f6524704..2bb1af1c 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -105,7 +105,12 @@ func TestNewStore(t *testing.T) {
 
 func TestStore_Instantiate(t *testing.T) {
 	s := newStore()
-	m, err := NewHostModule("foo", map[string]interface{}{"fn": func() {}}, map[string]*HostFuncNames{"fn": {}}, api.CoreFeaturesV1)
+	m, err := NewHostModule(
+		"foo",
+		[]string{"fn"},
+		map[string]*HostFunc{"fn": {ExportName: "fn", Code: Code{GoFunc: func() {}}}},
+		api.CoreFeaturesV1,
+	)
 	require.NoError(t, err)
 
 	sysCtx := sys.DefaultContext(nil)
@@ -184,7 +189,12 @@ func TestStore_CloseWithExitCode(t *testing.T) {
 func TestStore_hammer(t *testing.T) {
 	const importedModuleName = "imported"
 
-	m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func() {}}, map[string]*HostFuncNames{"fn": {}}, api.CoreFeaturesV1)
+	m, err := NewHostModule(
+		importedModuleName,
+		[]string{"fn"},
+		map[string]*HostFunc{"fn": {ExportName: "fn", Code: Code{GoFunc: func() {}}}},
+		api.CoreFeaturesV1,
+	)
 	require.NoError(t, err)
 
 	s := newStore()
@@ -239,7 +249,12 @@ func TestStore_hammer(t *testing.T) {
 func TestStore_hammer_close(t *testing.T) {
 	const importedModuleName = "imported"
 
-	m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func() {}}, map[string]*HostFuncNames{"fn": {}}, api.CoreFeaturesV1)
+	m, err := NewHostModule(
+		importedModuleName,
+		[]string{"fn"},
+		map[string]*HostFunc{"fn": {ExportName: "fn", Code: Code{GoFunc: func() {}}}},
+		api.CoreFeaturesV1,
+	)
 	require.NoError(t, err)
 
 	s := newStore()
@@ -299,7 +314,12 @@ func TestStore_Instantiate_Errors(t *testing.T) {
 	const importedModuleName = "imported"
 	const importingModuleName = "test"
 
-	m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func() {}}, map[string]*HostFuncNames{"fn": {}}, api.CoreFeaturesV1)
+	m, err := NewHostModule(
+		importedModuleName,
+		[]string{"fn"},
+		map[string]*HostFunc{"fn": {ExportName: "fn", Code: Code{GoFunc: func() {}}}},
+		api.CoreFeaturesV1,
+	)
 	require.NoError(t, err)
 
 	t.Run("Fails if module name already in use", func(t *testing.T) {