Repository: tetratelabs/wazero
Pull request #1396 (2023-04-24 09:19:48)

Proposal: "close" certain api interfaces for external implementation
As discussed in https://github.com/tetratelabs/wazero/pull/1332#issuecomment-1510010359, many of the interfaces in [`github.com/tetratelabs/wazero`](https://pkg.go.dev/github.com/tetratelabs/wazero) and [`github.com/tetratelabs/wazero/api`](https://pkg.go.dev/github.com/tetratelabs/wazero/api) are not meant to be externally implemented.

In strict terms, an interface can't be extended after it has been made public. Once made public, alternative implementations may be created, and adding a method to such an interface, means those implementations no longer compile, which breaks semver.

wazero 1.0 shipped with many such interfaces, which we may want to extend (e.g. #1332).

These are the paths available to us:
1. **that ship has sailed**: wazero 1.0 shipped, we simply can't extend these interfaces;
2. **documentation**: document *now* that certain interfaces are not meant to be implemented outside wazero;
3. **this PR**: "enforce" this, avoiding users implementing them by accident.

Option **1** is basically doing nothing. In the context of #1332 I can propose a different alternative, which will basically be a new `api.Function2` (name not important) interface that wraps the [`CallWithStack`](https://github.com/tetratelabs/wazero/pull/1332#issuecomment-1507793809) method. Then users either type-assert this interface, or we create new builders returning this interface.

Options **2** and **3** require deciding which interfaces are only for internal implementation.

Option **2** is a documentation change to [`github.com/tetratelabs/wazero`](https://pkg.go.dev/github.com/tetratelabs/wazero) and [`github.com/tetratelabs/wazero/api`](https://pkg.go.dev/github.com/tetratelabs/wazero/api), and to each such interface.

Option **3** is this PR, repeated for every such interface.

In the documentation of each of these interfaces, you'll [see](https://pkg.go.dev/go/types#Object):
```go
type Interface interface {
	PublicMethod() // public method
	// contains filtered or unexported methods
}
```

When you try to implement such an interface (create your own type and use it as if it implemented it), you get this error message:
```
cannot use &myCallEngine{…} (value of type *myCallEngine) as api.Function value in return statement: *myCallEngine does not implement api.Function (missing method wazeroOnly)
```

If you're doing it for mocking, which is the only way you'd do it for these wazero types, you can still deliberately sidestep the check by embedding the interface:
```go
type mockCallEngine struct {
	index         Index
	callFailIndex int
	api.Function
}
```

codefromthecrypt: Below should have been added to all interfaces except those that require the users to implement them. What happened was we did this to the top-level ones and didn't go recursively down when we should have.

```
// # Notes
//
//   - This is an interface for decoupling, not third-party implementations. All implementations are in wazero.
```

We should not escalate this doc bug as a perpetual promise. The only way someone could even use api.Function would be to wrap, and that would be a hard job as they'd also have to wrap what it returns. I suspect there are max one or two people who could have done this and we can apologize to them individually for the doc bug. If this includes you, please accept my apology!


Knowing this, here's my 2p

*  I mentioned to someone I think about the missing docs, but I should have done them myself to make sure they landed in the last release. I'll do that now, so that it remain any longer.
* I'm fine also adding the private thingy, but not as an alternative to docs. This should be in addition to docs.
* Unlike api.GoModuleFunction, api.Function was not documented for implementation. Feel free to add the function you need, I think I mentioned this offline a while but if I hadn't apologies!
codefromthecrypt: https://github.com/tetratelabs/wazero/pull/1397

Sorry, I should have done this immediately when it was brought up last, as it would have been in 1.0.3, then. I often leave work that is easy to do for others, as a way for folks to up stats etc. This is one of those things I should have done right away.
achille-roussel: I thought it might be worth chiming in with some examples of how the interfaces can be useful as extension mechanisms. I'm guilty of abusing the documentation stating that they should not be implemented outside of wazero, but there's an interesting use case in wazergo I think https://github.com/stealthrocket/wazergo/blob/main/module.go#L211-L222

The `wazergo.ModuleInstance[T]` type implements `api.Module` so when accessing exported functions, the context needed to invoke methods of the host module can be injected automatically.

I'm calling it out for two reasons:
- if the documented rules are not enforced, they will likely continue to be abused (by me probably, but it's unlikely that I will remain the only offender)
- there may be useful extension opportunities using the interfaces defined in wazero, maybe a follow up could be to start a discussion around how/where it makes sense for wazero to allow applications to leverage interfaces for customization

When this PR gets merged it will break wazergo so I'll follow up with a fix in that repository to address.
ncruces: You're for sure not the only one doing it, @achille-roussel. I [have](https://github.com/ncruces/go-sqlite3/blob/main/internal/util/mock.go) `mockModule` and `mockMemory` for testing.

The major difference is that (my) mocks don't ship downstream, so it's not an issue if my clients update `wazero` to a version newer than the one I used

OTOH, `wazergo` might get broken if users update `wazero` beyond the one declared in `wazergo` `go.mod`, which is an issue I don't really know to fix, except by saying `wazero` **can't** ever update these public interfaces.
achille-roussel: wazergo is not in 1.0 and the README calls out that some breaking changes may be needed for things like "design flaws" so I think it's OK at this stage. 
codefromthecrypt: FWIW if there's a secret way to "use at your own risk" I think it is fair to try not to break vs intentionally break.

in any case, the break would be adding new methods, not removing or changing existing ones. So, this means we would only break those who are sneakily wrapping. OTOH this means those wrapping can never be non-experimental or we can decide a version that can't add either.

For example, we can say each even version (e.g. 2.0) is super LTS meaning no functions added either. I don't think we should do that on 1.0 though.
codefromthecrypt: Funny or not, I'm fine with the name. Looking good!

Since there was some chatter on this, I think when ready a RATIONALE.md update is warranted. you can see some other notes about things we do that may be a bit strange in there. One note to put into this is that we are only adding these to "api" (as opposed to all non-internal interfaces) because "api" is most likely what users would expect to implement vs just consume.

Patch:
diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml
index 685cea99..a39cd4ea 100644
--- a/.github/workflows/commit.yaml
+++ b/.github/workflows/commit.yaml
@@ -76,12 +76,9 @@ jobs:
           go-version: ${{ matrix.go-version }}
           cache: true
 
-      - run: make test
-        if: ${{ github.event_name  == 'pull_request' }}
-
-      # Run tests with -race only on main branch push.
+      # Run -race could be really slow without -short, so run them together on this workflow.
+      # Since -short is not added in the scratch tests, all the tests are run in CI in practice.
       - run: make test go_test_options='-timeout 10m -race -short'
-        if: ${{ github.event_name  == 'push' }}
 
       - name: "Generate coverage report"  # only once (not per OS)
         if: runner.os == 'Linux'
diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml
index 99e3c1f5..909e8133 100644
--- a/.github/workflows/integration.yaml
+++ b/.github/workflows/integration.yaml
@@ -362,9 +362,7 @@ jobs:
     strategy:
       fail-fast: false # don't fail fast as sometimes failures are arch/OS specific
       matrix:
-        # TODO: windows-2022 - a bug around mount path like
-        # invalid mount: path "C:\\Program Files\\Git\\;C" error: CreateFile C:\Program Files\Git\;C: The system cannot find the file specified.
-        os: [ubuntu-22.04, macos-12]
+        os: [ubuntu-22.04, macos-12, windows-2022]
 
     steps:
       - name: Install Go
@@ -464,7 +462,8 @@ jobs:
       - name: Install wazero
         run: go install ./cmd/wazero
 
-      - name: Run standard library tests
+      - if: ${{ runner.os != 'Windows' }}
+        name: Run standard library tests
         run: |
           echo "Running $(find ~/sdk/tests -name *.test | wc -l) test binaries"
 
@@ -475,3 +474,45 @@ jobs:
             wazero run -mount=/:/ -env PWD=$PWD $bin -- -test.short -test.v
             popd
           done
+
+      - if: ${{ runner.os == 'Windows' }}
+        name: Run standard library tests
+        # Ack failures on Windows. https://github.com/tetratelabs/wazero/issues/1410
+        continue-on-error: true
+        run: |
+          GOOS=$(go env GOOS)
+          echo "Running $(find ~/sdk/tests -name *.test | wc -l) test binaries"
+
+          GOROOT=~/sdk/gotip
+
+          MOUNT=c:\\:/
+          SCRIPT="$HOME/sdk/tests.cmd"
+          # Trim `/c` from the in-Wasm GOROOT.
+          GOROOT=$(cygpath -u $GOROOT | cut -c 3-)
+          # Append early exit on cmd.
+          POSTFIX="if %errorlevel% neq 0 exit /b %errorlevel%"
+          RUNNER="cmd //c %USERPROFILE%\sdk\tests.cmd"
+          EXTRAPARAMS="-mount=%TEMP%:/tmp"
+
+          # Go tests often look for files relative to the source. Change to the corresponding directory.
+          for bin in ~/sdk/tests/*.test; do
+            dir=$(basename $bin); dir=${dir%.test}; dir=${dir//_/\/}
+            pushd ~/sdk/gotip/$dir
+
+            # Trim `/c` from the in-Wasm pwd.
+            IN_WASM_PWD=$(pwd | cut -c 3-)
+            # Convert to a Windows path.
+            bin=`cygpath -w $bin`
+
+            # Create a script with all the tests (do not run yet).
+            echo ${MOUNT} ${IN_WASM_PWD} ~/sdk/gotip/$dir
+            COMMAND="wazero run -mount=${MOUNT} ${EXTRAPARAMS} -hostlogging=filesystem -env PWD=${IN_WASM_PWD} -env GOROOT=${GOROOT} -env GOOS=wasip1 $bin -- -test.short -test.v"
+            echo $COMMAND >> $SCRIPT
+            # Uncomment the following line for early exit on error on Windows.
+            # Otherwise the tests will report are successful evne on failure.
+            # echo $POSTFIX >> $SCRIPT
+            popd
+          done
+
+          # Run all the tests in their own shell.
+          $RUNNER
diff --git a/LICENSE b/LICENSE
index bf89209d..e21d6995 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright 2020-2021 wazero authors
+   Copyright 2020-2023 wazero authors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/NOTICE b/NOTICE
index 273990ee..2f5ea8eb 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,2 +1,2 @@
 wazero
-Copyright 2020-2021 wazero authors
+Copyright 2020-2023 wazero authors
diff --git a/api/wasm.go b/api/wasm.go
index db66f9b9..ffab8928 100644
--- a/api/wasm.go
+++ b/api/wasm.go
@@ -5,6 +5,8 @@ import (
 	"context"
 	"fmt"
 	"math"
+
+	"github.com/tetratelabs/wazero/internal/internalapi"
 )
 
 // ExternType classifies imports and exports with their respective types.
@@ -187,6 +189,8 @@ type Module interface {
 
 	// Closer closes this module by delegating to CloseWithExitCode with an exit code of zero.
 	Closer
+
+	internalapi.WazeroOnly
 }
 
 // Closer closes a resource.
@@ -236,6 +240,8 @@ type ExportDefinition interface {
 	// Note: The empty name is allowed in the WebAssembly Core Specification,
 	// so "" is possible.
 	ExportNames() []string
+
+	internalapi.WazeroOnly
 }
 
 // MemoryDefinition is a WebAssembly memory exported in a module
@@ -256,6 +262,8 @@ type MemoryDefinition interface {
 	// Max returns the possibly zero max count of 64KB pages, or false if
 	// unbounded.
 	Max() (uint32, bool)
+
+	internalapi.WazeroOnly
 }
 
 // FunctionDefinition is a WebAssembly function exported in a module
@@ -318,6 +326,8 @@ type FunctionDefinition interface {
 	// ResultNames are index-correlated with ResultTypes or nil if not
 	// available for one or more results.
 	ResultNames() []string
+
+	internalapi.WazeroOnly
 }
 
 // Function is a WebAssembly function exported from an instantiated module
@@ -355,6 +365,8 @@ type Function interface {
 	// WithCloseOnContextDone on wazero.RuntimeConfig for detail. See examples in context_done_example_test.go for
 	// the end-to-end demonstrations of how these terminations can be performed.
 	Call(ctx context.Context, params ...uint64) ([]uint64, error)
+
+	internalapi.WazeroOnly
 }
 
 // GoModuleFunction is a Function implemented in Go instead of a wasm binary.
@@ -482,6 +494,8 @@ type MutableGlobal interface {
 	//
 	// See Global.Type for how to encode this value from a Go type.
 	Set(v uint64)
+
+	internalapi.WazeroOnly
 }
 
 // Memory allows restricted access to a module's memory. Notably, this does not allow growing.
@@ -607,6 +621,8 @@ type Memory interface {
 
 	// WriteString writes the string to the underlying buffer at the offset or returns false if out of range.
 	WriteString(offset uint32, v string) bool
+
+	internalapi.WazeroOnly
 }
 
 // CustomSection contains the name and raw data of a custom section.
@@ -620,6 +636,8 @@ type CustomSection interface {
 	Name() string
 	// Data is the raw data of the custom section
 	Data() []byte
+
+	internalapi.WazeroOnly
 }
 
 // EncodeExternref encodes the input as a ValueTypeExternref.
diff --git a/config.go b/config.go
index 3a9c8df8..5172967f 100644
--- a/config.go
+++ b/config.go
@@ -13,6 +13,7 @@ import (
 	"github.com/tetratelabs/wazero/internal/engine/compiler"
 	"github.com/tetratelabs/wazero/internal/engine/interpreter"
 	"github.com/tetratelabs/wazero/internal/filecache"
+	"github.com/tetratelabs/wazero/internal/internalapi"
 	"github.com/tetratelabs/wazero/internal/platform"
 	internalsys "github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/sysfs"
@@ -402,6 +403,7 @@ func (c *compiledModule) CustomSections() []api.CustomSection {
 
 // customSection implements wasm.CustomSection
 type customSection struct {
+	internalapi.WazeroOnlyType
 	name string
 	data []byte
 }
diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go
index f7fbe8f8..1745582c 100644
--- a/internal/engine/compiler/engine.go
+++ b/internal/engine/compiler/engine.go
@@ -14,6 +14,7 @@ import (
 	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/internal/asm"
 	"github.com/tetratelabs/wazero/internal/filecache"
+	"github.com/tetratelabs/wazero/internal/internalapi"
 	"github.com/tetratelabs/wazero/internal/platform"
 	"github.com/tetratelabs/wazero/internal/version"
 	"github.com/tetratelabs/wazero/internal/wasm"
@@ -53,6 +54,8 @@ type (
 	//
 	// This implements api.Function.
 	callEngine struct {
+		internalapi.WazeroOnlyType
+
 		// See note at top of file before modifying this struct.
 
 		// These contexts are read and written by compiled code.
diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go
index 9b2ada5b..dbe8655c 100644
--- a/internal/engine/interpreter/interpreter.go
+++ b/internal/engine/interpreter/interpreter.go
@@ -13,6 +13,7 @@ import (
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/internal/filecache"
+	"github.com/tetratelabs/wazero/internal/internalapi"
 	"github.com/tetratelabs/wazero/internal/moremath"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wasmdebug"
@@ -91,6 +92,8 @@ type moduleEngine struct {
 //
 // This implements api.Function.
 type callEngine struct {
+	internalapi.WazeroOnlyType
+
 	// stack contains the operands.
 	// Note that all the values are represented as uint64.
 	stack []uint64
diff --git a/internal/internalapi/internal.go b/internal/internalapi/internal.go
new file mode 100644
index 00000000..a4f35435
--- /dev/null
+++ b/internal/internalapi/internal.go
@@ -0,0 +1,9 @@
+package internalapi
+
+type WazeroOnly interface {
+	wazeroOnly()
+}
+
+type WazeroOnlyType struct{}
+
+func (WazeroOnlyType) wazeroOnly() {}
diff --git a/internal/platform/select_windows.go b/internal/platform/select_windows.go
index 164ed17f..55f9751d 100644
--- a/internal/platform/select_windows.go
+++ b/internal/platform/select_windows.go
@@ -1,21 +1,37 @@
 package platform
 
 import (
+	"context"
 	"syscall"
 	"time"
+	"unsafe"
 )
 
 // wasiFdStdin is the constant value for stdin on Wasi.
 // We need this constant because on Windows os.Stdin.Fd() != 0.
 const wasiFdStdin = 0
 
+// pollInterval is the interval between each calls to peekNamedPipe in pollNamedPipe
+const pollInterval = 100 * time.Millisecond
+
+// procPeekNamedPipe is the syscall.LazyProc in kernel32 for PeekNamedPipe
+var procPeekNamedPipe = kernel32.NewProc("PeekNamedPipe")
+
 // syscall_select emulates the select syscall on Windows for two, well-known cases, returns syscall.ENOSYS for all others.
-// If r contains fd 0, then it immediately returns 1 (data ready on stdin) and r will have the fd 0 bit set.
+// If r contains fd 0, and it is a regular file, then it immediately returns 1 (data ready on stdin)
+// and r will have the fd 0 bit set.
+// If r contains fd 0, and it is a FILE_TYPE_CHAR, then it invokes PeekNamedPipe to check the buffer for input;
+// if there is data ready, then it returns 1 and r will have fd 0 bit set.
 // If n==0 it will wait for the given timeout duration, but it will return syscall.ENOSYS if timeout is nil,
 // i.e. it won't block indefinitely.
+//
+// Note: idea taken from https://stackoverflow.com/questions/6839508/test-if-stdin-has-input-for-c-windows-and-or-linux
+// PeekNamedPipe: https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
+// "GetFileType can assist in determining what device type the handle refers to. A console handle presents as FILE_TYPE_CHAR."
+// https://learn.microsoft.com/en-us/windows/console/console-handles
 func syscall_select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error) {
 	if n == 0 {
-		// don't block indefinitely
+		// Don't block indefinitely.
 		if timeout == nil {
 			return -1, syscall.ENOSYS
 		}
@@ -23,9 +39,85 @@ func syscall_select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error)
 		return 0, nil
 	}
 	if r.IsSet(wasiFdStdin) {
+		fileType, err := syscall.GetFileType(syscall.Stdin)
+		if err != nil {
+			return 0, err
+		}
+		if fileType&syscall.FILE_TYPE_CHAR != 0 {
+			res, err := pollNamedPipe(context.TODO(), syscall.Stdin, timeout)
+			if err != nil {
+				return -1, err
+			}
+			if !res {
+				r.Zero()
+				return 0, nil
+			}
+		}
 		r.Zero()
 		r.Set(wasiFdStdin)
 		return 1, nil
 	}
 	return -1, syscall.ENOSYS
 }
+
+// pollNamedPipe polls the given named pipe handle for the given duration.
+//
+// The implementation actually polls every 100 milliseconds until it reaches the given duration.
+// The duration may be nil, in which case it will wait undefinely. The given ctx is
+// used to allow for cancellation. Currently used only in tests.
+func pollNamedPipe(ctx context.Context, pipeHandle syscall.Handle, duration *time.Duration) (bool, error) {
+	// Short circuit when the duration is zero.
+	if duration != nil && *duration == time.Duration(0) {
+		return peekNamedPipe(pipeHandle)
+	}
+
+	// Ticker that emits at every pollInterval.
+	tick := time.NewTicker(pollInterval)
+	tichCh := tick.C
+	defer tick.Stop()
+
+	// Timer that expires after the given duration.
+	// Initialize afterCh as nil: the select below will wait forever.
+	var afterCh <-chan time.Time
+	if duration != nil {
+		// If duration is not nil, instantiate the timer.
+		after := time.NewTimer(*duration)
+		defer after.Stop()
+		afterCh = after.C
+	}
+
+	for {
+		select {
+		case <-ctx.Done():
+			return false, nil
+		case <-afterCh:
+			return false, nil
+		case <-tichCh:
+			res, err := peekNamedPipe(pipeHandle)
+			if err != nil {
+				return false, err
+			}
+			if res {
+				return true, nil
+			}
+		}
+	}
+}
+
+// peekNamedPipe partially exposes PeekNamedPipe from the Win32 API
+// see https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
+func peekNamedPipe(handle syscall.Handle) (bool, error) {
+	var totalBytesAvail uint32
+	totalBytesPtr := uintptr(unsafe.Pointer(&totalBytesAvail))
+	_, _, err := procPeekNamedPipe.Call(
+		uintptr(handle), // [in]            HANDLE  hNamedPipe,
+		0,               // [out, optional] LPVOID  lpBuffer,
+		0,               // [in]            DWORD   nBufferSize,
+		0,               // [out, optional] LPDWORD lpBytesRead
+		totalBytesPtr,   // [out, optional] LPDWORD lpTotalBytesAvail,
+		0)               // [out, optional] LPDWORD lpBytesLeftThisMessage
+	if err == syscall.Errno(0) {
+		return totalBytesAvail > 0, nil
+	}
+	return totalBytesAvail > 0, err
+}
diff --git a/internal/wasm/function_definition.go b/internal/wasm/function_definition.go
index ba804c3f..24bb895b 100644
--- a/internal/wasm/function_definition.go
+++ b/internal/wasm/function_definition.go
@@ -2,6 +2,7 @@ package wasm
 
 import (
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/internalapi"
 	"github.com/tetratelabs/wazero/internal/wasmdebug"
 )
 
@@ -106,6 +107,7 @@ func (m *Module) BuildFunctionDefinitions() {
 
 // FunctionDefinition implements api.FunctionDefinition
 type FunctionDefinition struct {
+	internalapi.WazeroOnlyType
 	moduleName  string
 	index       Index
 	name        string
diff --git a/internal/wasm/global.go b/internal/wasm/global.go
index caddaaf0..2fef4e23 100644
--- a/internal/wasm/global.go
+++ b/internal/wasm/global.go
@@ -4,9 +4,11 @@ import (
 	"fmt"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/internalapi"
 )
 
 type mutableGlobal struct {
+	internalapi.WazeroOnlyType
 	g *GlobalInstance
 }
 
diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go
index 3ee69cad..c882b0f0 100644
--- a/internal/wasm/memory.go
+++ b/internal/wasm/memory.go
@@ -9,6 +9,7 @@ import (
 	"unsafe"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/internalapi"
 )
 
 const (
@@ -32,6 +33,8 @@ var _ api.Memory = &MemoryInstance{}
 // wasm.Store Memories index zero: `store.Memories[0]`
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0.
 type MemoryInstance struct {
+	internalapi.WazeroOnlyType
+
 	Buffer        []byte
 	Min, Cap, Max uint32
 	// mux is used to prevent overlapping calls to Grow.
diff --git a/internal/wasm/memory_definition.go b/internal/wasm/memory_definition.go
index bf0acfc1..03d6fd30 100644
--- a/internal/wasm/memory_definition.go
+++ b/internal/wasm/memory_definition.go
@@ -1,6 +1,9 @@
 package wasm
 
-import "github.com/tetratelabs/wazero/api"
+import (
+	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/internalapi"
+)
 
 // ImportedMemories implements the same method as documented on wazero.CompiledModule.
 func (m *Module) ImportedMemories() (ret []api.MemoryDefinition) {
@@ -81,6 +84,7 @@ func (m *Module) BuildMemoryDefinitions() {
 
 // MemoryDefinition implements api.MemoryDefinition
 type MemoryDefinition struct {
+	internalapi.WazeroOnlyType
 	moduleName  string
 	index       Index
 	importDesc  *[2]string
diff --git a/internal/wasm/module_instance.go b/internal/wasm/module_instance.go
index 18c7cc64..9f64878c 100644
--- a/internal/wasm/module_instance.go
+++ b/internal/wasm/module_instance.go
@@ -7,6 +7,7 @@ import (
 	"sync/atomic"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/internalapi"
 	"github.com/tetratelabs/wazero/sys"
 )
 
@@ -222,7 +223,7 @@ func (m *ModuleInstance) ExportedGlobal(name string) api.Global {
 	}
 	g := m.Globals[exp.Index]
 	if g.Type.Mutable {
-		return &mutableGlobal{g}
+		return &mutableGlobal{internalapi.WazeroOnlyType{}, g}
 	}
 	valType := g.Type.ValType
 	switch valType {
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index 9aac46fa..c2c1d63a 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -7,6 +7,7 @@ import (
 	"sync"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/internalapi"
 	"github.com/tetratelabs/wazero/internal/leb128"
 	internalsys "github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/sys"
@@ -67,6 +68,8 @@ type (
 	//
 	// This implements api.Module.
 	ModuleInstance struct {
+		internalapi.WazeroOnlyType
+
 		// Closed is used both to guard moduleEngine.CloseWithExitCode and to store the exit code.
 		//
 		// The update value is closedType + exitCode << 32. This ensures an exit code of zero isn't mistaken for never closed.
diff --git a/site/layouts/partials/footer.html b/site/layouts/partials/footer.html
index 191789ee..655397c4 100644
--- a/site/layouts/partials/footer.html
+++ b/site/layouts/partials/footer.html
@@ -1,6 +1,6 @@
 <footer class="footer">
     <div class="footer__inner">
-        <div class="copyright copyright--user">2022 © Tetrate.io, Inc.</div>
+        <div class="copyright copyright--user">2023 © Tetrate.io, Inc.</div>
     </div>
     <div class="footer__inner">
         <div class="copyright copyright--user">wazero is a registered trademark of Tetrate.io, Inc. in the United States and/or other countries</div>
diff --git a/site/static/install.sh b/site/static/install.sh
index 8f0ab909..461f6900 100644
--- a/site/static/install.sh
+++ b/site/static/install.sh
@@ -1,6 +1,6 @@
 #!/bin/sh
 
-# Copyright 2021 Tetrate
+# Copyright 2023 Tetrate
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -384,4 +384,4 @@ CHECKSUM=wazero_${VERSION}_checksums.txt
 CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
 
 
-execute
\ No newline at end of file
+execute


Test patch:
diff --git a/cmd/wazero/wazero_test.go b/cmd/wazero/wazero_test.go
index cde24da2..09d781ad 100644
--- a/cmd/wazero/wazero_test.go
+++ b/cmd/wazero/wazero_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/experimental/logging"
 	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
+	"github.com/tetratelabs/wazero/internal/internalapi"
 	"github.com/tetratelabs/wazero/internal/platform"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/version"
@@ -587,6 +588,7 @@ func TestRun_Errors(t *testing.T) {
 var _ api.FunctionDefinition = importer{}
 
 type importer struct {
+	internalapi.WazeroOnlyType
 	moduleName, name string
 }
 
@@ -616,27 +618,27 @@ func Test_detectImports(t *testing.T) {
 		{
 			message: "other imports",
 			imports: []api.FunctionDefinition{
-				importer{"env", "emscripten_notify_memory_growth"},
+				importer{internalapi.WazeroOnlyType{}, "env", "emscripten_notify_memory_growth"},
 			},
 		},
 		{
 			message: "wasi",
 			imports: []api.FunctionDefinition{
-				importer{wasi_snapshot_preview1.ModuleName, "fd_read"},
+				importer{internalapi.WazeroOnlyType{}, wasi_snapshot_preview1.ModuleName, "fd_read"},
 			},
 			mode: modeWasi,
 		},
 		{
 			message: "unstable_wasi",
 			imports: []api.FunctionDefinition{
-				importer{"wasi_unstable", "fd_read"},
+				importer{internalapi.WazeroOnlyType{}, "wasi_unstable", "fd_read"},
 			},
 			mode: modeWasiUnstable,
 		},
 		{
 			message: "GOARCH=wasm GOOS=js",
 			imports: []api.FunctionDefinition{
-				importer{"go", "syscall/js.valueCall"},
+				importer{internalapi.WazeroOnlyType{}, "go", "syscall/js.valueCall"},
 			},
 			mode: modeGo,
 		},
diff --git a/internal/integration_test/fuzzcases/fuzzcases_test.go b/internal/integration_test/fuzzcases/fuzzcases_test.go
index 9e9eed61..8aa2f90a 100644
--- a/internal/integration_test/fuzzcases/fuzzcases_test.go
+++ b/internal/integration_test/fuzzcases/fuzzcases_test.go
@@ -319,6 +319,7 @@ func Test733(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			if testing.Short() {
 				// Note: this case uses large memory space, so can be slow like 1 to 2 seconds even without -race.
+				// The reason is that this test requires roughly 2GB of in-Wasm memory.
 				t.SkipNow()
 			}
 			f := mod.ExportedFunction(name)
diff --git a/internal/platform/select_windows_test.go b/internal/platform/select_windows_test.go
new file mode 100644
index 00000000..561eb9cf
--- /dev/null
+++ b/internal/platform/select_windows_test.go
@@ -0,0 +1,194 @@
+package platform
+
+import (
+	"context"
+	"os"
+	"syscall"
+	"testing"
+	"time"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func TestSelect_Windows(t *testing.T) {
+	type result struct {
+		hasData bool
+		err     error
+	}
+
+	testCtx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	pollToChannel := func(readHandle syscall.Handle, duration *time.Duration, ch chan result) {
+		r := result{}
+		r.hasData, r.err = pollNamedPipe(testCtx, readHandle, duration)
+		ch <- r
+		close(ch)
+	}
+
+	t.Run("peekNamedPipe should report the correct state of incoming data in the pipe", func(t *testing.T) {
+		r, w, err := os.Pipe()
+		require.NoError(t, err)
+		rh := syscall.Handle(r.Fd())
+		wh := syscall.Handle(w.Fd())
+
+		// Ensure the pipe has data.
+		hasData, err := peekNamedPipe(rh)
+		require.NoError(t, err)
+		require.False(t, hasData)
+
+		// Write to the channel.
+		msg, err := syscall.ByteSliceFromString("test\n")
+		require.NoError(t, err)
+		_, err = syscall.Write(wh, msg)
+		require.NoError(t, err)
+
+		// Ensure the pipe has data.
+		hasData, err = peekNamedPipe(rh)
+		require.NoError(t, err)
+		require.True(t, hasData)
+	})
+
+	t.Run("pollNamedPipe should return immediately when duration is nil (no data)", func(t *testing.T) {
+		r, _, err := os.Pipe()
+		require.NoError(t, err)
+		rh := syscall.Handle(r.Fd())
+		d := time.Duration(0)
+		hasData, err := pollNamedPipe(testCtx, rh, &d)
+		require.NoError(t, err)
+		require.False(t, hasData)
+	})
+
+	t.Run("pollNamedPipe should return immediately when duration is nil (data)", func(t *testing.T) {
+		r, w, err := os.Pipe()
+		require.NoError(t, err)
+		rh := syscall.Handle(r.Fd())
+		wh := syscall.Handle(w.Fd())
+
+		// Write to the channel immediately.
+		msg, err := syscall.ByteSliceFromString("test\n")
+		require.NoError(t, err)
+		_, err = syscall.Write(wh, msg)
+		require.NoError(t, err)
+
+		// Verify that the write is reported.
+		d := time.Duration(0)
+		hasData, err := pollNamedPipe(testCtx, rh, &d)
+		require.NoError(t, err)
+		require.True(t, hasData)
+	})
+
+	t.Run("pollNamedPipe should wait forever when duration is nil", func(t *testing.T) {
+		r, _, err := os.Pipe()
+		require.NoError(t, err)
+		rh := syscall.Handle(r.Fd())
+
+		ch := make(chan result, 1)
+		go pollToChannel(rh, nil, ch)
+
+		// Wait a little, then ensure no writes occurred.
+		<-time.After(500 * time.Millisecond)
+		require.Equal(t, 0, len(ch))
+	})
+
+	t.Run("pollNamedPipe should wait forever when duration is nil", func(t *testing.T) {
+		r, w, err := os.Pipe()
+		require.NoError(t, err)
+		rh := syscall.Handle(r.Fd())
+		wh := syscall.Handle(w.Fd())
+
+		ch := make(chan result, 1)
+		go pollToChannel(rh, nil, ch)
+
+		// Wait a little, then ensure no writes occurred.
+		<-time.After(100 * time.Millisecond)
+		require.Equal(t, 0, len(ch))
+
+		// Write a message to the pipe.
+		msg, err := syscall.ByteSliceFromString("test\n")
+		require.NoError(t, err)
+		_, err = syscall.Write(wh, msg)
+		require.NoError(t, err)
+
+		// Ensure that the write occurs (panic after an arbitrary timeout).
+		select {
+		case <-time.After(500 * time.Millisecond):
+			panic("unreachable!")
+		case r := <-ch:
+			require.NoError(t, r.err)
+			require.True(t, r.hasData)
+		}
+	})
+
+	t.Run("pollNamedPipe should wait for the given duration", func(t *testing.T) {
+		r, w, err := os.Pipe()
+		require.NoError(t, err)
+		rh := syscall.Handle(r.Fd())
+		wh := syscall.Handle(w.Fd())
+
+		d := 500 * time.Millisecond
+		ch := make(chan result, 1)
+		go pollToChannel(rh, &d, ch)
+
+		// Wait a little, then ensure no writes occurred.
+		<-time.After(100 * time.Millisecond)
+		require.Equal(t, 0, len(ch))
+
+		// Write a message to the pipe.
+		msg, err := syscall.ByteSliceFromString("test\n")
+		require.NoError(t, err)
+		_, err = syscall.Write(wh, msg)
+		require.NoError(t, err)
+
+		// Ensure that the write occurs before the timer expires.
+		select {
+		case <-time.After(500 * time.Millisecond):
+			panic("no data!")
+		case r := <-ch:
+			require.NoError(t, r.err)
+			require.True(t, r.hasData)
+		}
+	})
+
+	t.Run("pollNamedPipe should timeout after the given duration", func(t *testing.T) {
+		r, _, err := os.Pipe()
+		require.NoError(t, err)
+		rh := syscall.Handle(r.Fd())
+
+		d := 200 * time.Millisecond
+		ch := make(chan result, 1)
+		go pollToChannel(rh, &d, ch)
+
+		// Wait a little, then ensure a message has been written to the channel.
+		<-time.After(300 * time.Millisecond)
+		require.Equal(t, 1, len(ch))
+
+		// Ensure that the timer has expired.
+		res := <-ch
+		require.NoError(t, res.err)
+		require.False(t, res.hasData)
+	})
+
+	t.Run("pollNamedPipe should return when a write occurs before the given duration", func(t *testing.T) {
+		r, w, err := os.Pipe()
+		require.NoError(t, err)
+		rh := syscall.Handle(r.Fd())
+		wh := syscall.Handle(w.Fd())
+
+		d := 600 * time.Millisecond
+		ch := make(chan result, 1)
+		go pollToChannel(rh, &d, ch)
+
+		<-time.After(300 * time.Millisecond)
+		require.Equal(t, 0, len(ch))
+
+		msg, err := syscall.ByteSliceFromString("test\n")
+		require.NoError(t, err)
+		_, err = syscall.Write(wh, msg)
+		require.NoError(t, err)
+
+		res := <-ch
+		require.NoError(t, res.err)
+		require.True(t, res.hasData)
+	})
+}
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index 0c9fe047..6d736b40 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/experimental"
+	"github.com/tetratelabs/wazero/internal/internalapi"
 	"github.com/tetratelabs/wazero/internal/leb128"
 	"github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/testing/hammer"
@@ -418,6 +419,7 @@ type mockModuleEngine struct {
 }
 
 type mockCallEngine struct {
+	internalapi.WazeroOnlyType
 	index         Index
 	callFailIndex int
 }