Repository: tetratelabs/wazero
Pull request #441 (2022-04-05 07:16:34)

Adds ModuleBuilder.ExportGlobalXXX to configure constants
I think this is the last to port @summerwind's lovely codebase

See https://github.com/summerwind/the-art-of-webassembly-go/issues/1


Patch:
diff --git a/builder.go b/builder.go
index e07063f7..783eeab7 100644
--- a/builder.go
+++ b/builder.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/leb128"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -14,18 +15,22 @@ import (
 //	hello := func() {
 //		fmt.Fprintln(stdout, "hello!")
 //	}
-//	_, err := r.NewModuleBuilder("env").ExportFunction("hello", hello).Instantiate()
+//	env, _ := 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()
+//	env, _ := r.NewModuleBuilder("env").ExportFunction("get_random_string", getRandomString).Build()
 //
-//	_, err := r.InstantiateModule(env.WithName("env.1"))
-//	_, err := r.InstantiateModule(env.WithName("env.2"))
+//	env1, _ := r.InstantiateModuleWithConfig(env, NewModuleConfig().WithName("env.1"))
+//	defer env1.Close()
+//
+//	env2, _ := r.InstantiateModuleWithConfig(env, NewModuleConfig().WithName("env.2"))
+//	defer env2.Close()
 //
 // 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 {
+	// Note: until golang/go#5860, we can't use example tests to embed code in interface godocs.
 
 	// ExportFunction adds a function written in Go, which a WebAssembly module can import.
 	//
