Repository: tetratelabs/wazero
Pull request #1182 (2023-03-01 18:06:29)

Ensure `nil` is accepted as FS config
In previous releases, passing a `nil` value as an FS config did not cause any error. However, in 1.0.0-rc.1 this leads
to the creation of an invalid `adapter{fs: nil}`, which eventually leads to a panic (nil):

    (f *FileEntry) Stat(st *platform.Stat_t) =>
        (r *lazyDir) file() =>
            r.fs.OpenFile(".", os.O_RDONLY, 0)

with fs == nil

The backwards-compatible fix is to make Adapt() return UnimplementedFS.

However, we may want to consider returning an error somewhere, because configuring a nil FS may be unintended.

Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>


evacchi: I originally did it that way and figured that it would do the same if I handled at Adapt; but it's ok to be a tad more defensive. I pushed the following, which avoids invoking `withFSConfig()` when fs is nil

```go
// WithFS implements ModuleConfig.WithFS
func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig {
	if fs == nil {
		return c
	}
	return c.WithFSConfig(NewFSConfig().WithFSMount(fs, ""))
}
```


codefromthecrypt: sorry to be a nitpicker, but any WithXXX(nil) should clear previous don't you think?
evacchi: oh right I didn't think about that case!

Patch:
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 988e257f..791ca5c9 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -50,15 +50,23 @@ jobs:
         run: sudo apt update -qq && sudo apt install -qq -y wixl osslsigncode
 
       - name: Download Windows code signing certificate
-        run: |
-          echo ${{ secrets.WINDOWS_CODESIGN_P12_BASE64 }} | base64 --decode > windows-certificate.p12
-          echo "WINDOWS_CODESIGN_P12=windows-certificate.p12" >> $GITHUB_ENV
+        env:
+          WINDOWS_CODESIGN_P12_BASE64: ${{ secrets.WINDOWS_CODESIGN_P12_BASE64 }}
+        run: | # On the fork PRs, our org secret is not visible.
+          if [ $WINDOWS_CODESIGN_P12_BASE64 ]; then
+            echo $WINDOWS_CODESIGN_P12_BASE64 | base64 --decode > windows-certificate.p12
+            echo "WINDOWS_CODESIGN_P12=windows-certificate.p12" >> $GITHUB_ENV
+          fi
 
       - name: Make artifacts (test)
         if: github.event_name != 'push' || !contains(github.ref, 'refs/tags/')
         env:
+          WINDOWS_CODESIGN_P12_BASE64: ${{ secrets.WINDOWS_CODESIGN_P12_BASE64 }}
           WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
-        run: |
+        run: | # On the fork PRs, our org secret is not visible. We unset the required env so that `make dist` uses default self-signed cert.
+          if [ -n $WINDOWS_CODESIGN_P12_BASE64 ]; then
+            unset WINDOWS_CODESIGN_PASSWORD
+          fi
           VERSION=${{ github.sha }}
           make dist VERSION=$VERSION
           echo "VERSION=${VERSION}" >> $GITHUB_ENV
@@ -67,6 +75,7 @@ jobs:
         # Triggers only on tag creation.
         if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
         env:
