Repository: tetratelabs/wazero (go)

Issue:
Provide a list of exports from an api.Module
**Is your feature request related to a problem? Please describe.**
I'd like to be able to list all exported functions in an `api.Module`.

**Describe the solution you'd like**
wasmer has an [`Exports`](https://docs.rs/wasmer/latest/wasmer/struct.Exports.html#method.iter) type that allows you to iterate over all exported functions.

Pull request description:
Adds ExportedFunctionDefinitions and ExportedMemoryDefinitions
This adds ExportedFunctionDefinitions and ExportedMemoryDefinitions to api.Module so that those who can't access CompileModule can see them.

Fixes #839

Patch:
diff --git a/api/wasm.go b/api/wasm.go
index a45dcdbf..6be8d243 100644
--- a/api/wasm.go
+++ b/api/wasm.go
@@ -147,6 +147,10 @@ type Module interface {
 	// ExportedFunction returns a function exported from this module or nil if it wasn't.
 	ExportedFunction(name string) Function
 
+	// ExportedFunctionDefinitions returns all the exported function
+	// definitions in this module, keyed on export name.
+	ExportedFunctionDefinitions() map[string]FunctionDefinition
+
 	// TODO: Table
 
 	// ExportedMemory returns a memory exported from this module or nil if it wasn't.
@@ -157,6 +161,13 @@ type Module interface {
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi
 	ExportedMemory(name string) Memory
 
+	// ExportedMemoryDefinitions returns all the exported memory definitions
+	// in this module, keyed on export name.
+	//
+	// Note: As of WebAssembly Core Specification 2.0, there can be at most one
+	// memory.
+	ExportedMemoryDefinitions() map[string]MemoryDefinition
+
 	// ExportedGlobal a global exported from this module or nil if it wasn't.
 	ExportedGlobal(name string) Global
 
diff --git a/internal/engine/compiler/impl_amd64.go b/internal/engine/compiler/impl_amd64.go
index 60163b6d..c7b13152 100644
--- a/internal/engine/compiler/impl_amd64.go
+++ b/internal/engine/compiler/impl_amd64.go
@@ -186,6 +186,12 @@ func (c *amd64Compiler) compileGoDefinedHostFunction() error {
 
 	// Initializes the reserved stack base pointer which is used to retrieve the call frame stack.
 	c.compileReservedStackBasePointerInitialization()
+
+	// Go function can change the module state in arbitrary way, so we have to force
+	// the callEngine.moduleContext initialization on the function return. To do so,
+	// we zero-out callEngine.moduleInstanceAddress.
+	c.assembler.CompileConstToMemory(amd64.MOVQ,
+		0, amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset)
 	return c.compileReturnFunction()
 }
 
diff --git a/internal/engine/compiler/impl_arm64.go b/internal/engine/compiler/impl_arm64.go
index 98793c56..692fff04 100644
--- a/internal/engine/compiler/impl_arm64.go
+++ b/internal/engine/compiler/impl_arm64.go
@@ -398,6 +398,14 @@ func (c *arm64Compiler) compileGoDefinedHostFunction() error {
 
 	// Initializes the reserved stack base pointer which is used to retrieve the call frame stack.
 	c.compileReservedStackBasePointerRegisterInitialization()
+
+	// Go function can change the module state in arbitrary way, so we have to force
+	// the callEngine.moduleContext initialization on the function return. To do so,
+	// we zero-out callEngine.moduleInstanceAddress.
+	c.assembler.CompileRegisterToMemory(arm64.STRD,
+		arm64.RegRZR,
+		arm64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset)
+
 	return c.compileReturnFunction()
 }
 
diff --git a/internal/wasm/call_context.go b/internal/wasm/call_context.go
index 7e70838e..bb8b2620 100644
--- a/internal/wasm/call_context.go
+++ b/internal/wasm/call_context.go
@@ -135,6 +135,21 @@ func (m *CallContext) ExportedMemory(name string) api.Memory {
 	return m.memory
 }
 
+// ExportedMemoryDefinitions implements the same method as documented on
+// api.Module.
+func (m *CallContext) ExportedMemoryDefinitions() map[string]api.MemoryDefinition {
+	// Special case as we currently only support one memory.
+	if mem := m.module.Memory; mem != nil {
+		// Now, find out if it is exported
+		for name, exp := range m.module.Exports {
+			if exp.Type == ExternTypeMemory {
+				return map[string]api.MemoryDefinition{name: mem.definition}
+			}
+		}
+	}
+	return map[string]api.MemoryDefinition{}
+}
+
 // ExportedFunction implements the same method as documented on api.Module.
 func (m *CallContext) ExportedFunction(name string) api.Function {
 	exp, err := m.module.getExport(name, ExternTypeFunc)
@@ -145,6 +160,18 @@ func (m *CallContext) ExportedFunction(name string) api.Function {
 	return m.function(&m.module.Functions[exp.Index])
 }
 
+// ExportedFunctionDefinitions implements the same method as documented on
+// api.Module.
+func (m *CallContext) ExportedFunctionDefinitions() map[string]api.FunctionDefinition {
+	result := map[string]api.FunctionDefinition{}
+	for name, exp := range m.module.Exports {
+		if exp.Type == ExternTypeFunc {
+			result[name] = m.module.Functions[exp.Index].Definition
+		}
+	}
+	return result
+}
+
 // Module is exposed for emscripten.
 func (m *CallContext) Module() *ModuleInstance {
 	return m.module
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index 6d20d2ff..8eb3011d 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -657,6 +657,7 @@ func (m *Module) buildMemory() (mem *MemoryInstance) {
 	memSec := m.MemorySection
 	if memSec != nil {
 		mem = NewMemoryInstance(memSec)
+		mem.definition = m.MemoryDefinitionSection[0]
 	}
 	return
 }


Test patch:
diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go
index 6001d794..d9499343 100644
--- a/internal/engine/compiler/engine_test.go
+++ b/internal/engine/compiler/engine_test.go
@@ -58,6 +58,11 @@ func TestCompiler_Engine_NewModuleEngine(t *testing.T) {
 	enginetest.RunTestEngine_NewModuleEngine(t, et)
 }
 
+func TestCompiler_MemoryGrowInRecursiveCall(t *testing.T) {
+	defer functionLog.Reset()
+	enginetest.RunTestEngine_MemoryGrowInRecursiveCall(t, et)
+}
+
 func TestCompiler_ModuleEngine_LookupFunction(t *testing.T) {
 	defer functionLog.Reset()
 	enginetest.RunTestModuleEngine_LookupFunction(t, et)
diff --git a/internal/engine/interpreter/interpreter_test.go b/internal/engine/interpreter/interpreter_test.go
index 55e95372..ff58ac08 100644
--- a/internal/engine/interpreter/interpreter_test.go
+++ b/internal/engine/interpreter/interpreter_test.go
@@ -95,6 +95,11 @@ func (e engineTester) CompiledFunctionPointerValue(me wasm.ModuleEngine, funcInd
 	return uint64(uintptr(unsafe.Pointer(internal.functions[funcIndex])))
 }
 
+func TestInterpreter_MemoryGrowInRecursiveCall(t *testing.T) {
+	defer functionLog.Reset()
+	enginetest.RunTestEngine_MemoryGrowInRecursiveCall(t, et)
+}
+
 func TestInterpreter_Engine_NewModuleEngine(t *testing.T) {
 	enginetest.RunTestEngine_NewModuleEngine(t, et)
 }
diff --git a/internal/gojs/misc_test.go b/internal/gojs/misc_test.go
index 510567aa..b48c1113 100644
--- a/internal/gojs/misc_test.go
+++ b/internal/gojs/misc_test.go
@@ -45,7 +45,6 @@ func Test_stdio(t *testing.T) {
 }
 
 func Test_stdio_large(t *testing.T) {
-	t.Skip("TODO: #980 memory out of bounds when run with compiler")
 	t.Parallel()
 
 	size := 2 * 1024 * 1024 // 2MB
diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go
index d588b609..71aec8e8 100644
--- a/internal/integration_test/spectest/spectest.go
+++ b/internal/integration_test/spectest/spectest.go
@@ -362,6 +362,7 @@ func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *was
 	mod.ExportSection = append(mod.ExportSection, &wasm.Export{Name: "table", Index: 0, Type: wasm.ExternTypeTable})
 
 	maybeSetMemoryCap(mod)
+	mod.BuildMemoryDefinitions()
 	mod.BuildFunctionDefinitions()
 
 	err = mod.Validate(enabledFeatures)
@@ -432,6 +433,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(
 						}
 
 						maybeSetMemoryCap(mod)
+						mod.BuildMemoryDefinitions()
 						mod.BuildFunctionDefinitions()
 						err = s.Engine.CompileModule(ctx, mod, nil)
 						require.NoError(t, err, msg)
@@ -603,6 +605,7 @@ func requireInstantiationError(t *testing.T, ctx context.Context, s *wasm.Store,
 	mod.AssignModuleID(buf)
 
 	maybeSetMemoryCap(mod)
+	mod.BuildMemoryDefinitions()
 	mod.BuildFunctionDefinitions()
 	err = s.Engine.CompileModule(ctx, mod, nil)
 	if err != nil {
diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go
index f735d256..9abe56d7 100644
--- a/internal/testing/enginetest/enginetest.go
+++ b/internal/testing/enginetest/enginetest.go
@@ -55,6 +55,64 @@ type EngineTester interface {
 	CompiledFunctionPointerValue(tme wasm.ModuleEngine, funcIndex wasm.Index) uint64
 }
 
+// RunTestEngine_MemoryGrowInRecursiveCall ensures that it's safe to grow memory in the recursive Wasm calls.
+func RunTestEngine_MemoryGrowInRecursiveCall(t *testing.T, et EngineTester) {
+	enabledFeatures := api.CoreFeaturesV1
+	e := et.NewEngine(enabledFeatures)
+	s, ns := wasm.NewStore(enabledFeatures, e)
+
+	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)
+	require.NoError(t, err)
+
+	err = s.Engine.CompileModule(testCtx, hm, nil)
+	require.NoError(t, err)
+
+	_, err = s.Instantiate(testCtx, ns, hm, hostModuleName, nil)
+	require.NoError(t, err)
+
+	m := &wasm.Module{
+		TypeSection:     []*wasm.FunctionType{{Params: []wasm.ValueType{}, Results: []wasm.ValueType{}}},
+		FunctionSection: []wasm.Index{0, 0},
+		CodeSection: []*wasm.Code{
+			{
+				Body: []byte{
+					// Calls the imported host function, which in turn calls the next in-Wasm function recursively.
+					wasm.OpcodeCall, 0,
+					// Access the memory and this should succeed as we already had memory grown at this point.
+					wasm.OpcodeI32Const, 0,
+					wasm.OpcodeI32Load, 0x2, 0x0,
+					wasm.OpcodeDrop,
+					wasm.OpcodeEnd,
+				},
+			},
+			{
+				// Grows memory by 1 page.
+				Body: []byte{wasm.OpcodeI32Const, 1, wasm.OpcodeMemoryGrow, wasm.OpcodeDrop, wasm.OpcodeEnd},
+			},
+		},
+		MemorySection: &wasm.Memory{Max: 1000},
+		ImportSection: []*wasm.Import{{Module: hostModuleName, Name: hostFnName, DescFunc: 0}},
+	}
+	m.BuildFunctionDefinitions()
+
+	err = s.Engine.CompileModule(testCtx, m, nil)
+	require.NoError(t, err)
+
+	inst, err := s.Instantiate(testCtx, ns, m, t.Name(), nil)
+	require.NoError(t, err)
+
+	growFn = inst.Function(2)
+	_, err = inst.Function(1).Call(context.Background())
+	require.NoError(t, err)
+}
+
 func RunTestEngine_NewModuleEngine(t *testing.T, et EngineTester) {
 	e := et.NewEngine(api.CoreFeaturesV1)
 
diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go
index 9839ac9f..6bf582f4 100644
--- a/internal/wasm/module_test.go
+++ b/internal/wasm/module_test.go
@@ -842,10 +842,15 @@ func TestModule_buildMemoryInstance(t *testing.T) {
 	t.Run("non-nil", func(t *testing.T) {
 		min := uint32(1)
 		max := uint32(10)
-		m := Module{MemorySection: &Memory{Min: min, Cap: min, Max: max}}
+		mDef := &MemoryDefinition{moduleName: "foo"}
+		m := Module{
+			MemorySection:           &Memory{Min: min, Cap: min, Max: max},
+			MemoryDefinitionSection: []*MemoryDefinition{mDef},
+		}
 		mem := m.buildMemory()
 		require.Equal(t, min, mem.Min)
 		require.Equal(t, max, mem.Max)
+		require.Equal(t, mDef, mem.definition)
 	})
 }
 
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index e0c95d12..95017b5f 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -29,33 +29,35 @@ func TestModuleInstance_Memory(t *testing.T) {
 			input: &Module{},
 		},
 		{
-			name:  "memory not exported",
-			input: &Module{MemorySection: &Memory{Min: 1, Cap: 1}},
-		},
-		{
-			name:  "memory not exported, one page",
-			input: &Module{MemorySection: &Memory{Min: 1, Cap: 1}},
+			name: "memory not exported, one page",
+			input: &Module{
+				MemorySection:           &Memory{Min: 1, Cap: 1},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+			},
 		},
 		{
 			name: "memory exported, different name",
 			input: &Module{
-				MemorySection: &Memory{Min: 1, Cap: 1},
-				ExportSection: []*Export{{Type: ExternTypeMemory, Name: "momory", Index: 0}},
+				MemorySection:           &Memory{Min: 1, Cap: 1},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+				ExportSection:           []*Export{{Type: ExternTypeMemory, Name: "momory", Index: 0}},
 			},
 		},
 		{
 			name: "memory exported, but zero length",
 			input: &Module{
-				MemorySection: &Memory{},
-				ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
+				MemorySection:           &Memory{},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+				ExportSection:           []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
 			},
 			expected: true,
 		},
 		{
 			name: "memory exported, one page",
 			input: &Module{
-				MemorySection: &Memory{Min: 1, Cap: 1},
-				ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
+				MemorySection:           &Memory{Min: 1, Cap: 1},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+				ExportSection:           []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
 			},
 			expected:    true,
 			expectedLen: 65536,
@@ -63,8 +65,9 @@ func TestModuleInstance_Memory(t *testing.T) {
 		{
 			name: "memory exported, two pages",
 			input: &Module{
-				MemorySection: &Memory{Min: 2, Cap: 2},
-				ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
+				MemorySection:           &Memory{Min: 2, Cap: 2},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+				ExportSection:           []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
 			},
 			expected:    true,
 			expectedLen: 65536 * 2,
@@ -151,11 +154,12 @@ func TestStore_CloseWithExitCode(t *testing.T) {
 			require.NoError(t, err)
 
 			m2, err := s.Instantiate(testCtx, ns, &Module{
-				TypeSection:   []*FunctionType{v_v},
-				ImportSection: []*Import{{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}},
-				MemorySection: &Memory{Min: 1, Cap: 1},
-				GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}},
-				TableSection:  []*Table{{Min: 10}},
+				TypeSection:             []*FunctionType{v_v},
+				ImportSection:           []*Import{{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}},
+				MemorySection:           &Memory{Min: 1, Cap: 1},
+				MemoryDefinitionSection: []*MemoryDefinition{{}},
+				GlobalSection:           []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}},
+				TableSection:            []*Table{{Min: 10}},
 			}, importingModuleName, nil)
 			require.NoError(t, err)
 
@@ -191,10 +195,11 @@ func TestStore_hammer(t *testing.T) {
 	require.True(t, ok)
 
 	importingModule := &Module{
-		TypeSection:     []*FunctionType{v_v},
-		FunctionSection: []uint32{0},
-		CodeSection:     []*Code{{Body: []byte{OpcodeEnd}}},
-		MemorySection:   &Memory{Min: 1, Cap: 1},
+		TypeSection:             []*FunctionType{v_v},
+		FunctionSection:         []uint32{0},
+		CodeSection:             []*Code{{Body: []byte{OpcodeEnd}}},
+		MemorySection:           &Memory{Min: 1, Cap: 1},
+		MemoryDefinitionSection: []*MemoryDefinition{{}},
 		GlobalSection: []*Global{{
 			Type: &GlobalType{ValType: ValueTypeI32},
 			Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(1)},
diff --git a/runtime_test.go b/runtime_test.go
index 310cfc1c..8c749bb4 100644
--- a/runtime_test.go
+++ b/runtime_test.go
@@ -225,8 +225,13 @@ func TestModule_Memory(t *testing.T) {
 			mem := module.ExportedMemory("memory")
 			if tc.expected {
 				require.Equal(t, tc.expectedLen, mem.Size())
+				defs := module.ExportedMemoryDefinitions()
+				require.Equal(t, 1, len(defs))
+				def := defs["memory"]
+				require.Equal(t, tc.expectedLen>>16, def.Min())
 			} else {
 				require.Nil(t, mem)
+				require.Zero(t, len(module.ExportedMemoryDefinitions()))
 			}
 		})
 	}
@@ -525,7 +530,11 @@ func TestRuntime_CloseWithExitCode(t *testing.T) {
 			require.NoError(t, err)
 
 			func1 := m1.ExportedFunction("func")
+			require.Equal(t, map[string]api.FunctionDefinition{"func": func1.Definition()},
+				m1.ExportedFunctionDefinitions())
 			func2 := m2.ExportedFunction("func")
+			require.Equal(t, map[string]api.FunctionDefinition{"func": func2.Definition()},
+				m2.ExportedFunctionDefinitions())
 
 			// Modules not closed so calls succeed