@@ -74,6 +79,10 @@ type ModuleBuilder interface {
 	// * name - the name to export. Ex "memory" for wasi.ModuleSnapshotPreview1
 	// * minPages - the possibly zero initial size in pages (65536 bytes per page).
 	//
+	// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
+	//	// (memory (export "memory") 1)
+	//	builder.ExportMemory(1)
+	//
 	// Note: This is allowed to grow to RuntimeConfig.WithMemoryMaxPages (4GiB). To bound it, use ExportMemoryWithMax.
 	// Note: If a memory is already exported with the same name, this overwrites it.
 	// Note: Version 1.0 (20191205) of the WebAssembly spec allows at most one memory per module.
@@ -82,9 +91,53 @@ type ModuleBuilder interface {
 
 	// ExportMemoryWithMax is like ExportMemory, but can prevent overuse of memory.
 	//
+	// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
+	//	// (memory (export "memory") 1 1)
+	//	builder.ExportMemoryWithMax(1, 1)
+	//
 	// Note: maxPages must be at least minPages and no larger than RuntimeConfig.WithMemoryMaxPages
 	ExportMemoryWithMax(name string, minPages, maxPages uint32) ModuleBuilder
 
+	// ExportGlobalI32 exports a global constant of type api.ValueTypeI32.
+	//
+	// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
+	//	// (global (export "canvas_width") i32 (i32.const 1024))
+	//	builder.ExportGlobalI32("canvas_width", 1024)
+	//
+	// Note: If a global is already exported with the same name, this overwrites it.
+	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-globaltype
+	ExportGlobalI32(name string, v uint32) ModuleBuilder
+
+	// ExportGlobalI64 exports a global constant of type api.ValueTypeI64.
+	//
+	// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
+	//	// (global (export "start_epoch") i64 (i64.const 1620216263544))
+	//	builder.ExportGlobalI64("start_epoch", 1620216263544)
+	//
+	// Note: If a global is already exported with the same name, this overwrites it.
+	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-globaltype
+	ExportGlobalI64(name string, v uint64) ModuleBuilder
+
+	// ExportGlobalF32 exports a global constant of type api.ValueTypeF32.
+	//
+	// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
+	//	// (global (export "math/pi") f32 (f32.const 3.1415926536))
+	//	builder.ExportGlobalF32("math/pi", 3.1415926536)
+	//
+	// Note: If a global is already exported with the same name, this overwrites it.
+	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-globaltype
+	ExportGlobalF32(name string, v float32) ModuleBuilder
+
+	// ExportGlobalF64 exports a global constant of type api.ValueTypeF64.
+	//
+	// For example, the WebAssembly 1.0 Text Format below is the equivalent of this builder method:
+	//	// (global (export "math/pi") f64 (f64.const 3.14159265358979323846264338327950288419716939937510582097494459))
+	//	builder.ExportGlobalF64("math/pi", 3.14159265358979323846264338327950288419716939937510582097494459)
+	//
+	// Note: If a global is already exported with the same name, this overwrites it.
+	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-globaltype
+	ExportGlobalF64(name string, v float64) ModuleBuilder
+
 	// Build returns a module to instantiate, or returns an error if any of the configuration is invalid.
 	Build() (*CompiledCode, error)
 
@@ -100,6 +153,7 @@ type moduleBuilder struct {
 	moduleName   string
 	nameToGoFunc map[string]interface{}
 	nameToMemory map[string]*wasm.Memory
+	nameToGlobal map[string]*wasm.Global
 }
 
 // NewModuleBuilder implements Runtime.NewModuleBuilder
@@ -109,6 +163,7 @@ func (r *runtime) NewModuleBuilder(moduleName string) ModuleBuilder {
 		moduleName:   moduleName,
 		nameToGoFunc: map[string]interface{}{},
 		nameToMemory: map[string]*wasm.Memory{},
+		nameToGlobal: map[string]*wasm.Global{},
 	}
 }
 
@@ -138,6 +193,42 @@ func (b *moduleBuilder) ExportMemoryWithMax(name string, minPages, maxPages uint
 	return b
 }
 
+// ExportGlobalI32 implements ModuleBuilder.ExportGlobalI32
+func (b *moduleBuilder) ExportGlobalI32(name string, v uint32) ModuleBuilder {
+	b.nameToGlobal[name] = &wasm.Global{
+		Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32},
+		Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeUint32(v)},
+	}
+	return b
+}
+
+// ExportGlobalI64 implements ModuleBuilder.ExportGlobalI64
+func (b *moduleBuilder) ExportGlobalI64(name string, v uint64) ModuleBuilder {
+	b.nameToGlobal[name] = &wasm.Global{
+		Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64},
+		Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeUint64(v)},
+	}
+	return b
+}
+
+// ExportGlobalF32 implements ModuleBuilder.ExportGlobalF32
+func (b *moduleBuilder) ExportGlobalF32(name string, v float32) ModuleBuilder {
+	b.nameToGlobal[name] = &wasm.Global{
+		Type: &wasm.GlobalType{ValType: wasm.ValueTypeF32},
+		Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF32Const, Data: leb128.EncodeUint64(api.EncodeF32(v))},
+	}
+	return b
+}
+
+// ExportGlobalF64 implements ModuleBuilder.ExportGlobalF64
+func (b *moduleBuilder) ExportGlobalF64(name string, v float64) ModuleBuilder {
+	b.nameToGlobal[name] = &wasm.Global{
+		Type: &wasm.GlobalType{ValType: wasm.ValueTypeF64},
+		Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF64Const, Data: leb128.EncodeUint64(api.EncodeF64(v))},
+	}
+	return b
+}
+
 // Build implements ModuleBuilder.Build
 func (b *moduleBuilder) Build() (*CompiledCode, error) {
 	// Verify the maximum limit here, so we don't have to pass it to wasm.NewHostModule
@@ -150,7 +241,7 @@ func (b *moduleBuilder) Build() (*CompiledCode, error) {
 	}
 
 	// TODO: we can use r.enabledFeatures to fail early on things like mutable globals
-	if module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.nameToMemory); err != nil {
+	if module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.nameToMemory, b.nameToGlobal); err != nil {
 		return nil, err
 	} else {
 		return &CompiledCode{module: module}, nil
diff --git a/internal/wasm/host.go b/internal/wasm/host.go
index db34687d..b3398126 100644
--- a/internal/wasm/host.go
+++ b/internal/wasm/host.go
@@ -10,7 +10,7 @@ import (
 )
 
 // 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{}, nameToMemory map[string]*Memory) (m *Module, err error) {
+func NewHostModule(moduleName string, nameToGoFunc map[string]interface{}, nameToMemory map[string]*Memory, nameToGlobal map[string]*Global) (m *Module, err error) {
 	if moduleName != "" {
 		m = &Module{NameSection: &NameSection{ModuleName: moduleName}}
 	} else {
@@ -19,7 +19,8 @@ func NewHostModule(moduleName string, nameToGoFunc map[string]interface{}, nameT
 
 	funcCount := uint32(len(nameToGoFunc))
 	memoryCount := uint32(len(nameToMemory))
-	exportCount := funcCount + memoryCount
+	globalCount := uint32(len(nameToGlobal))
+	exportCount := funcCount + memoryCount + globalCount
 	if exportCount > 0 {
 		m.ExportSection = make(map[string]*Export, exportCount)
 	}
@@ -35,6 +36,12 @@ func NewHostModule(moduleName string, nameToGoFunc map[string]interface{}, nameT
 			return
 		}
 	}
+
+	if globalCount > 0 {
+		if err = addGlobals(m, nameToGlobal); err != nil {
+			return
+		}
+	}
 	return
 }
 
@@ -101,6 +108,26 @@ func addMemory(m *Module, nameToMemory map[string]*Memory) error {
 	return nil
 }
 
+func addGlobals(m *Module, globals map[string]*Global) error {
+	globalCount := len(globals)
+	m.GlobalSection = make([]*Global, 0, globalCount)
+
+	globalNames := make([]string, 0, globalCount)
+	for name := range globals {
+		if e, ok := m.ExportSection[name]; ok { // Exports cannot collide on names, regardless of type.
+			return fmt.Errorf("global[%s] exports the same name as a %s", name, ExternTypeName(e.Type))
+		}
+		globalNames = append(globalNames, name)
+	}
+	sort.Strings(globalNames) // For consistent iteration order
+
+	for i, name := range globalNames {
+		m.GlobalSection = append(m.GlobalSection, globals[name])
+		m.ExportSection[name] = &Export{Type: ExternTypeGlobal, Name: name, Index: Index(i)}
+	}
+	return nil
+}
+
 func (m *Module) maybeAddType(ft *FunctionType) Index {
 	for i, t := range m.TypeSection {
 		if t.EqualsSignature(ft.Params, ft.Results) {
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index 9e20bae4..23f0b274 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -133,6 +133,7 @@ type (
 		Type *GlobalType
 		// Val holds a 64-bit representation of the actual value.
 		Val uint64
+		// ^^ TODO: this should be guarded with atomics when mutable
 	}
 
 	// FunctionTypeID is a uniquely assigned integer for a function type.


Test patch:
diff --git a/builder_test.go b/builder_test.go
index d2a8f777..b567a35a 100644
--- a/builder_test.go
+++ b/builder_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/leb128"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -43,18 +44,6 @@ func TestNewModuleBuilder_Build(t *testing.T) {
 			},
 			expected: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "env"}},
 		},
-		{
-			name: "ExportMemory",
-			input: func(r Runtime) ModuleBuilder {
-				return r.NewModuleBuilder("").ExportMemory("memory", 1)
-			},
-			expected: &wasm.Module{
-				MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages},
-				ExportSection: map[string]*wasm.Export{
-					"memory": {Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
-				},
-			},
-		},
 		{
 			name: "ExportFunction",
 			input: func(r Runtime) ModuleBuilder {
@@ -164,6 +153,190 @@ func TestNewModuleBuilder_Build(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "ExportMemory",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportMemory("memory", 1)
+			},
+			expected: &wasm.Module{
+				MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages},
+				ExportSection: map[string]*wasm.Export{
+					"memory": {Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
+				},
+			},
+		},
+		{
+			name: "ExportMemory overwrites",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportMemory("memory", 1).ExportMemory("memory", 2)
+			},
+			expected: &wasm.Module{
+				MemorySection: &wasm.Memory{Min: 2, Max: wasm.MemoryMaxPages},
+				ExportSection: map[string]*wasm.Export{
+					"memory": {Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
+				},
+			},
+		},
+		{
+			name: "ExportMemoryWithMax",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportMemoryWithMax("memory", 1, 1)
+			},
+			expected: &wasm.Module{
+				MemorySection: &wasm.Memory{Min: 1, Max: 1},
+				ExportSection: map[string]*wasm.Export{
+					"memory": {Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
+				},
+			},
+		},
+		{
+			name: "ExportMemoryWithMax overwrites",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportMemoryWithMax("memory", 1, 1).ExportMemoryWithMax("memory", 1, 2)
+			},
+			expected: &wasm.Module{
+				MemorySection: &wasm.Memory{Min: 1, Max: 2},
+				ExportSection: map[string]*wasm.Export{
+					"memory": {Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
+				},
+			},
+		},
+		{
+			name: "ExportGlobalI32",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportGlobalI32("canvas_width", 1024)
+			},
+			expected: &wasm.Module{
+				GlobalSection: []*wasm.Global{
+					{
+						Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32},
+						Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeUint32(1024)},
+					},
+				},
+				ExportSection: map[string]*wasm.Export{
+					"canvas_width": {Name: "canvas_width", Type: wasm.ExternTypeGlobal, Index: 0},
+				},
+			},
+		},
+		{
+			name: "ExportGlobalI32 overwrites",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportGlobalI32("canvas_width", 1024).ExportGlobalI32("canvas_width", 2048)
+			},
+			expected: &wasm.Module{
+				GlobalSection: []*wasm.Global{
+					{
+						Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32},
+						Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeUint32(2048)},
+					},
+				},
+				ExportSection: map[string]*wasm.Export{
+					"canvas_width": {Name: "canvas_width", Type: wasm.ExternTypeGlobal, Index: 0},
+				},
+			},
+		},
+		{
+			name: "ExportGlobalI64",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportGlobalI64("start_epoch", 1620216263544)
+			},
+			expected: &wasm.Module{
+				GlobalSection: []*wasm.Global{
+					{
+						Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64},
+						Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeUint64(1620216263544)},
+					},
+				},
+				ExportSection: map[string]*wasm.Export{
+					"start_epoch": {Name: "start_epoch", Type: wasm.ExternTypeGlobal, Index: 0},
+				},
+			},
+		},
+		{
+			name: "ExportGlobalI64 overwrites",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportGlobalI64("start_epoch", 1620216263544).ExportGlobalI64("start_epoch", 1620216263544000)
+			},
+			expected: &wasm.Module{
+				GlobalSection: []*wasm.Global{
+					{
+						Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64},
+						Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeUint64(1620216263544000)},
+					},
+				},
+				ExportSection: map[string]*wasm.Export{
+					"start_epoch": {Name: "start_epoch", Type: wasm.ExternTypeGlobal, Index: 0},
+				},
+			},
+		},
+		{
+			name: "ExportGlobalF32",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportGlobalF32("math/pi", 3.1415926536)
+			},
+			expected: &wasm.Module{
+				GlobalSection: []*wasm.Global{
+					{
+						Type: &wasm.GlobalType{ValType: wasm.ValueTypeF32},
+						Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF32Const, Data: leb128.EncodeUint64(api.EncodeF32(3.1415926536))},
+					},
+				},
+				ExportSection: map[string]*wasm.Export{
+					"math/pi": {Name: "math/pi", Type: wasm.ExternTypeGlobal, Index: 0},
+				},
+			},
+		},
+		{
+			name: "ExportGlobalF32 overwrites",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportGlobalF32("math/pi", 3.1415926536).ExportGlobalF32("math/pi", 3.14159)
+			},
+			expected: &wasm.Module{
+				GlobalSection: []*wasm.Global{
+					{
+						Type: &wasm.GlobalType{ValType: wasm.ValueTypeF32},
+						Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF32Const, Data: leb128.EncodeUint64(api.EncodeF32(3.14159))},
+					},
+				},
+				ExportSection: map[string]*wasm.Export{
+					"math/pi": {Name: "math/pi", Type: wasm.ExternTypeGlobal, Index: 0},
+				},
+			},
+		},
+		{
+			name: "ExportGlobalF64",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportGlobalF64("math/pi", math.Pi)
+			},
+			expected: &wasm.Module{
+				GlobalSection: []*wasm.Global{
+					{
+						Type: &wasm.GlobalType{ValType: wasm.ValueTypeF64},
+						Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF64Const, Data: leb128.EncodeUint64(api.EncodeF64(math.Pi))},
+					},
+				},
+				ExportSection: map[string]*wasm.Export{
+					"math/pi": {Name: "math/pi", Type: wasm.ExternTypeGlobal, Index: 0},
+				},
+			},
+		},
+		{
+			name: "ExportGlobalF64 overwrites",
+			input: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder("").ExportGlobalF64("math/pi", math.Pi).ExportGlobalF64("math/pi", 3.14159)
+			},
+			expected: &wasm.Module{
+				GlobalSection: []*wasm.Global{
+					{
+						Type: &wasm.GlobalType{ValType: wasm.ValueTypeF64},
+						Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF64Const, Data: leb128.EncodeUint64(api.EncodeF64(3.14159))},
+					},
+				},
+				ExportSection: map[string]*wasm.Export{
+					"math/pi": {Name: "math/pi", Type: wasm.ExternTypeGlobal, Index: 0},
+				},
+			},
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go
index bf72e036..e64bd5e6 100644
--- a/internal/wasm/host_test.go
+++ b/internal/wasm/host_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/leb128"
 )
 
 // wasiAPI simulates the real WASI api