+          WINDOWS_CODESIGN_P12_BASE64: ${{ secrets.WINDOWS_CODESIGN_P12_BASE64 }}
           WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
         run: | # Note: MSI_VERSION requires . as a separator, so replace "-" in the tag with ".".
           VERSION=${GITHUB_REF#refs/tags/v}
diff --git a/config.go b/config.go
index 4f7d0fd5..df4fa8f3 100644
--- a/config.go
+++ b/config.go
@@ -667,7 +667,11 @@ func (c *moduleConfig) WithEnv(key, value string) ModuleConfig {
 
 // WithFS implements ModuleConfig.WithFS
 func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig {
-	return c.WithFSConfig(NewFSConfig().WithFSMount(fs, ""))
+	var config FSConfig
+	if fs != nil {
+		config = NewFSConfig().WithFSMount(fs, "")
+	}
+	return c.WithFSConfig(config)
 }
 
 // WithFSConfig implements ModuleConfig.WithFSConfig
diff --git a/internal/sysfs/adapter.go b/internal/sysfs/adapter.go
index dda320ba..9c0dfa3a 100644
--- a/internal/sysfs/adapter.go
+++ b/internal/sysfs/adapter.go
@@ -19,6 +19,9 @@ import (
 // documentation does not require the file to be present. In summary, we can't
 // enforce flag behavior.
 func Adapt(fs fs.FS) FS {
+	if fs == nil {
+		return UnimplementedFS{}
+	}
 	if sys, ok := fs.(FS); ok {
 		return sys
 	}
diff --git a/internal/wasm/binary/encoder.go b/internal/testing/binaryencoding/encoder.go
similarity index 90%
rename from internal/wasm/binary/encoder.go
rename to internal/testing/binaryencoding/encoder.go
index 65f6029a..5ffe3a10 100644
--- a/internal/wasm/binary/encoder.go
+++ b/internal/testing/binaryencoding/encoder.go
@@ -1,4 +1,4 @@
-package binary
+package binaryencoding
 
 import (
 	"github.com/tetratelabs/wazero/internal/wasm"
@@ -18,7 +18,7 @@ func EncodeModule(m *wasm.Module) (bytes []byte) {
 		bytes = append(bytes, encodeImportSection(m.ImportSection)...)
 	}
 	if m.SectionElementCount(wasm.SectionIDFunction) > 0 {
-		bytes = append(bytes, encodeFunctionSection(m.FunctionSection)...)
+		bytes = append(bytes, EncodeFunctionSection(m.FunctionSection)...)
 	}
 	if m.SectionElementCount(wasm.SectionIDTable) > 0 {
 		bytes = append(bytes, encodeTableSection(m.TableSection)...)
@@ -33,7 +33,7 @@ func EncodeModule(m *wasm.Module) (bytes []byte) {
 		bytes = append(bytes, encodeExportSection(m.ExportSection)...)
 	}
 	if m.SectionElementCount(wasm.SectionIDStart) > 0 {
-		bytes = append(bytes, encodeStartSection(*m.StartSection)...)
+		bytes = append(bytes, EncodeStartSection(*m.StartSection)...)
 	}
 	if m.SectionElementCount(wasm.SectionIDElement) > 0 {
 		bytes = append(bytes, encodeElementSection(m.ElementSection)...)
@@ -48,7 +48,7 @@ func EncodeModule(m *wasm.Module) (bytes []byte) {
 		// >> The name section should appear only once in a module, and only after the data section.
 		// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namesec
 		if m.NameSection != nil {
-			nameSection := append(sizePrefixedName, encodeNameSectionData(m.NameSection)...)
+			nameSection := append(sizePrefixedName, EncodeNameSectionData(m.NameSection)...)
 			bytes = append(bytes, encodeSection(wasm.SectionIDCustom, nameSection)...)
 		}
 	}
diff --git a/internal/wasm/binary/code.go b/internal/wasm/binary/code.go
index e1c315f8..672cdd68 100644
--- a/internal/wasm/binary/code.go
+++ b/internal/wasm/binary/code.go
@@ -79,45 +79,3 @@ func decodeCode(r *bytes.Reader, codeSectionStart uint64) (*wasm.Code, error) {
 
 	return &wasm.Code{Body: body, LocalTypes: localTypes, BodyOffsetInCodeSection: bodyOffsetInCodeSection}, nil
 }
-
-// encodeCode returns the wasm.Code encoded in WebAssembly 1.0 (20191205) Binary Format.
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-code
-func encodeCode(c *wasm.Code) []byte {
-	if c.GoFunc != nil {
-		panic("BUG: GoFunction is not encodable")
-	}
-
-	// local blocks compress locals while preserving index order by grouping locals of the same type.
-	// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#code-section%E2%91%A0
-	localBlockCount := uint32(0) // how many blocks of locals with the same type (types can repeat!)
-	var localBlocks []byte
-	localTypeLen := len(c.LocalTypes)
-	if localTypeLen > 0 {
-		i := localTypeLen - 1
-		var runCount uint32              // count of the same type
-		var lastValueType wasm.ValueType // initialize to an invalid type 0
-
-		// iterate backwards so it is easier to size prefix
-		for ; i >= 0; i-- {
-			vt := c.LocalTypes[i]
-			if lastValueType != vt {
-				if runCount != 0 { // Only on the first iteration, this is zero when vt is compared against invalid
-					localBlocks = append(leb128.EncodeUint32(runCount), localBlocks...)
-				}
-				lastValueType = vt
-				localBlocks = append(leb128.EncodeUint32(uint32(vt)), localBlocks...) // reuse the EncodeUint32 cache
-				localBlockCount++
-				runCount = 1
-			} else {
-				runCount++
-			}
-		}
-		localBlocks = append(leb128.EncodeUint32(runCount), localBlocks...)
-		localBlocks = append(leb128.EncodeUint32(localBlockCount), localBlocks...)
-	} else {
-		localBlocks = leb128.EncodeUint32(0)
-	}
-	code := append(localBlocks, c.Body...)
-	return append(leb128.EncodeUint32(uint32(len(code))), code...)
-}
diff --git a/internal/wasm/binary/const_expr.go b/internal/wasm/binary/const_expr.go
index b391e740..db5606f6 100644
--- a/internal/wasm/binary/const_expr.go
+++ b/internal/wasm/binary/const_expr.go
@@ -103,10 +103,3 @@ func decodeConstantExpression(r *bytes.Reader, enabledFeatures api.CoreFeatures)
 
 	return &wasm.ConstantExpression{Opcode: opcode, Data: data}, nil
 }
-
-func encodeConstantExpression(expr *wasm.ConstantExpression) (ret []byte) {
-	ret = append(ret, expr.Opcode)
-	ret = append(ret, expr.Data...)
-	ret = append(ret, wasm.OpcodeEnd)
-	return
-}
diff --git a/internal/wasm/binary/data.go b/internal/wasm/binary/data.go
index aa9f6394..4a116ce3 100644
--- a/internal/wasm/binary/data.go
+++ b/internal/wasm/binary/data.go
@@ -77,12 +77,3 @@ func decodeDataSegment(r *bytes.Reader, enabledFeatures api.CoreFeatures) (*wasm
 		Init:             b,
 	}, nil
 }
-
-func encodeDataSegment(d *wasm.DataSegment) (ret []byte) {
-	// Currently multiple memories are not supported.
-	ret = append(ret, leb128.EncodeInt32(0)...)
-	ret = append(ret, encodeConstantExpression(d.OffsetExpression)...)
-	ret = append(ret, leb128.EncodeUint32(uint32(len(d.Init)))...)
-	ret = append(ret, d.Init...)
-	return
-}
diff --git a/internal/wasm/binary/element.go b/internal/wasm/binary/element.go
index 7876fed9..6f8d0307 100644
--- a/internal/wasm/binary/element.go
+++ b/internal/wasm/binary/element.go
@@ -281,20 +281,3 @@ func decodeElementSegment(r *bytes.Reader, enabledFeatures api.CoreFeatures) (*w
 		return nil, fmt.Errorf("invalid element segment prefix: 0x%x", prefix)
 	}
 }
-
-// encodeCode returns the wasm.ElementSegment encoded in WebAssembly 1.0 (20191205) Binary Format.
-//
-// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#element-section%E2%91%A0
-func encodeElement(e *wasm.ElementSegment) (ret []byte) {
-	if e.Mode == wasm.ElementModeActive {
-		ret = append(ret, leb128.EncodeInt32(int32(e.TableIndex))...)
-		ret = append(ret, encodeConstantExpression(e.OffsetExpr)...)
-		ret = append(ret, leb128.EncodeUint32(uint32(len(e.Init)))...)
-		for _, idx := range e.Init {
-			ret = append(ret, leb128.EncodeInt32(int32(*idx))...)
-		}
-	} else {
-		panic("TODO: support encoding for non-active elements in bulk-memory-operations proposal")
-	}
-	return
-}
diff --git a/internal/wasm/binary/export.go b/internal/wasm/binary/export.go
index 7b10d202..95d91cb6 100644
--- a/internal/wasm/binary/export.go
+++ b/internal/wasm/binary/export.go
@@ -31,13 +31,3 @@ func decodeExport(r *bytes.Reader) (i *wasm.Export, err error) {
 	}
 	return
 }
-
-// encodeExport returns the wasm.Export encoded in WebAssembly 1.0 (20191205) Binary Format.
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0
-func encodeExport(i *wasm.Export) []byte {
-	data := encodeSizePrefixed([]byte(i.Name))
-	data = append(data, i.Type)
-	data = append(data, leb128.EncodeUint32(i.Index)...)
-	return data
-}
diff --git a/internal/wasm/binary/function.go b/internal/wasm/binary/function.go
index f077091f..e1665478 100644
--- a/internal/wasm/binary/function.go
+++ b/internal/wasm/binary/function.go
@@ -9,16 +9,6 @@ import (
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
-// encodeFunctionType returns the wasm.FunctionType encoded in WebAssembly 1.0 (20191205) Binary Format.
-//
-// Note: Function types are encoded by the byte 0x60 followed by the respective vectors of parameter and result types.
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-types%E2%91%A4
-func encodeFunctionType(t *wasm.FunctionType) []byte {
-	// Only reached when "multi-value" is enabled because WebAssembly 1.0 (20191205) supports at most 1 result.
-	data := append([]byte{0x60}, encodeValTypes(t.Params)...)
-	return append(data, encodeValTypes(t.Results)...)
-}
-
 func decodeFunctionType(enabledFeatures api.CoreFeatures, r *bytes.Reader) (*wasm.FunctionType, error) {
 	b, err := r.ReadByte()
 	if err != nil {
diff --git a/internal/wasm/binary/global.go b/internal/wasm/binary/global.go
index 4da56562..aceaa296 100644
--- a/internal/wasm/binary/global.go
+++ b/internal/wasm/binary/global.go
@@ -52,16 +52,3 @@ func decodeGlobalType(r *bytes.Reader) (*wasm.GlobalType, error) {
 	}
 	return ret, nil
 }
-
-// encodeGlobal returns the wasm.Global encoded in WebAssembly 1.0 (20191205) Binary Format.
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0
-func encodeGlobal(g *wasm.Global) (data []byte) {
-	var mutable byte
-	if g.Type.Mutable {
-		mutable = 1
-	}
-	data = []byte{g.Type.ValType, mutable}
-	data = append(data, encodeConstantExpression(g.Init)...)
-	return
-}
diff --git a/internal/wasm/binary/import.go b/internal/wasm/binary/import.go
index d5b502ad..8f09ae74 100644
--- a/internal/wasm/binary/import.go
+++ b/internal/wasm/binary/import.go
@@ -47,35 +47,3 @@ func decodeImport(
 	}
 	return
 }
-
-// encodeImport returns the wasm.Import encoded in WebAssembly 1.0 (20191205) Binary Format.
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-import
-func encodeImport(i *wasm.Import) []byte {
-	data := encodeSizePrefixed([]byte(i.Module))
-	data = append(data, encodeSizePrefixed([]byte(i.Name))...)
-	data = append(data, i.Type)
-	switch i.Type {
-	case wasm.ExternTypeFunc:
-		data = append(data, leb128.EncodeUint32(i.DescFunc)...)
-	case wasm.ExternTypeTable:
-		data = append(data, wasm.RefTypeFuncref)
-		data = append(data, encodeLimitsType(i.DescTable.Min, i.DescTable.Max)...)
-	case wasm.ExternTypeMemory:
-		maxPtr := &i.DescMem.Max
-		if !i.DescMem.IsMaxEncoded {
-			maxPtr = nil
-		}
-		data = append(data, encodeLimitsType(i.DescMem.Min, maxPtr)...)
-	case wasm.ExternTypeGlobal:
-		g := i.DescGlobal
-		var mutable byte
-		if g.Mutable {
-			mutable = 1
-		}
-		data = append(data, g.ValType, mutable)
-	default:
-		panic(fmt.Errorf("invalid externtype: %s", wasm.ExternTypeName(i.Type)))
-	}
-	return data
-}
diff --git a/internal/wasm/binary/limits.go b/internal/wasm/binary/limits.go
index c5f07f34..1ec8d7c3 100644
--- a/internal/wasm/binary/limits.go
+++ b/internal/wasm/binary/limits.go
@@ -40,13 +40,3 @@ func decodeLimitsType(r *bytes.Reader) (min uint32, max *uint32, err error) {
 	}
 	return
 }
-
-// encodeLimitsType returns the `limitsType` (min, max) encoded in WebAssembly 1.0 (20191205) Binary Format.
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#limits%E2%91%A6
-func encodeLimitsType(min uint32, max *uint32) []byte {
-	if max == nil {
-		return append(leb128.EncodeUint32(0x00), leb128.EncodeUint32(min)...)
-	}
-	return append(leb128.EncodeUint32(0x01), append(leb128.EncodeUint32(min), leb128.EncodeUint32(*max)...)...)
-}
diff --git a/internal/wasm/binary/memory.go b/internal/wasm/binary/memory.go
index 420f9a55..fd6b32a6 100644
--- a/internal/wasm/binary/memory.go
+++ b/internal/wasm/binary/memory.go
@@ -11,7 +11,7 @@ import (
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory
 func decodeMemory(
 	r *bytes.Reader,
-	memorySizer memorySizer,
+	memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32),
 	memoryLimitPages uint32,
 ) (*wasm.Memory, error) {
 	min, maxP, err := decodeLimitsType(r)
@@ -24,14 +24,3 @@ func decodeMemory(
 
 	return mem, mem.Validate(memoryLimitPages)
 }
-
-// encodeMemory returns the wasm.Memory encoded in WebAssembly 1.0 (20191205) Binary Format.
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory
-func encodeMemory(i *wasm.Memory) []byte {
-	maxPtr := &i.Max
-	if !i.IsMaxEncoded {
-		maxPtr = nil
-	}
-	return encodeLimitsType(i.Min, maxPtr)
-}
diff --git a/internal/wasm/binary/names.go b/internal/wasm/binary/names.go
index ab02ddc6..e2249194 100644
--- a/internal/wasm/binary/names.go
+++ b/internal/wasm/binary/names.go
@@ -149,80 +149,3 @@ func decodeFunctionCount(r *bytes.Reader, subsectionID uint8) (uint32, error) {
 	}
 	return functionCount, nil
 }
-
-// encodeNameSectionData serializes the data for the "name" key in wasm.SectionIDCustom according to the
-// standard:
-//
-// Note: The result can be nil because this does not encode empty subsections
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namesec
-func encodeNameSectionData(n *wasm.NameSection) (data []byte) {
-	if n.ModuleName != "" {
-		data = append(data, encodeNameSubsection(subsectionIDModuleName, encodeSizePrefixed([]byte(n.ModuleName)))...)
-	}
-	if fd := encodeFunctionNameData(n); len(fd) > 0 {
-		data = append(data, encodeNameSubsection(subsectionIDFunctionNames, fd)...)
-	}
-	if ld := encodeLocalNameData(n); len(ld) > 0 {
-		data = append(data, encodeNameSubsection(subsectionIDLocalNames, ld)...)
-	}
-	return
-}
-
-// encodeFunctionNameData encodes the data for the function name subsection.
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-funcnamesec
-func encodeFunctionNameData(n *wasm.NameSection) []byte {
-	if len(n.FunctionNames) == 0 {
-		return nil
-	}
-
-	return encodeNameMap(n.FunctionNames)
-}
-
-func encodeNameMap(m wasm.NameMap) []byte {
-	count := uint32(len(m))
-	data := leb128.EncodeUint32(count)
-	for _, na := range m {
-		data = append(data, encodeNameAssoc(na)...)
-	}
-	return data
-}
-
-// encodeLocalNameData encodes the data for the local name subsection.
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-localnamesec
-func encodeLocalNameData(n *wasm.NameSection) []byte {
-	if len(n.LocalNames) == 0 {
-		return nil
-	}
-
-	funcNameCount := uint32(len(n.LocalNames))
-	subsection := leb128.EncodeUint32(funcNameCount)
-
-	for _, na := range n.LocalNames {
-		locals := encodeNameMap(na.NameMap)
-		subsection = append(subsection, append(leb128.EncodeUint32(na.Index), locals...)...)
-	}
-	return subsection
-}
-
-// encodeNameSubsection returns a buffer encoding the given subsection
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#subsections%E2%91%A0
-func encodeNameSubsection(subsectionID uint8, content []byte) []byte {
-	contentSizeInBytes := leb128.EncodeUint32(uint32(len(content)))
-	result := []byte{subsectionID}
-	result = append(result, contentSizeInBytes...)
-	result = append(result, content...)
-	return result
-}
-
-// encodeNameAssoc encodes the index and data prefixed by their size.
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namemap
-func encodeNameAssoc(na *wasm.NameAssoc) []byte {
-	return append(leb128.EncodeUint32(na.Index), encodeSizePrefixed([]byte(na.Name))...)
-}
-
-// encodeSizePrefixed encodes the data prefixed by their size.
-func encodeSizePrefixed(data []byte) []byte {
-	size := leb128.EncodeUint32(uint32(len(data)))
-	return append(size, data...)
-}
diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go
index c4baba64..974c3056 100644
--- a/internal/wasm/binary/section.go
+++ b/internal/wasm/binary/section.go
@@ -203,141 +203,3 @@ func decodeDataCountSection(r *bytes.Reader) (count *uint32, err error) {
 	}
 	return &v, nil
 }
-
-// encodeSection encodes the sectionID, the size of its contents in bytes, followed by the contents.
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#sections%E2%91%A0
-func encodeSection(sectionID wasm.SectionID, contents []byte) []byte {
-	return append([]byte{sectionID}, encodeSizePrefixed(contents)...)
-}
-
-// encodeTypeSection encodes a wasm.SectionIDType for the given imports in WebAssembly 1.0 (20191205) Binary
-// Format.
-//
-// See encodeFunctionType
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#type-section%E2%91%A0
-func encodeTypeSection(types []*wasm.FunctionType) []byte {
-	contents := leb128.EncodeUint32(uint32(len(types)))
-	for _, t := range types {
-		contents = append(contents, encodeFunctionType(t)...)
-	}
-	return encodeSection(wasm.SectionIDType, contents)
-}
-
-// encodeImportSection encodes a wasm.SectionIDImport for the given imports in WebAssembly 1.0 (20191205) Binary
-// Format.
-//
-// See encodeImport
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#import-section%E2%91%A0
-func encodeImportSection(imports []*wasm.Import) []byte {
-	contents := leb128.EncodeUint32(uint32(len(imports)))
-	for _, i := range imports {
-		contents = append(contents, encodeImport(i)...)
-	}
-	return encodeSection(wasm.SectionIDImport, contents)
-}
-
-// encodeFunctionSection encodes a wasm.SectionIDFunction for the type indices associated with module-defined
-// functions in WebAssembly 1.0 (20191205) Binary Format.
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-section%E2%91%A0
-func encodeFunctionSection(typeIndices []wasm.Index) []byte {
-	contents := leb128.EncodeUint32(uint32(len(typeIndices)))
-	for _, index := range typeIndices {
-		contents = append(contents, leb128.EncodeUint32(index)...)
-	}
-	return encodeSection(wasm.SectionIDFunction, contents)
-}
-
-// encodeCodeSection encodes a wasm.SectionIDCode for the module-defined function in WebAssembly 1.0 (20191205)
-// Binary Format.
-//
-// See encodeCode
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#code-section%E2%91%A0
-func encodeCodeSection(code []*wasm.Code) []byte {
-	contents := leb128.EncodeUint32(uint32(len(code)))
-	for _, i := range code {
-		contents = append(contents, encodeCode(i)...)
-	}
-	return encodeSection(wasm.SectionIDCode, contents)
-}
-
-// encodeTableSection encodes a wasm.SectionIDTable for the module-defined function in WebAssembly 1.0
-// (20191205) Binary Format.
-//
-// See encodeTable
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-section%E2%91%A0
-func encodeTableSection(tables []*wasm.Table) []byte {
-	var contents []byte = leb128.EncodeUint32(uint32(len(tables)))
-	for _, table := range tables {
-		contents = append(contents, encodeTable(table)...)
-	}
-	return encodeSection(wasm.SectionIDTable, contents)
-}
-
-// encodeMemorySection encodes a wasm.SectionIDMemory for the module-defined function in WebAssembly 1.0
-// (20191205) Binary Format.
-//
-// See encodeMemory
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0
-func encodeMemorySection(memory *wasm.Memory) []byte {
-	contents := append([]byte{1}, encodeMemory(memory)...)
-	return encodeSection(wasm.SectionIDMemory, contents)
-}
-
-// encodeGlobalSection encodes a wasm.SectionIDGlobal for the given globals in WebAssembly 1.0 (20191205) Binary
-// Format.
-//
-// See encodeGlobal
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0
-func encodeGlobalSection(globals []*wasm.Global) []byte {
-	contents := leb128.EncodeUint32(uint32(len(globals)))
-	for _, g := range globals {
-		contents = append(contents, encodeGlobal(g)...)
-	}
-	return encodeSection(wasm.SectionIDGlobal, contents)
-}
-
-// encodeExportSection encodes a wasm.SectionIDExport for the given exports in WebAssembly 1.0 (20191205) Binary
-// Format.
-//
-// See encodeExport
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0
-func encodeExportSection(exports []*wasm.Export) []byte {
-	contents := leb128.EncodeUint32(uint32(len(exports)))
-	for _, e := range exports {
-		contents = append(contents, encodeExport(e)...)
-	}
-	return encodeSection(wasm.SectionIDExport, contents)
-}
-
-// encodeStartSection encodes a wasm.SectionIDStart for the given function index in WebAssembly 1.0 (20191205)
-// Binary Format.
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#start-section%E2%91%A0
-func encodeStartSection(funcidx wasm.Index) []byte {
-	return encodeSection(wasm.SectionIDStart, leb128.EncodeUint32(funcidx))
-}
-
-// encodeEelementSection encodes a wasm.SectionIDElement for the elements in WebAssembly 1.0 (20191205)
-// Binary Format.
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#element-section%E2%91%A0
-func encodeElementSection(elements []*wasm.ElementSegment) []byte {
-	contents := leb128.EncodeUint32(uint32(len(elements)))
-	for _, e := range elements {
-		contents = append(contents, encodeElement(e)...)
-	}
-	return encodeSection(wasm.SectionIDElement, contents)
-}
-
-// encodeDataSection encodes a wasm.SectionIDData for the data in WebAssembly 1.0 (20191205)
-// Binary Format.
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#data-section%E2%91%A0
-func encodeDataSection(datum []*wasm.DataSegment) []byte {
-	contents := leb128.EncodeUint32(uint32(len(datum)))
-	for _, d := range datum {
-		contents = append(contents, encodeDataSegment(d)...)
-	}
-	return encodeSection(wasm.SectionIDData, contents)
-}
diff --git a/internal/wasm/binary/table.go b/internal/wasm/binary/table.go
index 8aad3fff..dac86f33 100644
--- a/internal/wasm/binary/table.go
+++ b/internal/wasm/binary/table.go
@@ -37,10 +37,3 @@ func decodeTable(r *bytes.Reader, enabledFeatures api.CoreFeatures) (*wasm.Table
 	}
 	return &wasm.Table{Min: min, Max: max, Type: tableType}, nil
 }
-
-// encodeTable returns the wasm.Table encoded in WebAssembly 1.0 (20191205) Binary Format.
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-table
-func encodeTable(i *wasm.Table) []byte {
-	return append([]byte{i.Type}, encodeLimitsType(i.Min, i.Max)...)
-}
diff --git a/internal/wasm/binary/value.go b/internal/wasm/binary/value.go
index 6a305472..4ab3d88f 100644
--- a/internal/wasm/binary/value.go
+++ b/internal/wasm/binary/value.go
@@ -10,41 +10,6 @@ import (
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
-var noValType = []byte{0}
-
-// encodedValTypes is a cache of size prefixed binary encoding of known val types.
-var encodedValTypes = map[wasm.ValueType][]byte{
-	wasm.ValueTypeI32:       {1, wasm.ValueTypeI32},
-	wasm.ValueTypeI64:       {1, wasm.ValueTypeI64},
-	wasm.ValueTypeF32:       {1, wasm.ValueTypeF32},
-	wasm.ValueTypeF64:       {1, wasm.ValueTypeF64},
-	wasm.ValueTypeExternref: {1, wasm.ValueTypeExternref},
-	wasm.ValueTypeFuncref:   {1, wasm.ValueTypeFuncref},
-	wasm.ValueTypeV128:      {1, wasm.ValueTypeV128},
-}
-
-// encodeValTypes fast paths binary encoding of common value type lengths
-func encodeValTypes(vt []wasm.ValueType) []byte {
-	// Special case nullary and parameter lengths of wasi_snapshot_preview1 to avoid excess allocations
-	switch uint32(len(vt)) {
-	case 0: // nullary
-		return noValType
-	case 1: // ex $wasi.fd_close or any result
-		if encoded, ok := encodedValTypes[vt[0]]; ok {
-			return encoded
-		}
-	case 2: // ex $wasi.environ_sizes_get
-		return []byte{2, vt[0], vt[1]}
-	case 4: // ex $wasi.fd_write
-		return []byte{4, vt[0], vt[1], vt[2], vt[3]}
-	case 9: // ex $wasi.fd_write
-		return []byte{9, vt[0], vt[1], vt[2], vt[3], vt[4], vt[5], vt[6], vt[7], vt[8]}
-	}
-	// Slow path others until someone complains with a valid signature
-	count := leb128.EncodeUint32(uint32(len(vt)))
-	return append(count, vt...)
-}
-
 func decodeValueTypes(r *bytes.Reader, num uint32) ([]wasm.ValueType, error) {
 	if num == 0 {
 		return nil, nil
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index 42603981..40e57d6a 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -29,7 +29,7 @@ type DecodeModule func(
 ) (result *Module, err error)
 
 // EncodeModule encodes the given module into a byte slice depending on the format of the implementation.
-// See binary.EncodeModule
+// See binaryencoding.EncodeModule
 type EncodeModule func(m *Module) (bytes []byte)
 
 // Module is a WebAssembly binary representation.
diff --git a/site/content/community/users.md b/site/content/community/users.md
index d2361d77..211ab6a9 100644
--- a/site/content/community/users.md
+++ b/site/content/community/users.md
@@ -21,7 +21,6 @@ considering their efforts before starting your own!
 | Name           | Description                                                                   |
 |:---------------|-------------------------------------------------------------------------------|
 | [go-plugin][2] | implements [Protocol Buffers][8] services with WebAssembly vi code generation |
-| [scale][13]    | implements [Polyglot][14] interfaces with WebAssembly via code generation     |
 | [waPC][5]      | implements [Apex][6] interfaces with WebAssembly via code generation          |
 
 ### Middleware
@@ -45,9 +44,10 @@ considering their efforts before starting your own!
 
 ### Cloud Platforms
 
-| Name          | Description                                                           |
-|:--------------|-----------------------------------------------------------------------|
-| [taubyte][21] | edge computing and web3 platform that runs [serverless functions][22] |
+| Name          | Description                                                               |
+|:--------------|---------------------------------------------------------------------------|
+| [scale][13]   | implements [Polyglot][14] interfaces with WebAssembly via code generation |
+| [taubyte][21] | edge computing and web3 platform that runs [serverless functions][22]     |
 
 ## Updating this list
 


Test patch:
diff --git a/config_test.go b/config_test.go
index 24aa19ef..1174669f 100644
--- a/config_test.go
+++ b/config_test.go
@@ -360,6 +360,24 @@ func TestModuleConfig_toSysContext(t *testing.T) {
 				sysfs.Adapt(testFS2), // fs
 			),
 		},
+		{
+			name:  "WithFS nil",
+			input: base.WithFS(nil),
+			expected: requireSysContext(t,
+				math.MaxUint32, // max
+				nil,            // args
+				nil,            // environ
+				nil,            // stdin
+				nil,            // stdout
+				nil,            // stderr
+				nil,            // randSource
+				&wt, 1,         // walltime, walltimeResolution
+				&nt, 1, // nanotime, nanotimeResolution
+				nil, // nanosleep
+				nil, // osyield
+				nil, // fs
+			),
+		},
 		{
 			name:  "WithRandSource",
 			input: base.WithRandSource(rand.Reader),
diff --git a/experimental/listener_test.go b/experimental/listener_test.go
index f15efd83..d53e66a2 100644
--- a/experimental/listener_test.go
+++ b/experimental/listener_test.go
@@ -8,9 +8,9 @@ import (
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
 	. "github.com/tetratelabs/wazero/experimental"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
-	"github.com/tetratelabs/wazero/internal/wasm/binary"
 )
 
 // compile-time check to ensure recorder implements FunctionListenerFactory
@@ -41,7 +41,7 @@ func TestFunctionListenerFactory(t *testing.T) {
 	ctx := context.WithValue(context.Background(), FunctionListenerFactoryKey{}, factory)
 
 	// Define a module with two functions
-	bin := binary.EncodeModule(&wasm.Module{
+	bin := binaryencoding.EncodeModule(&wasm.Module{
 		TypeSection:     []*wasm.FunctionType{{}},
 		ImportSection:   []*wasm.Import{{}},
 		FunctionSection: []wasm.Index{0, 0},
diff --git a/fsconfig_test.go b/fsconfig_test.go
index 144b1f87..b114dafe 100644
--- a/fsconfig_test.go
+++ b/fsconfig_test.go
@@ -35,6 +35,11 @@ func TestFSConfig(t *testing.T) {
 			input:    base.WithFSMount(testFS, "/").WithFSMount(testFS2, "/"),
 			expected: sysfs.Adapt(testFS2),
 		},
+		{
+			name:     "WithFsMount nil",
+			input:    base.WithFSMount(nil, "/"),
+			expected: sysfs.UnimplementedFS{},
+		},
 		{
 			name:     "WithDirMount overwrites",
 			input:    base.WithFSMount(testFS, "/").WithDirMount(".", "/"),
diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go
index 0f548278..0bd91169 100644
--- a/internal/gojs/compiler_test.go
+++ b/internal/gojs/compiler_test.go
@@ -21,6 +21,7 @@ import (
 	"github.com/tetratelabs/wazero/internal/fstest"
 	internalgojs "github.com/tetratelabs/wazero/internal/gojs"
 	"github.com/tetratelabs/wazero/internal/gojs/run"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary"
 )
@@ -115,7 +116,7 @@ func TestMain(m *testing.M) {
 	// Set max to a high value, e.g. so that Test_stdio_large can pass
 	parsed.MemorySection.Max = 1024 // 64MB
 	parsed.MemorySection.IsMaxEncoded = true
-	testBin = binaryformat.EncodeModule(parsed)
+	testBin = binaryencoding.EncodeModule(parsed)
 
 	// Seed wazero's compilation cache to see any error up-front and to prevent
 	// one test from a cache-miss performance penalty.
diff --git a/internal/integration_test/engine/adhoc_test.go b/internal/integration_test/engine/adhoc_test.go
index 2775896f..df8b1314 100644
--- a/internal/integration_test/engine/adhoc_test.go
+++ b/internal/integration_test/engine/adhoc_test.go
@@ -13,10 +13,10 @@ import (
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/platform"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/proxy"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
-	"github.com/tetratelabs/wazero/internal/wasm/binary"
 	"github.com/tetratelabs/wazero/sys"
 )
 
@@ -507,7 +507,7 @@ func callReturnImportWasm(t *testing.T, importedModule, importingModule string,
 		},
 	}
 	require.NoError(t, module.Validate(api.CoreFeaturesV2))
-	return binary.EncodeModule(module)
+	return binaryencoding.EncodeModule(module)
 }
 
 func callOuterInnerWasm(t *testing.T, importedModule, importingModule string) []byte {
@@ -543,7 +543,7 @@ func callOuterInnerWasm(t *testing.T, importedModule, importingModule string) []
 		},
 	}
 	require.NoError(t, module.Validate(api.CoreFeaturesV2))
-	return binary.EncodeModule(module)
+	return binaryencoding.EncodeModule(module)
 }
 
 func testCloseInFlight(t *testing.T, r wazero.Runtime) {
@@ -700,7 +700,7 @@ func testMemOps(t *testing.T, r wazero.Runtime) {
 }
 
 func testMultipleInstantiation(t *testing.T, r wazero.Runtime) {
-	bin := binary.EncodeModule(&wasm.Module{
+	bin := binaryencoding.EncodeModule(&wasm.Module{
 		TypeSection:     []*wasm.FunctionType{{}},
 		FunctionSection: []wasm.Index{0},
 		MemorySection:   &wasm.Memory{Min: 1, Cap: 1, Max: 1, IsMaxEncoded: true},
diff --git a/internal/integration_test/fuzz/wazerolib/nodiff.go b/internal/integration_test/fuzz/wazerolib/nodiff.go
index 7ed7d0b0..0f3db868 100644
--- a/internal/integration_test/fuzz/wazerolib/nodiff.go
+++ b/internal/integration_test/fuzz/wazerolib/nodiff.go
@@ -12,8 +12,8 @@ import (
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/wasm"
-	"github.com/tetratelabs/wazero/internal/wasm/binary"
 )
 
 // require_no_diff ensures that the behavior is the same between the compiler and the interpreter for any given binary.
@@ -219,7 +219,7 @@ func ensureDummyImports(r wazero.Runtime, origin *wasm.Module, requireNoError fu
 			}
 			m.ExportSection = append(m.ExportSection, &wasm.Export{Type: imp.Type, Name: imp.Name, Index: index})
 		}
-		_, err := r.Instantiate(context.Background(), binary.EncodeModule(m))
+		_, err := r.Instantiate(context.Background(), binaryencoding.EncodeModule(m))
 		requireNoError(err)
 	}
 	return
diff --git a/internal/integration_test/fuzzcases/fuzzcases_test.go b/internal/integration_test/fuzzcases/fuzzcases_test.go
index b007678a..74df15b0 100644
--- a/internal/integration_test/fuzzcases/fuzzcases_test.go
+++ b/internal/integration_test/fuzzcases/fuzzcases_test.go
@@ -9,9 +9,9 @@ import (
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/platform"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
-	"github.com/tetratelabs/wazero/internal/wasm/binary"
 )
 
 var ctx = context.Background()
@@ -354,7 +354,7 @@ func Test888(t *testing.T) {
 	// This tests that importing FuncRef type globals and using it as an initialization of the locally-defined
 	// FuncRef global works fine.
 	run(t, func(t *testing.T, r wazero.Runtime) {
-		imported := binary.EncodeModule(&wasm.Module{
+		imported := binaryencoding.EncodeModule(&wasm.Module{
 			MemorySection: &wasm.Memory{Min: 0, Max: 5, IsMaxEncoded: true},
 			GlobalSection: []*wasm.Global{
 				{
diff --git a/internal/sysfs/adapter_test.go b/internal/sysfs/adapter_test.go
index 008ef4f0..b877d7d9 100644
--- a/internal/sysfs/adapter_test.go
+++ b/internal/sysfs/adapter_test.go
@@ -14,6 +14,12 @@ import (
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
+func TestAdapt_nil(t *testing.T) {
+	testFS := Adapt(nil)
+	_, ok := testFS.(UnimplementedFS)
+	require.True(t, ok)
+}
+
 func TestAdapt_String(t *testing.T) {
 	testFS := Adapt(os.DirFS("."))
 	require.Equal(t, ".", testFS.String())
diff --git a/internal/testing/binaryencoding/code.go b/internal/testing/binaryencoding/code.go
new file mode 100644
index 00000000..87c2d522
--- /dev/null
+++ b/internal/testing/binaryencoding/code.go
@@ -0,0 +1,48 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/leb128"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+// encodeCode returns the wasm.Code encoded in WebAssembly 1.0 (20191205) Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-code
+func encodeCode(c *wasm.Code) []byte {
+	if c.GoFunc != nil {
+		panic("BUG: GoFunction is not encodable")
+	}
+
+	// local blocks compress locals while preserving index order by grouping locals of the same type.
+	// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#code-section%E2%91%A0
+	localBlockCount := uint32(0) // how many blocks of locals with the same type (types can repeat!)
+	var localBlocks []byte
+	localTypeLen := len(c.LocalTypes)
+	if localTypeLen > 0 {
+		i := localTypeLen - 1
+		var runCount uint32              // count of the same type
+		var lastValueType wasm.ValueType // initialize to an invalid type 0
+
+		// iterate backwards so it is easier to size prefix
+		for ; i >= 0; i-- {
+			vt := c.LocalTypes[i]
+			if lastValueType != vt {
+				if runCount != 0 { // Only on the first iteration, this is zero when vt is compared against invalid
+					localBlocks = append(leb128.EncodeUint32(runCount), localBlocks...)
+				}
+				lastValueType = vt
+				localBlocks = append(leb128.EncodeUint32(uint32(vt)), localBlocks...) // reuse the EncodeUint32 cache
+				localBlockCount++
+				runCount = 1
+			} else {
+				runCount++
+			}
+		}
+		localBlocks = append(leb128.EncodeUint32(runCount), localBlocks...)
+		localBlocks = append(leb128.EncodeUint32(localBlockCount), localBlocks...)
+	} else {
+		localBlocks = leb128.EncodeUint32(0)
+	}
+	code := append(localBlocks, c.Body...)
+	return append(leb128.EncodeUint32(uint32(len(code))), code...)
+}
diff --git a/internal/wasm/binary/code_test.go b/internal/testing/binaryencoding/code_test.go
similarity index 99%
rename from internal/wasm/binary/code_test.go
rename to internal/testing/binaryencoding/code_test.go
index f058c875..f398f0f1 100644
--- a/internal/wasm/binary/code_test.go
+++ b/internal/testing/binaryencoding/code_test.go
@@ -1,4 +1,4 @@
-package binary
+package binaryencoding
 
 import (
 	"testing"
diff --git a/internal/testing/binaryencoding/const_expr.go b/internal/testing/binaryencoding/const_expr.go
new file mode 100644
index 00000000..7a41841a
--- /dev/null
+++ b/internal/testing/binaryencoding/const_expr.go
@@ -0,0 +1,12 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func encodeConstantExpression(expr *wasm.ConstantExpression) (ret []byte) {
+	ret = append(ret, expr.Opcode)
+	ret = append(ret, expr.Data...)
+	ret = append(ret, wasm.OpcodeEnd)
+	return
+}
diff --git a/internal/testing/binaryencoding/data.go b/internal/testing/binaryencoding/data.go
new file mode 100644
index 00000000..e4405e5d
--- /dev/null
+++ b/internal/testing/binaryencoding/data.go
@@ -0,0 +1,15 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/leb128"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func encodeDataSegment(d *wasm.DataSegment) (ret []byte) {
+	// Currently multiple memories are not supported.
+	ret = append(ret, leb128.EncodeInt32(0)...)
+	ret = append(ret, encodeConstantExpression(d.OffsetExpression)...)
+	ret = append(ret, leb128.EncodeUint32(uint32(len(d.Init)))...)
+	ret = append(ret, d.Init...)
+	return
+}
diff --git a/internal/testing/binaryencoding/element.go b/internal/testing/binaryencoding/element.go
new file mode 100644
index 00000000..a238176c
--- /dev/null
+++ b/internal/testing/binaryencoding/element.go
@@ -0,0 +1,37 @@
+package binaryencoding
+
+import (
+	"bytes"
+	"fmt"
+
+	"github.com/tetratelabs/wazero/internal/leb128"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func ensureElementKindFuncRef(r *bytes.Reader) error {
+	elemKind, err := r.ReadByte()
+	if err != nil {
+		return fmt.Errorf("read element prefix: %w", err)
+	}
+	if elemKind != 0x0 { // ElemKind is fixed to 0x0 now: https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#element-section
+		return fmt.Errorf("element kind must be zero but was 0x%x", elemKind)
+	}
+	return nil
+}
+
+// encodeCode returns the wasm.ElementSegment encoded in WebAssembly 1.0 (20191205) Binary Format.
+//
+// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#element-section%E2%91%A0
+func encodeElement(e *wasm.ElementSegment) (ret []byte) {
+	if e.Mode == wasm.ElementModeActive {
+		ret = append(ret, leb128.EncodeInt32(int32(e.TableIndex))...)
+		ret = append(ret, encodeConstantExpression(e.OffsetExpr)...)
+		ret = append(ret, leb128.EncodeUint32(uint32(len(e.Init)))...)
+		for _, idx := range e.Init {
+			ret = append(ret, leb128.EncodeInt32(int32(*idx))...)
+		}
+	} else {
+		panic("TODO: support encoding for non-active elements in bulk-memory-operations proposal")
+	}
+	return
+}
diff --git a/internal/testing/binaryencoding/element_test.go b/internal/testing/binaryencoding/element_test.go
new file mode 100644
index 00000000..d1f93023
--- /dev/null
+++ b/internal/testing/binaryencoding/element_test.go
@@ -0,0 +1,13 @@
+package binaryencoding
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func Test_ensureElementKindFuncRef(t *testing.T) {
+	require.NoError(t, ensureElementKindFuncRef(bytes.NewReader([]byte{0x0})))
+	require.Error(t, ensureElementKindFuncRef(bytes.NewReader([]byte{0x1})))
+}
diff --git a/internal/wasm/binary/encoder_test.go b/internal/testing/binaryencoding/encoder_test.go
similarity index 99%
rename from internal/wasm/binary/encoder_test.go
rename to internal/testing/binaryencoding/encoder_test.go
index 8d80755b..f6f8d186 100644
--- a/internal/wasm/binary/encoder_test.go
+++ b/internal/testing/binaryencoding/encoder_test.go
@@ -1,4 +1,4 @@
-package binary
+package binaryencoding
 
 import (
 	"testing"
diff --git a/internal/testing/binaryencoding/export.go b/internal/testing/binaryencoding/export.go
new file mode 100644
index 00000000..5bedf446
--- /dev/null
+++ b/internal/testing/binaryencoding/export.go
@@ -0,0 +1,16 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/leb128"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+// encodeExport returns the wasm.Export encoded in WebAssembly 1.0 (20191205) Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0
+func encodeExport(i *wasm.Export) []byte {
+	data := encodeSizePrefixed([]byte(i.Name))
+	data = append(data, i.Type)
+	data = append(data, leb128.EncodeUint32(i.Index)...)
+	return data
+}
diff --git a/internal/wasm/binary/export_test.go b/internal/testing/binaryencoding/export_test.go
similarity index 99%
rename from internal/wasm/binary/export_test.go
rename to internal/testing/binaryencoding/export_test.go
index 9a62f1af..7c5ba2bd 100644
--- a/internal/wasm/binary/export_test.go
+++ b/internal/testing/binaryencoding/export_test.go
@@ -1,4 +1,4 @@
-package binary
+package binaryencoding
 
 import (
 	"testing"
diff --git a/internal/testing/binaryencoding/function.go b/internal/testing/binaryencoding/function.go
new file mode 100644
index 00000000..a31be4b6
--- /dev/null
+++ b/internal/testing/binaryencoding/function.go
@@ -0,0 +1,15 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+// EncodeFunctionType returns the wasm.FunctionType encoded in WebAssembly 1.0 (20191205) Binary Format.
+//
+// Note: Function types are encoded by the byte 0x60 followed by the respective vectors of parameter and result types.
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-types%E2%91%A4
+func EncodeFunctionType(t *wasm.FunctionType) []byte {
+	// Only reached when "multi-value" is enabled because WebAssembly 1.0 (20191205) supports at most 1 result.
+	data := append([]byte{0x60}, EncodeValTypes(t.Params)...)
+	return append(data, EncodeValTypes(t.Results)...)
+}
diff --git a/internal/testing/binaryencoding/global.go b/internal/testing/binaryencoding/global.go
new file mode 100644
index 00000000..357a3aba
--- /dev/null
+++ b/internal/testing/binaryencoding/global.go
@@ -0,0 +1,18 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+// encodeGlobal returns the wasm.Global encoded in WebAssembly 1.0 (20191205) Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0
+func encodeGlobal(g *wasm.Global) (data []byte) {
+	var mutable byte
+	if g.Type.Mutable {
+		mutable = 1
+	}
+	data = []byte{g.Type.ValType, mutable}
+	data = append(data, encodeConstantExpression(g.Init)...)
+	return
+}
diff --git a/internal/wasm/binary/global_test.go b/internal/testing/binaryencoding/global_test.go
similarity index 98%
rename from internal/wasm/binary/global_test.go
rename to internal/testing/binaryencoding/global_test.go
index 3ccfa93a..6ef9dff3 100644
--- a/internal/wasm/binary/global_test.go
+++ b/internal/testing/binaryencoding/global_test.go
@@ -1,4 +1,4 @@
-package binary
+package binaryencoding
 
 import (
 	"testing"
diff --git a/internal/testing/binaryencoding/header.go b/internal/testing/binaryencoding/header.go
new file mode 100644
index 00000000..e8161659
--- /dev/null
+++ b/internal/testing/binaryencoding/header.go
@@ -0,0 +1,9 @@
+package binaryencoding
+
+// Magic is the 4 byte preamble (literally "\0asm") of the binary format
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-magic
+var Magic = []byte{0x00, 0x61, 0x73, 0x6D}
+
+// version is format version and doesn't change between known specification versions
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-version
+var version = []byte{0x01, 0x00, 0x00, 0x00}
diff --git a/internal/testing/binaryencoding/import.go b/internal/testing/binaryencoding/import.go
new file mode 100644
index 00000000..0646a19c
--- /dev/null
+++ b/internal/testing/binaryencoding/import.go
@@ -0,0 +1,40 @@
+package binaryencoding
+
+import (
+	"fmt"
+
+	"github.com/tetratelabs/wazero/internal/leb128"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+// EncodeImport returns the wasm.Import encoded in WebAssembly 1.0 (20191205) Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-import
+func EncodeImport(i *wasm.Import) []byte {
+	data := encodeSizePrefixed([]byte(i.Module))
+	data = append(data, encodeSizePrefixed([]byte(i.Name))...)
+	data = append(data, i.Type)
+	switch i.Type {
+	case wasm.ExternTypeFunc:
+		data = append(data, leb128.EncodeUint32(i.DescFunc)...)
+	case wasm.ExternTypeTable:
+		data = append(data, wasm.RefTypeFuncref)
+		data = append(data, EncodeLimitsType(i.DescTable.Min, i.DescTable.Max)...)
+	case wasm.ExternTypeMemory:
+		maxPtr := &i.DescMem.Max
+		if !i.DescMem.IsMaxEncoded {
+			maxPtr = nil
+		}
+		data = append(data, EncodeLimitsType(i.DescMem.Min, maxPtr)...)
+	case wasm.ExternTypeGlobal:
+		g := i.DescGlobal
+		var mutable byte
+		if g.Mutable {
+			mutable = 1
+		}
+		data = append(data, g.ValType, mutable)
+	default:
+		panic(fmt.Errorf("invalid externtype: %s", wasm.ExternTypeName(i.Type)))
+	}
+	return data
+}
diff --git a/internal/testing/binaryencoding/import_test.go b/internal/testing/binaryencoding/import_test.go
new file mode 100644
index 00000000..0b0d79a4
--- /dev/null
+++ b/internal/testing/binaryencoding/import_test.go
@@ -0,0 +1,161 @@
+package binaryencoding
+
+import (
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func TestEncodeImport(t *testing.T) {
+	ptrOfUint32 := func(v uint32) *uint32 {
+		return &v
+	}
+
+	tests := []struct {
+		name     string
+		input    *wasm.Import
+		expected []byte
+	}{
+		{
+			name: "func no module, no name, type index 0",
+			input: &wasm.Import{ // e.g. (import "" "" (func (type 0)))
+				Type:     wasm.ExternTypeFunc,
+				Module:   "",
+				Name:     "",
+				DescFunc: 0,
+			},
+			expected: []byte{wasm.ExternTypeFunc, 0x00, 0x00, 0x00},
+		},
+		{
+			name: "func module, no name, type index 0",
+			input: &wasm.Import{ // e.g. (import "$test" "" (func (type 0)))
+				Type:     wasm.ExternTypeFunc,
+				Module:   "test",
+				Name:     "",
+				DescFunc: 0,
+			},
+			expected: []byte{
+				0x04, 't', 'e', 's', 't',
+				0x00,
+				wasm.ExternTypeFunc,
+				0x00,
+			},
+		},
+		{
+			name: "func module, name, type index 0",
+			input: &wasm.Import{ // e.g. (import "$math" "$pi" (func (type 0)))
+				Type:     wasm.ExternTypeFunc,
+				Module:   "math",
+				Name:     "pi",
+				DescFunc: 0,
+			},
+			expected: []byte{
+				0x04, 'm', 'a', 't', 'h',
+				0x02, 'p', 'i',
+				wasm.ExternTypeFunc,
+				0x00,
+			},
+		},
+		{
+			name: "func module, name, type index 10",
+			input: &wasm.Import{ // e.g. (import "$math" "$pi" (func (type 10)))
+				Type:     wasm.ExternTypeFunc,
+				Module:   "math",
+				Name:     "pi",
+				DescFunc: 10,
+			},
+			expected: []byte{
+				0x04, 'm', 'a', 't', 'h',
+				0x02, 'p', 'i',
+				wasm.ExternTypeFunc,
+				0x0a,
+			},
+		},
+		{
+			name: "global const",
+			input: &wasm.Import{
+				Type:       wasm.ExternTypeGlobal,
+				Module:     "math",
+				Name:       "pi",
+				DescGlobal: &wasm.GlobalType{ValType: wasm.ValueTypeF64},
+			},
+			expected: []byte{
+				0x04, 'm', 'a', 't', 'h',
+				0x02, 'p', 'i',
+				wasm.ExternTypeGlobal,
+				wasm.ValueTypeF64, 0x00, // 0 == const
+			},
+		},
+		{
+			name: "global var",
+			input: &wasm.Import{
+				Type:       wasm.ExternTypeGlobal,
+				Module:     "math",
+				Name:       "pi",
+				DescGlobal: &wasm.GlobalType{ValType: wasm.ValueTypeF64, Mutable: true},
+			},
+			expected: []byte{
+				0x04, 'm', 'a', 't', 'h',
+				0x02, 'p', 'i',
+				wasm.ExternTypeGlobal,
+				wasm.ValueTypeF64, 0x01, // 1 == var
+			},
+		},
+		{
+			name: "table",
+			input: &wasm.Import{
+				Type:      wasm.ExternTypeTable,
+				Module:    "my",
+				Name:      "table",
+				DescTable: &wasm.Table{Min: 1, Max: ptrOfUint32(2)},
+			},
+			expected: []byte{
+				0x02, 'm', 'y',
+				0x05, 't', 'a', 'b', 'l', 'e',
+				wasm.ExternTypeTable,
+				wasm.RefTypeFuncref,
+				0x1, 0x1, 0x2, // Limit with max.
+			},
+		},
+		{
+			name: "memory",
+			input: &wasm.Import{
+				Type:    wasm.ExternTypeMemory,
+				Module:  "my",
+				Name:    "memory",
+				DescMem: &wasm.Memory{Min: 1, Max: 2, IsMaxEncoded: true},
+			},
+			expected: []byte{
+				0x02, 'm', 'y',
+				0x06, 'm', 'e', 'm', 'o', 'r', 'y',
+				wasm.ExternTypeMemory,
+				0x1, 0x1, 0x2, // Limit with max.
+			},
+		},
+		{
+			name: "memory - defaultt max",
+			input: &wasm.Import{
+				Type:    wasm.ExternTypeMemory,
+				Module:  "my",
+				Name:    "memory",
+				DescMem: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages, IsMaxEncoded: false},
+			},
+			expected: []byte{
+				0x02, 'm', 'y',
+				0x06, 'm', 'e', 'm', 'o', 'r', 'y',
+				wasm.ExternTypeMemory,
+				0x0, 0x1, // Limit without max.
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			bytes := EncodeImport(tc.input)
+			require.Equal(t, tc.expected, bytes)
+		})
+	}
+}
diff --git a/internal/testing/binaryencoding/limits.go b/internal/testing/binaryencoding/limits.go
new file mode 100644
index 00000000..016e4dc7
--- /dev/null
+++ b/internal/testing/binaryencoding/limits.go
@@ -0,0 +1,15 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/leb128"
+)
+
+// EncodeLimitsType returns the `limitsType` (min, max) encoded in WebAssembly 1.0 (20191205) Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#limits%E2%91%A6
+func EncodeLimitsType(min uint32, max *uint32) []byte {
+	if max == nil {
+		return append(leb128.EncodeUint32(0x00), leb128.EncodeUint32(min)...)
+	}
+	return append(leb128.EncodeUint32(0x01), append(leb128.EncodeUint32(min), leb128.EncodeUint32(*max)...)...)
+}
diff --git a/internal/testing/binaryencoding/memory.go b/internal/testing/binaryencoding/memory.go
new file mode 100644
index 00000000..938667a6
--- /dev/null
+++ b/internal/testing/binaryencoding/memory.go
@@ -0,0 +1,16 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+// EncodeMemory returns the wasm.Memory encoded in WebAssembly 1.0 (20191205) Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory
+func EncodeMemory(i *wasm.Memory) []byte {
+	maxPtr := &i.Max
+	if !i.IsMaxEncoded {
+		maxPtr = nil
+	}
+	return EncodeLimitsType(i.Min, maxPtr)
+}
diff --git a/internal/testing/binaryencoding/names.go b/internal/testing/binaryencoding/names.go
new file mode 100644
index 00000000..efa0bf0d
--- /dev/null
+++ b/internal/testing/binaryencoding/names.go
@@ -0,0 +1,93 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/leb128"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+const (
+	// subsectionIDModuleName contains only the module name.
+	subsectionIDModuleName = uint8(0)
+	// subsectionIDFunctionNames is a map of indices to function names, in ascending order by function index
+	subsectionIDFunctionNames = uint8(1)
+	// subsectionIDLocalNames contain a map of function indices to a map of local indices to their names, in ascending
+	// order by function and local index
+	subsectionIDLocalNames = uint8(2)
+)
+
+// EncodeNameSectionData serializes the data for the "name" key in wasm.SectionIDCustom according to the
+// standard:
+//
+// Note: The result can be nil because this does not encode empty subsections
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namesec
+func EncodeNameSectionData(n *wasm.NameSection) (data []byte) {
+	if n.ModuleName != "" {
+		data = append(data, encodeNameSubsection(subsectionIDModuleName, encodeSizePrefixed([]byte(n.ModuleName)))...)
+	}
+	if fd := encodeFunctionNameData(n); len(fd) > 0 {
+		data = append(data, encodeNameSubsection(subsectionIDFunctionNames, fd)...)
+	}
+	if ld := encodeLocalNameData(n); len(ld) > 0 {
+		data = append(data, encodeNameSubsection(subsectionIDLocalNames, ld)...)
+	}
+	return
+}
+
+// encodeFunctionNameData encodes the data for the function name subsection.
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-funcnamesec
+func encodeFunctionNameData(n *wasm.NameSection) []byte {
+	if len(n.FunctionNames) == 0 {
+		return nil
+	}
+
+	return encodeNameMap(n.FunctionNames)
+}
+
+func encodeNameMap(m wasm.NameMap) []byte {
+	count := uint32(len(m))
+	data := leb128.EncodeUint32(count)
+	for _, na := range m {
+		data = append(data, encodeNameAssoc(na)...)
+	}
+	return data
+}
+
+// encodeLocalNameData encodes the data for the local name subsection.
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-localnamesec
+func encodeLocalNameData(n *wasm.NameSection) []byte {
+	if len(n.LocalNames) == 0 {
+		return nil
+	}
+
+	funcNameCount := uint32(len(n.LocalNames))
+	subsection := leb128.EncodeUint32(funcNameCount)
+
+	for _, na := range n.LocalNames {
+		locals := encodeNameMap(na.NameMap)
+		subsection = append(subsection, append(leb128.EncodeUint32(na.Index), locals...)...)
+	}
+	return subsection
+}
+
+// encodeNameSubsection returns a buffer encoding the given subsection
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#subsections%E2%91%A0
+func encodeNameSubsection(subsectionID uint8, content []byte) []byte {
+	contentSizeInBytes := leb128.EncodeUint32(uint32(len(content)))
+	result := []byte{subsectionID}
+	result = append(result, contentSizeInBytes...)
+	result = append(result, content...)
+	return result
+}
+
+// encodeNameAssoc encodes the index and data prefixed by their size.
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namemap
+func encodeNameAssoc(na *wasm.NameAssoc) []byte {
+	return append(leb128.EncodeUint32(na.Index), encodeSizePrefixed([]byte(na.Name))...)
+}
+
+// encodeSizePrefixed encodes the data prefixed by their size.
+func encodeSizePrefixed(data []byte) []byte {
+	size := leb128.EncodeUint32(uint32(len(data)))
+	return append(size, data...)
+}
diff --git a/internal/testing/binaryencoding/names_test.go b/internal/testing/binaryencoding/names_test.go
new file mode 100644
index 00000000..54f92ff6
--- /dev/null
+++ b/internal/testing/binaryencoding/names_test.go
@@ -0,0 +1,147 @@
+package binaryencoding
+
+import (
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func TestEncodeNameSectionData(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    *wasm.NameSection
+		expected []byte
+	}{
+		{
+			name:  "empty",
+			input: &wasm.NameSection{},
+		},
+		{
+			name: "only module",
+			// e.g. (module $simple )
+			input: &wasm.NameSection{ModuleName: "simple"},
+			expected: []byte{
+				subsectionIDModuleName, 0x07, // 7 bytes
+				0x06, // the Module name simple is 6 bytes long
+				's', 'i', 'm', 'p', 'l', 'e',
+			},
+		},
+		{
+			name: "module and function name",
+			//	(module $simple
+			//		(import "" "Hello" (func $hello))
+			//		(start $hello)
+			//	)
+			input: &wasm.NameSection{
+				ModuleName:    "simple",
+				FunctionNames: wasm.NameMap{{Index: wasm.Index(0), Name: "hello"}},
+			},
+			expected: []byte{
+				subsectionIDModuleName, 0x07, // 7 bytes
+				0x06, // the Module name simple is 6 bytes long
+				's', 'i', 'm', 'p', 'l', 'e',
+				subsectionIDFunctionNames, 0x08, // 8 bytes
+				0x01, // one function name
+				0x00, // the function index is zero
+				0x05, // the function name hello is 5 bytes long
+				'h', 'e', 'l', 'l', 'o',
+			},
+		},
+		{
+			name: "two function names", // e.g. TinyGo which at one point didn't set a module name
+			//	(module
+			//		(import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (param i32, i32) (result i32)))
+			//		(import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (param i32, i32, i32, i32) (result i32)))
+			//	)
+			input: &wasm.NameSection{
+				FunctionNames: wasm.NameMap{
+					{Index: wasm.Index(0), Name: "wasi.args_sizes_get"},
+					{Index: wasm.Index(1), Name: "wasi.fd_write"},
+				},
+			},
+			expected: []byte{
+				subsectionIDFunctionNames, 0x25, // 37 bytes
+				0x02, // two function names
+				0x00, // the function index is zero
+				0x13, // the function name wasi.args_sizes_get is 19 bytes long
+				'w', 'a', 's', 'i', '.', 'a', 'r', 'g', 's', '_', 's', 'i', 'z', 'e', 's', '_', 'g', 'e', 't',
+				0x01, // the function index is one
+				0x0d, // the function name wasi.fd_write is 13 bytes long
+				'w', 'a', 's', 'i', '.', 'f', 'd', '_', 'w', 'r', 'i', 't', 'e',
+			},
+		},
+		{
+			name: "function with local names",
+			//	(module
+			//		(import "Math" "Mul" (func $mul (param $x f32) (param $y f32) (result f32)))
+			//		(import "Math" "Add" (func $add (param $l f32) (param $r f32) (result f32)))
+			//	)
+			input: &wasm.NameSection{
+				FunctionNames: wasm.NameMap{
+					{Index: wasm.Index(0), Name: "mul"},
+					{Index: wasm.Index(1), Name: "add"},
+				},
+				LocalNames: wasm.IndirectNameMap{
+					{Index: wasm.Index(0), NameMap: wasm.NameMap{
+						{Index: wasm.Index(0), Name: "x"},
+						{Index: wasm.Index(1), Name: "y"},
+					}},
+					{Index: wasm.Index(1), NameMap: wasm.NameMap{
+						{Index: wasm.Index(0), Name: "l"},
+						{Index: wasm.Index(1), Name: "r"},
+					}},
+				},
+			},
+			expected: []byte{
+				subsectionIDFunctionNames, 0x0b, // 7 bytes
+				0x02,                      // two function names
+				0x00, 0x03, 'm', 'u', 'l', // index 0, size of "mul", "mul"
+				0x01, 0x03, 'a', 'd', 'd', // index 1, size of "add", "add"
+				subsectionIDLocalNames, 0x11, // 17 bytes
+				0x02,       // two functions
+				0x00, 0x02, // index 0 has 2 locals
+				0x00, 0x01, 'x', // index 0, size of "x", "x"
+				0x01, 0x01, 'y', // index 1, size of "y", "y"
+				0x01, 0x02, // index 1 has 2 locals
+				0x00, 0x01, 'l', // index 0, size of "l", "l"
+				0x01, 0x01, 'r', // index 1, size of "r", "r"
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			bytes := EncodeNameSectionData(tc.input)
+			require.Equal(t, tc.expected, bytes)
+		})
+	}
+}
+
+func TestEncodeNameSubsection(t *testing.T) {
+	subsectionID := uint8(1)
+	name := []byte("simple")
+	require.Equal(t, []byte{
+		subsectionID,
+		byte(1 + 6), // 1 is the size of 6 in LEB128 encoding
+		6, 's', 'i', 'm', 'p', 'l', 'e',
+	}, encodeNameSubsection(subsectionID, encodeSizePrefixed(name)))
+}
+
+func TestEncodeNameAssoc(t *testing.T) {
+	na := &wasm.NameAssoc{Index: 1, Name: "hello"}
+	require.Equal(t, []byte{byte(na.Index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameAssoc(na))
+}
+
+func TestEncodeNameMap(t *testing.T) {
+	na := &wasm.NameAssoc{Index: 1, Name: "hello"}
+	m := wasm.NameMap{na}
+	require.Equal(t, []byte{byte(1), byte(na.Index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameMap(m))
+}
+
+func TestEncodeSizePrefixed(t *testing.T) {
+	// We expect size in bytes (LEB128 encoded) then the bytes
+	require.Equal(t, []byte{5, 'h', 'e', 'l', 'l', 'o'}, encodeSizePrefixed([]byte("hello")))
+}
diff --git a/internal/testing/binaryencoding/section.go b/internal/testing/binaryencoding/section.go
new file mode 100644
index 00000000..381ac006
--- /dev/null
+++ b/internal/testing/binaryencoding/section.go
@@ -0,0 +1,144 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/leb128"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+// encodeSection encodes the sectionID, the size of its contents in bytes, followed by the contents.
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#sections%E2%91%A0
+func encodeSection(sectionID wasm.SectionID, contents []byte) []byte {
+	return append([]byte{sectionID}, encodeSizePrefixed(contents)...)
+}
+
+// encodeTypeSection encodes a wasm.SectionIDType for the given imports in WebAssembly 1.0 (20191205) Binary
+// Format.
+//
+// See EncodeFunctionType
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#type-section%E2%91%A0
+func encodeTypeSection(types []*wasm.FunctionType) []byte {
+	contents := leb128.EncodeUint32(uint32(len(types)))
+	for _, t := range types {
+		contents = append(contents, EncodeFunctionType(t)...)
+	}
+	return encodeSection(wasm.SectionIDType, contents)
+}
+
+// encodeImportSection encodes a wasm.SectionIDImport for the given imports in WebAssembly 1.0 (20191205) Binary
+// Format.
+//
+// See EncodeImport
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#import-section%E2%91%A0
+func encodeImportSection(imports []*wasm.Import) []byte {
+	contents := leb128.EncodeUint32(uint32(len(imports)))
+	for _, i := range imports {
+		contents = append(contents, EncodeImport(i)...)
+	}
+	return encodeSection(wasm.SectionIDImport, contents)
+}
+
+// EncodeFunctionSection encodes a wasm.SectionIDFunction for the type indices associated with module-defined
+// functions in WebAssembly 1.0 (20191205) Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-section%E2%91%A0
+func EncodeFunctionSection(typeIndices []wasm.Index) []byte {
+	contents := leb128.EncodeUint32(uint32(len(typeIndices)))
+	for _, index := range typeIndices {
+		contents = append(contents, leb128.EncodeUint32(index)...)
+	}
+	return encodeSection(wasm.SectionIDFunction, contents)
+}
+
+// encodeCodeSection encodes a wasm.SectionIDCode for the module-defined function in WebAssembly 1.0 (20191205)
+// Binary Format.
+//
+// See encodeCode
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#code-section%E2%91%A0
+func encodeCodeSection(code []*wasm.Code) []byte {
+	contents := leb128.EncodeUint32(uint32(len(code)))
+	for _, i := range code {
+		contents = append(contents, encodeCode(i)...)
+	}
+	return encodeSection(wasm.SectionIDCode, contents)
+}
+
+// encodeTableSection encodes a wasm.SectionIDTable for the module-defined function in WebAssembly 1.0
+// (20191205) Binary Format.
+//
+// See EncodeTable
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-section%E2%91%A0
+func encodeTableSection(tables []*wasm.Table) []byte {
+	var contents []byte = leb128.EncodeUint32(uint32(len(tables)))
+	for _, table := range tables {
+		contents = append(contents, EncodeTable(table)...)
+	}
+	return encodeSection(wasm.SectionIDTable, contents)
+}
+
+// encodeMemorySection encodes a wasm.SectionIDMemory for the module-defined function in WebAssembly 1.0
+// (20191205) Binary Format.
+//
+// See EncodeMemory
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0
+func encodeMemorySection(memory *wasm.Memory) []byte {
+	contents := append([]byte{1}, EncodeMemory(memory)...)
+	return encodeSection(wasm.SectionIDMemory, contents)
+}
+
+// encodeGlobalSection encodes a wasm.SectionIDGlobal for the given globals in WebAssembly 1.0 (20191205) Binary
+// Format.
+//
+// See encodeGlobal
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0
+func encodeGlobalSection(globals []*wasm.Global) []byte {
+	contents := leb128.EncodeUint32(uint32(len(globals)))
+	for _, g := range globals {
+		contents = append(contents, encodeGlobal(g)...)
+	}
+	return encodeSection(wasm.SectionIDGlobal, contents)
+}
+
+// encodeExportSection encodes a wasm.SectionIDExport for the given exports in WebAssembly 1.0 (20191205) Binary
+// Format.
+//
+// See encodeExport
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0
+func encodeExportSection(exports []*wasm.Export) []byte {
+	contents := leb128.EncodeUint32(uint32(len(exports)))
+	for _, e := range exports {
+		contents = append(contents, encodeExport(e)...)
+	}
+	return encodeSection(wasm.SectionIDExport, contents)
+}
+
+// EncodeStartSection encodes a wasm.SectionIDStart for the given function index in WebAssembly 1.0 (20191205)
+// Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#start-section%E2%91%A0
+func EncodeStartSection(funcidx wasm.Index) []byte {
+	return encodeSection(wasm.SectionIDStart, leb128.EncodeUint32(funcidx))
+}
+
+// encodeEelementSection encodes a wasm.SectionIDElement for the elements in WebAssembly 1.0 (20191205)
+// Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#element-section%E2%91%A0
+func encodeElementSection(elements []*wasm.ElementSegment) []byte {
+	contents := leb128.EncodeUint32(uint32(len(elements)))
+	for _, e := range elements {
+		contents = append(contents, encodeElement(e)...)
+	}
+	return encodeSection(wasm.SectionIDElement, contents)
+}
+
+// encodeDataSection encodes a wasm.SectionIDData for the data in WebAssembly 1.0 (20191205)
+// Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#data-section%E2%91%A0
+func encodeDataSection(datum []*wasm.DataSegment) []byte {
+	contents := leb128.EncodeUint32(uint32(len(datum)))
+	for _, d := range datum {
+		contents = append(contents, encodeDataSegment(d)...)
+	}
+	return encodeSection(wasm.SectionIDData, contents)
+}
diff --git a/internal/testing/binaryencoding/section_test.go b/internal/testing/binaryencoding/section_test.go
new file mode 100644
index 00000000..9cde8eb6
--- /dev/null
+++ b/internal/testing/binaryencoding/section_test.go
@@ -0,0 +1,17 @@
+package binaryencoding
+
+import (
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func TestEncodeFunctionSection(t *testing.T) {
+	require.Equal(t, []byte{wasm.SectionIDFunction, 0x2, 0x01, 0x05}, EncodeFunctionSection([]wasm.Index{5}))
+}
+
+// TestEncodeStartSection uses the same index as TestEncodeFunctionSection to highlight the encoding is different.
+func TestEncodeStartSection(t *testing.T) {
+	require.Equal(t, []byte{wasm.SectionIDStart, 0x01, 0x05}, EncodeStartSection(5))
+}
diff --git a/internal/testing/binaryencoding/table.go b/internal/testing/binaryencoding/table.go
new file mode 100644
index 00000000..2985dbdb
--- /dev/null
+++ b/internal/testing/binaryencoding/table.go
@@ -0,0 +1,12 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+// EncodeTable returns the wasm.Table encoded in WebAssembly 1.0 (20191205) Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-table
+func EncodeTable(i *wasm.Table) []byte {
+	return append([]byte{i.Type}, EncodeLimitsType(i.Min, i.Max)...)
+}
diff --git a/internal/testing/binaryencoding/value.go b/internal/testing/binaryencoding/value.go
new file mode 100644
index 00000000..9ec583e9
--- /dev/null
+++ b/internal/testing/binaryencoding/value.go
@@ -0,0 +1,41 @@
+package binaryencoding
+
+import (
+	"github.com/tetratelabs/wazero/internal/leb128"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+var noValType = []byte{0}
+
+// encodedValTypes is a cache of size prefixed binary encoding of known val types.
+var encodedValTypes = map[wasm.ValueType][]byte{
+	wasm.ValueTypeI32:       {1, wasm.ValueTypeI32},
+	wasm.ValueTypeI64:       {1, wasm.ValueTypeI64},
+	wasm.ValueTypeF32:       {1, wasm.ValueTypeF32},
+	wasm.ValueTypeF64:       {1, wasm.ValueTypeF64},
+	wasm.ValueTypeExternref: {1, wasm.ValueTypeExternref},
+	wasm.ValueTypeFuncref:   {1, wasm.ValueTypeFuncref},
+	wasm.ValueTypeV128:      {1, wasm.ValueTypeV128},
+}
+
+// EncodeValTypes fast paths binary encoding of common value type lengths
+func EncodeValTypes(vt []wasm.ValueType) []byte {
+	// Special case nullary and parameter lengths of wasi_snapshot_preview1 to avoid excess allocations
+	switch uint32(len(vt)) {
+	case 0: // nullary
+		return noValType
+	case 1: // ex $wasi.fd_close or any result
+		if encoded, ok := encodedValTypes[vt[0]]; ok {
+			return encoded
+		}
+	case 2: // ex $wasi.environ_sizes_get
+		return []byte{2, vt[0], vt[1]}
+	case 4: // ex $wasi.fd_write
+		return []byte{4, vt[0], vt[1], vt[2], vt[3]}
+	case 9: // ex $wasi.fd_write
+		return []byte{9, vt[0], vt[1], vt[2], vt[3], vt[4], vt[5], vt[6], vt[7], vt[8]}
+	}
+	// Slow path others until someone complains with a valid signature
+	count := leb128.EncodeUint32(uint32(len(vt)))
+	return append(count, vt...)
+}
diff --git a/internal/testing/binaryencoding/value_test.go b/internal/testing/binaryencoding/value_test.go
new file mode 100644
index 00000000..b3110474
--- /dev/null
+++ b/internal/testing/binaryencoding/value_test.go
@@ -0,0 +1,112 @@
+package binaryencoding
+
+import (
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func TestEncodeValTypes(t *testing.T) {
+	i32, i64, f32, f64, ext, fref := wasm.ValueTypeI32, wasm.ValueTypeI64, wasm.ValueTypeF32, wasm.ValueTypeF64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref
+	tests := []struct {
+		name     string
+		input    []wasm.ValueType
+		expected []byte
+	}{
+		{
+			name:     "empty",
+			input:    []wasm.ValueType{},
+			expected: []byte{0},
+		},
+		{
+			name:     "undefined", // ensure future spec changes don't panic
+			input:    []wasm.ValueType{0x6f},
+			expected: []byte{1, 0x6f},
+		},
+		{
+			name:     "funcref",
+			input:    []wasm.ValueType{fref},
+			expected: []byte{1, fref},
+		},
+		{
+			name:     "externref",
+			input:    []wasm.ValueType{ext},
+			expected: []byte{1, ext},
+		},
+		{
+			name:     "i32",
+			input:    []wasm.ValueType{i32},
+			expected: []byte{1, i32},
+		},
+		{
+			name:     "i64",
+			input:    []wasm.ValueType{i64},
+			expected: []byte{1, i64},
+		},
+		{
+			name:     "f32",
+			input:    []wasm.ValueType{f32},
+			expected: []byte{1, f32},
+		},
+		{
+			name:     "f64",
+			input:    []wasm.ValueType{f64},
+			expected: []byte{1, f64},
+		},
+		{
+			name:     "i32i64",
+			input:    []wasm.ValueType{i32, i64},
+			expected: []byte{2, i32, i64},
+		},
+		{
+			name:     "i32i64f32",
+			input:    []wasm.ValueType{i32, i64, f32},
+			expected: []byte{3, i32, i64, f32},
+		},
+		{
+			name:     "i32i64f32f64",
+			input:    []wasm.ValueType{i32, i64, f32, f64},
+			expected: []byte{4, i32, i64, f32, f64},
+		},
+		{
+			name:     "i32i64f32f64i32",
+			input:    []wasm.ValueType{i32, i64, f32, f64, i32},
+			expected: []byte{5, i32, i64, f32, f64, i32},
+		},
+		{
+			name:     "i32i64f32f64i32i64",
+			input:    []wasm.ValueType{i32, i64, f32, f64, i32, i64},
+			expected: []byte{6, i32, i64, f32, f64, i32, i64},
+		},
+		{
+			name:     "i32i64f32f64i32i64f32",
+			input:    []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32},
+			expected: []byte{7, i32, i64, f32, f64, i32, i64, f32},
+		},
+		{
+			name:     "i32i64f32f64i32i64f32f64",
+			input:    []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32, f64},
+			expected: []byte{8, i32, i64, f32, f64, i32, i64, f32, f64},
+		},
+		{
+			name:     "i32i64f32f64i32i64f32f64i32",
+			input:    []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32, f64, i32},
+			expected: []byte{9, i32, i64, f32, f64, i32, i64, f32, f64, i32},
+		},
+		{
+			name:     "i32i64f32f64i32i64f32f64i32i64",
+			input:    []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32, f64, i32, i64},
+			expected: []byte{10, i32, i64, f32, f64, i32, i64, f32, f64, i32, i64},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			bytes := EncodeValTypes(tc.input)
+			require.Equal(t, tc.expected, bytes)
+		})
+	}
+}
diff --git a/internal/testing/proxy/proxy.go b/internal/testing/proxy/proxy.go
index 3691641c..c444e6d7 100644
--- a/internal/testing/proxy/proxy.go
+++ b/internal/testing/proxy/proxy.go
@@ -6,8 +6,8 @@ import (
 	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/experimental/logging"
 	"github.com/tetratelabs/wazero/internal/leb128"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/wasm"
-	binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary"
 )
 
 const proxyModuleName = "internal/testing/proxy/proxy.go"
@@ -95,5 +95,5 @@ func NewModuleBinary(moduleName string, proxyTarget wazero.CompiledModule) []byt
 		})
 		cnt++
 	}
-	return binaryformat.EncodeModule(proxyModule)
+	return binaryencoding.EncodeModule(proxyModule)
 }
diff --git a/internal/wasm/binary/decoder_test.go b/internal/wasm/binary/decoder_test.go
index aa06aa9c..f0bb2a68 100644
--- a/internal/wasm/binary/decoder_test.go
+++ b/internal/wasm/binary/decoder_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/dwarftestdata"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
@@ -82,7 +83,7 @@ func TestDecodeModule(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			m, e := DecodeModule(EncodeModule(tc.input), api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false, false)
+			m, e := DecodeModule(binaryencoding.EncodeModule(tc.input), api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false, false)
 			require.NoError(t, e)
 			// Set the FunctionType keys on the input.
 			for _, f := range tc.input.TypeSection {
diff --git a/internal/wasm/binary/function_test.go b/internal/wasm/binary/function_test.go
index d73fa1c0..354ab75a 100644
--- a/internal/wasm/binary/function_test.go
+++ b/internal/wasm/binary/function_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -77,7 +78,7 @@ func TestFunctionType(t *testing.T) {
 	for _, tt := range tests {
 		tc := tt
 
-		b := encodeFunctionType(tc.input)
+		b := binaryencoding.EncodeFunctionType(tc.input)
 		t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) {
 			require.Equal(t, tc.expected, b)
 		})
diff --git a/internal/wasm/binary/import_test.go b/internal/wasm/binary/import_test.go
index bf637983..1679671f 100644
--- a/internal/wasm/binary/import_test.go
+++ b/internal/wasm/binary/import_test.go
@@ -3,6 +3,7 @@ package binary
 import (
 	"testing"
 
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -154,7 +155,7 @@ func TestEncodeImport(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			bytes := encodeImport(tc.input)
+			bytes := binaryencoding.EncodeImport(tc.input)
 			require.Equal(t, tc.expected, bytes)
 		})
 	}
diff --git a/internal/wasm/binary/limits_test.go b/internal/wasm/binary/limits_test.go
index 4c56f09f..e6f66393 100644
--- a/internal/wasm/binary/limits_test.go
+++ b/internal/wasm/binary/limits_test.go
@@ -6,6 +6,7 @@ import (
 	"math"
 	"testing"
 
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
@@ -49,7 +50,7 @@ func TestLimitsType(t *testing.T) {
 	for _, tt := range tests {
 		tc := tt
 
-		b := encodeLimitsType(tc.min, tc.max)
+		b := binaryencoding.EncodeLimitsType(tc.min, tc.max)
 		t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) {
 			require.Equal(t, tc.expected, b)
 		})
diff --git a/internal/wasm/binary/memory_test.go b/internal/wasm/binary/memory_test.go
index ce983505..1c2c7840 100644
--- a/internal/wasm/binary/memory_test.go
+++ b/internal/wasm/binary/memory_test.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -134,7 +135,7 @@ func TestMemoryType(t *testing.T) {
 	for _, tt := range tests {
 		tc := tt
 
-		b := encodeMemory(tc.input)
+		b := binaryencoding.EncodeMemory(tc.input)
 		t.Run(fmt.Sprintf("encode %s", tc.name), func(t *testing.T) {
 			require.Equal(t, tc.expected, b)
 		})
diff --git a/internal/wasm/binary/names_test.go b/internal/wasm/binary/names_test.go
index aca225c2..8e84e218 100644
--- a/internal/wasm/binary/names_test.go
+++ b/internal/wasm/binary/names_test.go
@@ -4,149 +4,11 @@ import (
 	"bytes"
 	"testing"
 
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
-func TestEncodeNameSectionData(t *testing.T) {
-	tests := []struct {
-		name     string
-		input    *wasm.NameSection
-		expected []byte
-	}{
-		{
-			name:  "empty",
-			input: &wasm.NameSection{},
-		},
-		{
-			name: "only module",
-			// e.g. (module $simple )
-			input: &wasm.NameSection{ModuleName: "simple"},
-			expected: []byte{
-				subsectionIDModuleName, 0x07, // 7 bytes
-				0x06, // the Module name simple is 6 bytes long
-				's', 'i', 'm', 'p', 'l', 'e',
-			},
-		},
-		{
-			name: "module and function name",
-			//	(module $simple
-			//		(import "" "Hello" (func $hello))
-			//		(start $hello)
-			//	)
-			input: &wasm.NameSection{
-				ModuleName:    "simple",
-				FunctionNames: wasm.NameMap{{Index: wasm.Index(0), Name: "hello"}},
-			},
-			expected: []byte{
-				subsectionIDModuleName, 0x07, // 7 bytes
-				0x06, // the Module name simple is 6 bytes long
-				's', 'i', 'm', 'p', 'l', 'e',
-				subsectionIDFunctionNames, 0x08, // 8 bytes
-				0x01, // one function name
-				0x00, // the function index is zero
-				0x05, // the function name hello is 5 bytes long
-				'h', 'e', 'l', 'l', 'o',
-			},
-		},
-		{
-			name: "two function names", // e.g. TinyGo which at one point didn't set a module name
-			//	(module
-			//		(import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (param i32, i32) (result i32)))
-			//		(import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (param i32, i32, i32, i32) (result i32)))
-			//	)
-			input: &wasm.NameSection{
-				FunctionNames: wasm.NameMap{
-					{Index: wasm.Index(0), Name: "wasi.args_sizes_get"},
-					{Index: wasm.Index(1), Name: "wasi.fd_write"},
-				},
-			},
-			expected: []byte{
-				subsectionIDFunctionNames, 0x25, // 37 bytes
-				0x02, // two function names
-				0x00, // the function index is zero
-				0x13, // the function name wasi.args_sizes_get is 19 bytes long
-				'w', 'a', 's', 'i', '.', 'a', 'r', 'g', 's', '_', 's', 'i', 'z', 'e', 's', '_', 'g', 'e', 't',
-				0x01, // the function index is one
-				0x0d, // the function name wasi.fd_write is 13 bytes long
-				'w', 'a', 's', 'i', '.', 'f', 'd', '_', 'w', 'r', 'i', 't', 'e',
-			},
-		},
-		{
-			name: "function with local names",
-			//	(module
-			//		(import "Math" "Mul" (func $mul (param $x f32) (param $y f32) (result f32)))
-			//		(import "Math" "Add" (func $add (param $l f32) (param $r f32) (result f32)))
-			//	)
-			input: &wasm.NameSection{
-				FunctionNames: wasm.NameMap{
-					{Index: wasm.Index(0), Name: "mul"},
-					{Index: wasm.Index(1), Name: "add"},
-				},
-				LocalNames: wasm.IndirectNameMap{
-					{Index: wasm.Index(0), NameMap: wasm.NameMap{
-						{Index: wasm.Index(0), Name: "x"},
-						{Index: wasm.Index(1), Name: "y"},
-					}},
-					{Index: wasm.Index(1), NameMap: wasm.NameMap{
-						{Index: wasm.Index(0), Name: "l"},
-						{Index: wasm.Index(1), Name: "r"},
-					}},
-				},
-			},
-			expected: []byte{
-				subsectionIDFunctionNames, 0x0b, // 7 bytes
-				0x02,                      // two function names
-				0x00, 0x03, 'm', 'u', 'l', // index 0, size of "mul", "mul"
-				0x01, 0x03, 'a', 'd', 'd', // index 1, size of "add", "add"
-				subsectionIDLocalNames, 0x11, // 17 bytes
-				0x02,       // two functions
-				0x00, 0x02, // index 0 has 2 locals
-				0x00, 0x01, 'x', // index 0, size of "x", "x"
-				0x01, 0x01, 'y', // index 1, size of "y", "y"
-				0x01, 0x02, // index 1 has 2 locals
-				0x00, 0x01, 'l', // index 0, size of "l", "l"
-				0x01, 0x01, 'r', // index 1, size of "r", "r"
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		tc := tt
-
-		t.Run(tc.name, func(t *testing.T) {
-			bytes := encodeNameSectionData(tc.input)
-			require.Equal(t, tc.expected, bytes)
-		})
-	}
-}
-
-func TestEncodeNameSubsection(t *testing.T) {
-	subsectionID := uint8(1)
-	name := []byte("simple")
-	require.Equal(t, []byte{
-		subsectionID,
-		byte(1 + 6), // 1 is the size of 6 in LEB128 encoding
-		6, 's', 'i', 'm', 'p', 'l', 'e',
-	}, encodeNameSubsection(subsectionID, encodeSizePrefixed(name)))
-}
-
-func TestEncodeNameAssoc(t *testing.T) {
-	na := &wasm.NameAssoc{Index: 1, Name: "hello"}
-	require.Equal(t, []byte{byte(na.Index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameAssoc(na))
-}
-
-func TestEncodeNameMap(t *testing.T) {
-	na := &wasm.NameAssoc{Index: 1, Name: "hello"}
-	m := wasm.NameMap{na}
-	require.Equal(t, []byte{byte(1), byte(na.Index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameMap(m))
-}
-
-func TestEncodeSizePrefixed(t *testing.T) {
-	// We expect size in bytes (LEB128 encoded) then the bytes
-	require.Equal(t, []byte{5, 'h', 'e', 'l', 'l', 'o'}, encodeSizePrefixed([]byte("hello")))
-}
-
 // TestDecodeNameSection relies on unit tests for NameSection.EncodeData, specifically that the encoding is
 // both known and correct. This avoids having to copy/paste or share variables to assert against byte arrays.
 func TestDecodeNameSection(t *testing.T) {
@@ -203,7 +65,7 @@ func TestDecodeNameSection(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			data := encodeNameSectionData(tc.input)
+			data := binaryencoding.EncodeNameSectionData(tc.input)
 			ns, err := decodeNameSection(bytes.NewReader(data), uint64(len(data)))
 			require.NoError(t, err)
 			require.Equal(t, tc.input, ns)
diff --git a/internal/wasm/binary/section_test.go b/internal/wasm/binary/section_test.go
index c2b1bc75..05f406f3 100644
--- a/internal/wasm/binary/section_test.go
+++ b/internal/wasm/binary/section_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -213,12 +214,12 @@ func TestDecodeExportSection_Errors(t *testing.T) {
 }
 
 func TestEncodeFunctionSection(t *testing.T) {
-	require.Equal(t, []byte{wasm.SectionIDFunction, 0x2, 0x01, 0x05}, encodeFunctionSection([]wasm.Index{5}))
+	require.Equal(t, []byte{wasm.SectionIDFunction, 0x2, 0x01, 0x05}, binaryencoding.EncodeFunctionSection([]wasm.Index{5}))
 }
 
 // TestEncodeStartSection uses the same index as TestEncodeFunctionSection to highlight the encoding is different.
 func TestEncodeStartSection(t *testing.T) {
-	require.Equal(t, []byte{wasm.SectionIDStart, 0x01, 0x05}, encodeStartSection(5))
+	require.Equal(t, []byte{wasm.SectionIDStart, 0x01, 0x05}, binaryencoding.EncodeStartSection(5))
 }
 
 func TestDecodeDataCountSection(t *testing.T) {
diff --git a/internal/wasm/binary/table_test.go b/internal/wasm/binary/table_test.go
index a91000df..c4dc327d 100644
--- a/internal/wasm/binary/table_test.go
+++ b/internal/wasm/binary/table_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -54,7 +55,7 @@ func TestTableType(t *testing.T) {
 	for _, tt := range tests {
 		tc := tt
 
-		b := encodeTable(tc.input)
+		b := binaryencoding.EncodeTable(tc.input)
 		t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) {
 			require.Equal(t, tc.expected, b)
 		})
diff --git a/internal/wasm/binary/value_test.go b/internal/wasm/binary/value_test.go
index 44af4d96..41f2a24e 100644
--- a/internal/wasm/binary/value_test.go
+++ b/internal/wasm/binary/value_test.go
@@ -3,6 +3,7 @@ package binary
 import (
 	"testing"
 
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -105,7 +106,7 @@ func TestEncodeValTypes(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			bytes := encodeValTypes(tc.input)
+			bytes := binaryencoding.EncodeValTypes(tc.input)
 			require.Equal(t, tc.expected, bytes)
 		})
 	}
diff --git a/runtime_test.go b/runtime_test.go
index df549be3..55d8a40a 100644
--- a/runtime_test.go
+++ b/runtime_test.go
@@ -13,14 +13,14 @@ import (
 	"github.com/tetratelabs/wazero/internal/filecache"
 	"github.com/tetratelabs/wazero/internal/leb128"
 	"github.com/tetratelabs/wazero/internal/platform"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
-	binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary"
 	"github.com/tetratelabs/wazero/sys"
 )
 
 var (
-	binaryNamedZero = binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "0"}})
+	binaryNamedZero = binaryencoding.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "0"}})
 	// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
 	testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
 )
@@ -50,22 +50,22 @@ func TestRuntime_CompileModule(t *testing.T) {
 	}{
 		{
 			name: "no name section",
-			wasm: binaryformat.EncodeModule(&wasm.Module{}),
+			wasm: binaryencoding.EncodeModule(&wasm.Module{}),
 		},
 		{
 			name: "empty NameSection.ModuleName",
-			wasm: binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{}}),
+			wasm: binaryencoding.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{}}),
 		},
 		{
 			name: "NameSection.ModuleName",
-			wasm: binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}}),
+			wasm: binaryencoding.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}}),
 			expected: func(compiled CompiledModule) {
 				require.Equal(t, "test", compiled.Name())
 			},
 		},
 		{
 			name: "FunctionSection, but not exported",
-			wasm: binaryformat.EncodeModule(&wasm.Module{
+			wasm: binaryencoding.EncodeModule(&wasm.Module{
 				TypeSection:     []*wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}}},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{{Body: []byte{wasm.OpcodeEnd}}},
@@ -77,7 +77,7 @@ func TestRuntime_CompileModule(t *testing.T) {
 		},
 		{
 			name: "FunctionSection exported",
-			wasm: binaryformat.EncodeModule(&wasm.Module{
+			wasm: binaryencoding.EncodeModule(&wasm.Module{
 				TypeSection:     []*wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}}},
 				FunctionSection: []wasm.Index{0},
 				CodeSection:     []*wasm.Code{{Body: []byte{wasm.OpcodeEnd}}},
@@ -95,7 +95,7 @@ func TestRuntime_CompileModule(t *testing.T) {
 		},
 		{
 			name: "MemorySection, but not exported",
-			wasm: binaryformat.EncodeModule(&wasm.Module{
+			wasm: binaryencoding.EncodeModule(&wasm.Module{
 				MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true},
 			}),
 			expected: func(compiled CompiledModule) {
@@ -105,7 +105,7 @@ func TestRuntime_CompileModule(t *testing.T) {
 		},
 		{
 			name: "MemorySection exported",
-			wasm: binaryformat.EncodeModule(&wasm.Module{
+			wasm: binaryencoding.EncodeModule(&wasm.Module{
 				MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true},
 				ExportSection: []*wasm.Export{{
 					Type:  wasm.ExternTypeMemory,
@@ -154,12 +154,12 @@ func TestRuntime_CompileModule_Errors(t *testing.T) {
 		},
 		{
 			name:        "invalid binary",
-			wasm:        append(binaryformat.Magic, []byte("yolo")...),
+			wasm:        append(binaryencoding.Magic, []byte("yolo")...),
 			expectedErr: "invalid version header",
 		},
 		{
 			name:        "memory has too many pages",
-			wasm:        binaryformat.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: 70000, IsMaxEncoded: true}}),
+			wasm:        binaryencoding.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: 70000, IsMaxEncoded: true}}),
 			expectedErr: "section memory: max 70000 pages (4 Gi) over limit of 65536 pages (4 Gi)",
 		},
 	}
@@ -187,11 +187,11 @@ func TestModule_Memory(t *testing.T) {
 	}{
 		{
 			name: "no memory",
-			wasm: binaryformat.EncodeModule(&wasm.Module{}),
+			wasm: binaryencoding.EncodeModule(&wasm.Module{}),
 		},
 		{
 			name: "memory exported, one page",
-			wasm: binaryformat.EncodeModule(&wasm.Module{
+			wasm: binaryencoding.EncodeModule(&wasm.Module{
 				MemorySection: &wasm.Memory{Min: 1},
 				ExportSection: []*wasm.Export{{Name: "memory", Type: api.ExternTypeMemory}},
 			}),
@@ -333,7 +333,7 @@ func TestRuntime_InstantiateModule_UsesContext(t *testing.T) {
 	require.NoError(t, err)
 
 	one := uint32(1)
-	binary := binaryformat.EncodeModule(&wasm.Module{
+	binary := binaryencoding.EncodeModule(&wasm.Module{
 		TypeSection:     []*wasm.FunctionType{{}},
 		ImportSection:   []*wasm.Import{{Module: "env", Name: "start", Type: wasm.ExternTypeFunc, DescFunc: 0}},
 		FunctionSection: []wasm.Index{0},
@@ -363,7 +363,7 @@ func TestRuntime_Instantiate_DoesntEnforce_Start(t *testing.T) {
 	r := NewRuntime(testCtx)
 	defer r.Close(testCtx)
 
-	binary := binaryformat.EncodeModule(&wasm.Module{
+	binary := binaryencoding.EncodeModule(&wasm.Module{
 		MemorySection: &wasm.Memory{Min: 1},
 		ExportSection: []*wasm.Export{{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0}},
 	})
@@ -462,7 +462,7 @@ func TestRuntime_InstantiateModule_ExitError(t *testing.T) {
 	require.NoError(t, err)
 
 	one := uint32(1)
-	binary := binaryformat.EncodeModule(&wasm.Module{
+	binary := binaryencoding.EncodeModule(&wasm.Module{
 		TypeSection:     []*wasm.FunctionType{{}},
 		ImportSection:   []*wasm.Import{{Module: "env", Name: "exit", Type: wasm.ExternTypeFunc, DescFunc: 0}},
 		FunctionSection: []wasm.Index{0},
@@ -481,7 +481,7 @@ func TestRuntime_InstantiateModule_ExitError(t *testing.T) {
 }
 
 func TestRuntime_CloseWithExitCode(t *testing.T) {
-	bin := binaryformat.EncodeModule(&wasm.Module{
+	bin := binaryencoding.EncodeModule(&wasm.Module{
 		TypeSection:     []*wasm.FunctionType{{}},
 		FunctionSection: []wasm.Index{0},
 		CodeSection:     []*wasm.Code{{Body: []byte{wasm.OpcodeEnd}}},
@@ -585,7 +585,7 @@ func TestHostFunctionWithCustomContext(t *testing.T) {
 	require.NoError(t, err)
 
 	one := uint32(0)
-	binary := binaryformat.EncodeModule(&wasm.Module{
+	binary := binaryencoding.EncodeModule(&wasm.Module{
 		TypeSection: []*wasm.FunctionType{{}, {}},
 		ImportSection: []*wasm.Import{
 			{Module: "env", Name: "host", Type: wasm.ExternTypeFunc, DescFunc: 0},