@@ -38,6 +39,7 @@ func TestNewHostModule(t *testing.T) {
 		name, moduleName string
 		nameToGoFunc     map[string]interface{}
 		nameToMemory     map[string]*Memory
+		nameToGlobal     map[string]*Global
 		expected         *Module
 	}{
 		{
@@ -50,17 +52,7 @@ func TestNewHostModule(t *testing.T) {
 			expected:   &Module{NameSection: &NameSection{ModuleName: "test"}},
 		},
 		{
-			name:         "memory",
-			nameToMemory: map[string]*Memory{"memory": {1, 2}},
-			expected: &Module{
-				MemorySection: &Memory{Min: 1, Max: 2},
-				ExportSection: map[string]*Export{
-					"memory": {Name: "memory", Type: ExternTypeMemory, Index: 0},
-				},
-			},
-		},
-		{
-			name:       "two struct funcs",
+			name:       "funcs",
 			moduleName: "wasi_snapshot_preview1",
 			nameToGoFunc: map[string]interface{}{
 				functionArgsSizesGet: a.ArgsSizesGet,
@@ -86,6 +78,45 @@ func TestNewHostModule(t *testing.T) {
 				},
 			},
 		},
+		{
+			name:         "memory",
+			nameToMemory: map[string]*Memory{"memory": {1, 2}},
+			expected: &Module{
+				MemorySection: &Memory{Min: 1, Max: 2},
+				ExportSection: map[string]*Export{
+					"memory": {Name: "memory", Type: ExternTypeMemory, Index: 0},
+				},
+			},
+		},
+		{
+			name: "globals",
+			nameToGlobal: map[string]*Global{
+				"g2": {
+					Type: &GlobalType{ValType: i32},
+					Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(2)},
+				},
+				"g1": {
+					Type: &GlobalType{ValType: i32},
+					Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(1)},
+				},
+			},
+			expected: &Module{
+				GlobalSection: []*Global{
+					{
+						Type: &GlobalType{ValType: i32},
+						Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(1)},
+					},
+					{
+						Type: &GlobalType{ValType: i32},
+						Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(2)},
+					},
+				},
+				ExportSection: map[string]*Export{
+					"g1": {Name: "g1", Type: ExternTypeGlobal, Index: 0},
+					"g2": {Name: "g2", Type: ExternTypeGlobal, Index: 1},
+				},
+			},
+		},
 		{
 			name:       "one of each",
 			moduleName: "env",
@@ -95,17 +126,30 @@ func TestNewHostModule(t *testing.T) {
 			nameToMemory: map[string]*Memory{
 				"memory": {1, 1},
 			},
+			nameToGlobal: map[string]*Global{
+				"g": {
+					Type: &GlobalType{ValType: i32},
+					Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(1)},
+				},
+			},
 			expected: &Module{
 				TypeSection: []*FunctionType{
 					{Params: []ValueType{i32, i32}, Results: []ValueType{i32}},
 				},
 				FunctionSection:     []Index{0},
 				HostFunctionSection: []*reflect.Value{&fnArgsSizesGet},
+				GlobalSection: []*Global{
+					{
+						Type: &GlobalType{ValType: i32},
+						Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeUint32(1)},
+					},
+				},
+				MemorySection: &Memory{Min: 1, Max: 1},
 				ExportSection: map[string]*Export{
 					"args_sizes_get": {Name: "args_sizes_get", Type: ExternTypeFunc, Index: 0},
 					"memory":         {Name: "memory", Type: ExternTypeMemory, Index: 0},
+					"g":              {Name: "g", Type: ExternTypeGlobal, Index: 0},
 				},
-				MemorySection: &Memory{Min: 1, Max: 1},
 				NameSection: &NameSection{
 					ModuleName: "env",
 					FunctionNames: NameMap{
@@ -120,7 +164,7 @@ func TestNewHostModule(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			m, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory)
+			m, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory, tc.nameToGlobal)
 			require.NoError(t, e)
 			requireHostModuleEquals(t, tc.expected, m)
 		})
@@ -154,6 +198,7 @@ func TestNewHostModule_Errors(t *testing.T) {
 		name, moduleName string
 		nameToGoFunc     map[string]interface{}
 		nameToMemory     map[string]*Memory
+		nameToGlobal     map[string]*Global
 		expectedErr      string
 	}{
 		{
@@ -177,13 +222,19 @@ func TestNewHostModule_Errors(t *testing.T) {
 			nameToMemory: map[string]*Memory{"memory": {1, 0}},
 			expectedErr:  "memory[memory] min 1 pages (64 Ki) > max 0 pages (0 Ki)",
 		},
+		{
+			name:         "global collides on func name",
+			nameToGoFunc: map[string]interface{}{"fn": ArgsSizesGet},
+			nameToGlobal: map[string]*Global{"fn": {}},
+			expectedErr:  "global[fn] exports the same name as a func",
+		},
 	}
 
 	for _, tt := range tests {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			_, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory)
+			_, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory, tc.nameToGlobal)
 			require.EqualError(t, e, tc.expectedErr)
 		})
 	}
diff --git a/internal/wasm/jit/engine_test.go b/internal/wasm/jit/engine_test.go
index aad23b25..6f3fdd56 100644
--- a/internal/wasm/jit/engine_test.go
+++ b/internal/wasm/jit/engine_test.go
@@ -408,7 +408,7 @@ func TestJIT_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.Memory{})
+	}}, map[string]*wasm.Memory{}, map[string]*wasm.Global{})
 	require.NoError(t, err)
 
 	_, err = store.Instantiate(context.Background(), hm, hostModuleName, nil)
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index 77864297..40a682da 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -90,7 +90,7 @@ func TestModuleInstance_Memory(t *testing.T) {
 
 func TestStore_Instantiate(t *testing.T) {
 	s := newStore()
-	m, err := NewHostModule("", map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{})
+	m, err := NewHostModule("", map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
 	require.NoError(t, err)
 
 	type key string
@@ -120,7 +120,7 @@ func TestStore_CloseModule(t *testing.T) {
 		{
 			name: "Module imports HostModule",
 			initializer: func(t *testing.T, s *Store) {
-				m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{})
+				m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
 				require.NoError(t, err)
 				_, err = s.Instantiate(context.Background(), m, importedModuleName, nil)
 				require.NoError(t, err)
@@ -177,7 +177,7 @@ func TestStore_CloseModule(t *testing.T) {
 func TestStore_hammer(t *testing.T) {
 	const importedModuleName = "imported"
 
-	m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{})
+	m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
 	require.NoError(t, err)
 
 	s := newStore()
@@ -227,7 +227,7 @@ func TestStore_Instantiate_Errors(t *testing.T) {
 	const importedModuleName = "imported"
 	const importingModuleName = "test"
 
-	m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{})
+	m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
 	require.NoError(t, err)
 
 	t.Run("Fails if module name already in use", func(t *testing.T) {
@@ -312,7 +312,7 @@ func TestStore_Instantiate_Errors(t *testing.T) {
 }
 
 func TestModuleContext_ExportedFunction(t *testing.T) {
-	host, err := NewHostModule("host", map[string]interface{}{"host_fn": func(api.Module) {}}, map[string]*Memory{})
+	host, err := NewHostModule("host", map[string]interface{}{"host_fn": func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
 	require.NoError(t, err)
 
 	s := newStore()
@@ -347,7 +347,7 @@ func TestFunctionInstance_Call(t *testing.T) {
 	functionName := "fn"
 
 	// This is a fake engine, so we don't capture inside the function body.
-	m, err := NewHostModule("host", map[string]interface{}{functionName: func(api.Module) {}}, map[string]*Memory{})
+	m, err := NewHostModule("host", map[string]interface{}{functionName: func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
 	require.NoError(t, err)
 
 	// Add the host module
diff --git a/wasm_test.go b/wasm_test.go
index e3b5c707..a1a7aba6 100644
--- a/wasm_test.go
+++ b/wasm_test.go
@@ -167,6 +167,7 @@ func TestModule_Global(t *testing.T) {
 	tests := []struct {
 		name                      string
 		module                    *wasm.Module // module as wat doesn't yet support globals
+		builder                   func(Runtime) ModuleBuilder
 		expected, expectedMutable bool
 	}{
 		{
@@ -186,16 +187,8 @@ func TestModule_Global(t *testing.T) {
 		},
 		{
 			name: "global exported",
-			module: &wasm.Module{
-				GlobalSection: []*wasm.Global{
-					{
-						Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64},
-						Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: []byte{1}},
-					},
-				},
-				ExportSection: map[string]*wasm.Export{
-					"global": {Type: wasm.ExternTypeGlobal, Name: "global"},
-				},
+			builder: func(r Runtime) ModuleBuilder {
+				return r.NewModuleBuilder(t.Name()).ExportGlobalI64("global", 1)
 			},
 			expected: true,
 		},
@@ -222,8 +215,15 @@ func TestModule_Global(t *testing.T) {
 
 		r := NewRuntime()
 		t.Run(tc.name, func(t *testing.T) {
+			var code *CompiledCode
+			if tc.module != nil {
+				code = &CompiledCode{module: tc.module}
+			} else {
+				code, _ = tc.builder(r).Build()
+			}
+
 			// Instantiate the module and get the export of the above global
-			module, err := r.InstantiateModule(&CompiledCode{module: tc.module})
+			module, err := r.InstantiateModule(code)
 			require.NoError(t, err)
 			defer module.Close()