updated vendor

This commit is contained in:
2026-06-16 08:02:19 +02:00
parent 2f7f99d3f0
commit 77299d0c64
1283 changed files with 67302 additions and 208958 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ That module is maintained by the Go authors, and covers OSes that the syscall
package neglects.
After [heavy consideration](https://github.com/wazero/wazero/issues/2434) we
decided to it as a dependency.
decided to add it as a dependency.
Using was shown to improve the experience of using wazero on older,
or less common, OSes without increasing the maintenance work, or creating
+1 -1
View File
@@ -96,7 +96,7 @@ OpenBSD, DragonFly BSD, illumos and Solaris.
We also test cross compilation for many `GOOS` and `GOARCH` combinations.
* Interpreter
* Linux is tested on amd64 and arm64 (native) as well as riscv64 via emulation.
* Linux is tested on amd64, arm64 and riscv64.
* Windows, FreeBSD, NetBSD, OpenBSD, DragonFly BSD, illumos and Solaris are
tested only on amd64.
* macOS is tested only on arm64.
+14
View File
@@ -209,6 +209,20 @@ func featureName(f CoreFeatures) string {
case CoreFeatureSIMD:
// match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/simd/SIMD.md
return "simd"
// The cases below cover features defined in the experimental package
// (experimental.CoreFeaturesThreads, CoreFeaturesTailCall,
// experimental.CoreFeaturesExtendedConst, experimental.CoreFeaturesExceptionHandling).
// They cannot be imported here (circular dependency), so we match by value.
case CoreFeatureSIMD << 1: // experimental.CoreFeaturesThreads
return "threads"
case CoreFeatureSIMD << 2: // experimental.CoreFeaturesTailCall
return "tail-call"
case CoreFeatureSIMD << 3: // experimental.CoreFeaturesExtendedConst
return "extended-const"
case CoreFeatureSIMD << 4: // experimental.CoreFeaturesExceptionHandling
return "exception-handling"
case CoreFeatureSIMD << 5: // experimental.CoreFeaturesTypedFunctionReferences
return "typed-function-references"
}
return ""
}
+2 -2
View File
@@ -243,13 +243,13 @@ type hostFunctionBuilder struct {
// WithGoFunction implements HostFunctionBuilder.WithGoFunction
func (h *hostFunctionBuilder) WithGoFunction(fn api.GoFunction, params, results []api.ValueType) HostFunctionBuilder {
h.fn = &wasm.HostFunc{ParamTypes: params, ResultTypes: results, Code: wasm.Code{GoFunc: fn}}
h.fn = &wasm.HostFunc{ParamTypes: wasm.FromApiValueType(params), ResultTypes: wasm.FromApiValueType(results), Code: wasm.Code{GoFunc: fn}}
return h
}
// WithGoModuleFunction implements HostFunctionBuilder.WithGoModuleFunction
func (h *hostFunctionBuilder) WithGoModuleFunction(fn api.GoModuleFunction, params, results []api.ValueType) HostFunctionBuilder {
h.fn = &wasm.HostFunc{ParamTypes: params, ResultTypes: results, Code: wasm.Code{GoFunc: fn}}
h.fn = &wasm.HostFunc{ParamTypes: wasm.FromApiValueType(params), ResultTypes: wasm.FromApiValueType(results), Code: wasm.Code{GoFunc: fn}}
return h
}
+22 -1
View File
@@ -14,5 +14,26 @@ import "github.com/tetratelabs/wazero/api"
// without mmap, consider editing the binary to reduce the max size setting of memory.
const CoreFeaturesThreads = api.CoreFeatureSIMD << 1
// CoreFeaturesThreads enables tail call instructions ("tail-call").
// CoreFeaturesTailCall enables tail call instructions ("tail-call").
const CoreFeaturesTailCall = api.CoreFeatureSIMD << 2
// CoreFeaturesExtendedConst enables extended constant expressions.
//
// # Notes
//
// - Enables i32.add/sub/mul and i64.add/sub/mul in constant expressions.
// - Enables references to any previous global index in constant expressions,
// instead of just imported globals.
//
// See https://github.com/WebAssembly/extended-const for further details.
const CoreFeaturesExtendedConst = api.CoreFeatureSIMD << 3
// CoreFeaturesExceptionHandling enables exception handling instructions.
//
// See https://github.com/WebAssembly/exception-handling for further details.
const CoreFeaturesExceptionHandling = api.CoreFeatureSIMD << 4
// CoreFeaturesTypedFunctionReferences enables typed function references.
//
// See https://github.com/WebAssembly/function-references for further details.
const CoreFeaturesTypedFunctionReferences = api.CoreFeatureSIMD << 5
+40
View File
@@ -314,3 +314,43 @@ type File interface {
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html
Close() Errno
}
// PollableFile is a File that additionally supports polling for readiness
// and non-blocking mode. This is separated from File to avoid breaking
// downstream implementations that do not need polling.
//
// Implementations should embed UnimplementedFile for forward compatibility
// of the base File methods, and add Poll, IsNonblock, and SetNonblock.
//
// # Notes
//
// - This is the public equivalent of the internal fsapi.File interface.
// - See Pollable for a standalone poll interface usable with io.Reader
// or fs.File implementations that are not full File implementations.
type PollableFile interface {
File
Pollable
// IsNonblock returns true if the file was opened with O_NONBLOCK, or
// SetNonblock was successfully enabled on this file.
//
// # Notes
//
// - This might not match the underlying state of the file descriptor if
// the file was not opened via OpenFile.
IsNonblock() bool
// SetNonblock toggles the non-blocking mode (O_NONBLOCK) of this file.
//
// # Errors
//
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed.
//
// # Notes
//
// - This is like syscall.SetNonblock and `fcntl` with O_NONBLOCK in
// POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html
SetNonblock(enable bool) Errno
}
+48
View File
@@ -0,0 +1,48 @@
package sys
// Pflag are bit flags used for polling. Values, including zero, should not
// be interpreted numerically. Instead, use by constants prefixed with 'POLL'.
//
// # Notes
//
// - This is like `pollfd.events` flags for `poll` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/poll.h.html
type Pflag uint32
// Only define bitflags we support and are needed by `poll_oneoff` in wasip1
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#eventrwflags
const (
// POLLIN is a read event.
POLLIN Pflag = 1 << iota
// POLLOUT is a write event.
POLLOUT
)
// Pollable is implemented by custom readers that support polling for
// readiness. If a custom io.Reader passed to WithStdin implements this
// interface, poll_oneoff will use it for asynchronous I/O instead of
// returning "always ready".
//
// # Parameters
//
// The `flag` parameter determines which event to await, such as POLLIN,
// POLLOUT, or a combination like `POLLIN|POLLOUT`.
//
// The `timeoutMillis` parameter is how long to block for an event, or
// interrupted, in milliseconds. There are two special values:
// - zero returns immediately
// - any negative value blocks any amount of time
//
// # Results
//
// `ready` means there was data ready to read or written. False can mean no
// event was ready or `errno` is not zero.
//
// A zero `errno` is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - ENOTSUP: the implementation does not support the flag combination.
// - EINTR: the call was interrupted prior to an event.
type Pollable interface {
Poll(flag Pflag, timeoutMillis int32) (ready bool, errno Errno)
}
@@ -41,7 +41,7 @@ import (
// See argsSizesGet
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_get
// See https://en.wikipedia.org/wiki/Null-terminated_string
var argsGet = newHostFunc(wasip1.ArgsGetName, argsGetFn, []api.ValueType{i32, i32}, "argv", "argv_buf")
var argsGet = newHostFunc(wasip1.ArgsGetName, argsGetFn, []wasm.ValueType{i32, i32}, "argv", "argv_buf")
func argsGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -78,7 +78,7 @@ func argsGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
// See argsGet
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_sizes_get
// See https://en.wikipedia.org/wiki/Null-terminated_string
var argsSizesGet = newHostFunc(wasip1.ArgsSizesGetName, argsSizesGetFn, []api.ValueType{i32, i32}, "result.argc", "result.argv_len")
var argsSizesGet = newHostFunc(wasip1.ArgsSizesGetName, argsSizesGetFn, []wasm.ValueType{i32, i32}, "result.argc", "result.argv_len")
func argsSizesGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -37,7 +37,7 @@ import (
// Note: This is similar to `clock_getres` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_res_getid-clockid---errno-timestamp
// See https://linux.die.net/man/3/clock_getres
var clockResGet = newHostFunc(wasip1.ClockResGetName, clockResGetFn, []api.ValueType{i32, i32}, "id", "result.resolution")
var clockResGet = newHostFunc(wasip1.ClockResGetName, clockResGetFn, []wasm.ValueType{i32, i32}, "id", "result.resolution")
func clockResGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -90,7 +90,7 @@ func clockResGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno
// Note: This is similar to `clock_gettime` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_time_getid-clockid-precision-timestamp---errno-timestamp
// See https://linux.die.net/man/3/clock_gettime
var clockTimeGet = newHostFunc(wasip1.ClockTimeGetName, clockTimeGetFn, []api.ValueType{i32, i64, i32}, "id", "precision", "result.timestamp")
var clockTimeGet = newHostFunc(wasip1.ClockTimeGetName, clockTimeGetFn, []wasm.ValueType{i32, i64, i32}, "id", "precision", "result.timestamp")
func clockTimeGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -41,7 +41,7 @@ import (
// See environSizesGet
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_get
// See https://en.wikipedia.org/wiki/Null-terminated_string
var environGet = newHostFunc(wasip1.EnvironGetName, environGetFn, []api.ValueType{i32, i32}, "environ", "environ_buf")
var environGet = newHostFunc(wasip1.EnvironGetName, environGetFn, []wasm.ValueType{i32, i32}, "environ", "environ_buf")
func environGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -81,7 +81,7 @@ func environGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno
// See environGet
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_sizes_get
// and https://en.wikipedia.org/wiki/Null-terminated_string
var environSizesGet = newHostFunc(wasip1.EnvironSizesGetName, environSizesGetFn, []api.ValueType{i32, i32}, "result.environc", "result.environv_len")
var environSizesGet = newHostFunc(wasip1.EnvironSizesGetName, environSizesGetFn, []wasm.ValueType{i32, i32}, "result.environc", "result.environv_len")
func environSizesGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
+25 -20
View File
@@ -115,7 +115,7 @@ func fdAllocateFn(_ context.Context, mod api.Module, params []uint64) experiment
// Note: This is similar to `close` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_close
// and https://linux.die.net/man/3/close
var fdClose = newHostFunc(wasip1.FdCloseName, fdCloseFn, []api.ValueType{i32}, "fd")
var fdClose = newHostFunc(wasip1.FdCloseName, fdCloseFn, []wasm.ValueType{i32}, "fd")
func fdCloseFn(_ context.Context, mod api.Module, params []uint64) experimentalsys.Errno {
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -128,7 +128,7 @@ func fdCloseFn(_ context.Context, mod api.Module, params []uint64) experimentals
// the data of a file to disk.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_datasyncfd-fd---errno
var fdDatasync = newHostFunc(wasip1.FdDatasyncName, fdDatasyncFn, []api.ValueType{i32}, "fd")
var fdDatasync = newHostFunc(wasip1.FdDatasyncName, fdDatasyncFn, []wasm.ValueType{i32}, "fd")
func fdDatasyncFn(_ context.Context, mod api.Module, params []uint64) experimentalsys.Errno {
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -179,7 +179,7 @@ func fdDatasyncFn(_ context.Context, mod api.Module, params []uint64) experiment
// well as additional fields.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fdstat
// and https://linux.die.net/man/3/fsync
var fdFdstatGet = newHostFunc(wasip1.FdFdstatGetName, fdFdstatGetFn, []api.ValueType{i32, i32}, "fd", "result.stat")
var fdFdstatGet = newHostFunc(wasip1.FdFdstatGetName, fdFdstatGetFn, []wasm.ValueType{i32, i32}, "fd", "result.stat")
// fdFdstatGetFn cannot currently use proxyResultParams because fdstat is larger
// than api.ValueTypeI64 (i64 == 8 bytes, but fdstat is 24).
@@ -206,7 +206,7 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) experimen
fdflags |= wasip1.FD_APPEND
}
if f.File.IsNonblock() {
if isNonblock(f.File) {
fdflags |= wasip1.FD_NONBLOCK
}
@@ -311,9 +311,14 @@ func fdFdstatSetFlagsFn(_ context.Context, mod api.Module, params []uint64) expe
return experimentalsys.EBADF
} else {
nonblock := wasip1.FD_NONBLOCK&wasiFlag != 0
errno := f.File.SetNonblock(nonblock)
if errno != 0 {
return errno
if pf, ok := f.File.(experimentalsys.PollableFile); ok {
if errno := pf.SetNonblock(nonblock); errno != 0 {
return errno
}
} else if isDir, errno := f.File.IsDir(); errno == 0 && isDir {
return experimentalsys.EISDIR
} else if nonblock {
return experimentalsys.ENOSYS
}
if stat, err := f.File.Stat(); err == 0 && stat.Mode.IsRegular() {
// For normal files, proceed to apply an append flag.
@@ -381,7 +386,7 @@ var fdFdstatSetRights = stubFunction(
// Note: This is similar to `fstat` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_getfd-fd---errno-filestat
// and https://linux.die.net/man/3/fstat
var fdFilestatGet = newHostFunc(wasip1.FdFilestatGetName, fdFilestatGetFn, []api.ValueType{i32, i32}, "fd", "result.filestat")
var fdFilestatGet = newHostFunc(wasip1.FdFilestatGetName, fdFilestatGetFn, []wasm.ValueType{i32, i32}, "fd", "result.filestat")
// fdFilestatGetFn cannot currently use proxyResultParams because filestat is
// larger than api.ValueTypeI64 (i64 == 8 bytes, but filestat is 64).
@@ -559,7 +564,7 @@ func toTimes(walltime func() int64, atim, mtim int64, fstFlags uint16) (int64, i
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_preadfd-fd-iovs-iovec_array-offset-filesize---errno-size
var fdPread = newHostFunc(
wasip1.FdPreadName, fdPreadFn,
[]api.ValueType{i32, i32, i32, i64, i32},
[]wasm.ValueType{i32, i32, i32, i64, i32},
"fd", "iovs", "iovs_len", "offset", "result.nread",
)
@@ -598,7 +603,7 @@ func fdPreadFn(_ context.Context, mod api.Module, params []uint64) experimentals
//
// See fdPrestatDirName and
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#prestat
var fdPrestatGet = newHostFunc(wasip1.FdPrestatGetName, fdPrestatGetFn, []api.ValueType{i32, i32}, "fd", "result.prestat")
var fdPrestatGet = newHostFunc(wasip1.FdPrestatGetName, fdPrestatGetFn, []wasm.ValueType{i32, i32}, "fd", "result.prestat")
func fdPrestatGetFn(_ context.Context, mod api.Module, params []uint64) experimentalsys.Errno {
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -611,7 +616,7 @@ func fdPrestatGetFn(_ context.Context, mod api.Module, params []uint64) experime
// Upper 32-bits are zero because...
// * Zero-value 8-bit tag, and 3-byte zero-value padding
prestat := uint64(len(name) << 32)
prestat := uint64(len(name)) << 32
if !mod.Memory().WriteUint64Le(resultPrestat, prestat) {
return experimentalsys.EFAULT
}
@@ -650,7 +655,7 @@ func fdPrestatGetFn(_ context.Context, mod api.Module, params []uint64) experime
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name
var fdPrestatDirName = newHostFunc(
wasip1.FdPrestatDirNameName, fdPrestatDirNameFn,
[]api.ValueType{i32, i32, i32},
[]wasm.ValueType{i32, i32, i32},
"fd", "result.path", "result.path_len",
)
@@ -682,7 +687,7 @@ func fdPrestatDirNameFn(_ context.Context, mod api.Module, params []uint64) expe
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_pwritefd-fd-iovs-ciovec_array-offset-filesize---errno-size
var fdPwrite = newHostFunc(
wasip1.FdPwriteName, fdPwriteFn,
[]api.ValueType{i32, i32, i32, i64, i32},
[]wasm.ValueType{i32, i32, i32, i64, i32},
"fd", "iovs", "iovs_len", "offset", "result.nwritten",
)
@@ -741,7 +746,7 @@ func fdPwriteFn(_ context.Context, mod api.Module, params []uint64) experimental
// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_read
var fdRead = newHostFunc(
wasip1.FdReadName, fdReadFn,
[]api.ValueType{i32, i32, i32, i32},
[]wasm.ValueType{i32, i32, i32, i32},
"fd", "iovs", "iovs_len", "result.nread",
)
@@ -1108,7 +1113,7 @@ func fdRenumberFn(_ context.Context, mod api.Module, params []uint64) experiment
// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_seek
var fdSeek = newHostFunc(
wasip1.FdSeekName, fdSeekFn,
[]api.ValueType{i32, i64, i32, i32},
[]wasm.ValueType{i32, i64, i32, i32},
"fd", "offset", "whence", "result.newoffset",
)
@@ -1135,7 +1140,7 @@ func fdSeekFn(_ context.Context, mod api.Module, params []uint64) experimentalsy
// and metadata of a file to disk.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_syncfd-fd---errno
var fdSync = newHostFunc(wasip1.FdSyncName, fdSyncFn, []api.ValueType{i32}, "fd")
var fdSync = newHostFunc(wasip1.FdSyncName, fdSyncFn, []wasm.ValueType{i32}, "fd")
func fdSyncFn(_ context.Context, mod api.Module, params []uint64) experimentalsys.Errno {
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -1153,7 +1158,7 @@ func fdSyncFn(_ context.Context, mod api.Module, params []uint64) experimentalsy
// offset of a file descriptor.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_tellfd-fd---errno-filesize
var fdTell = newHostFunc(wasip1.FdTellName, fdTellFn, []api.ValueType{i32, i32}, "fd", "result.offset")
var fdTell = newHostFunc(wasip1.FdTellName, fdTellFn, []wasm.ValueType{i32, i32}, "fd", "result.offset")
func fdTellFn(ctx context.Context, mod api.Module, params []uint64) experimentalsys.Errno {
fd := params[0]
@@ -1225,7 +1230,7 @@ func fdTellFn(ctx context.Context, mod api.Module, params []uint64) experimental
// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write
var fdWrite = newHostFunc(
wasip1.FdWriteName, fdWriteFn,
[]api.ValueType{i32, i32, i32, i32},
[]wasm.ValueType{i32, i32, i32, i32},
"fd", "iovs", "iovs_len", "result.nwritten",
)
@@ -1385,7 +1390,7 @@ func pathCreateDirectoryFn(_ context.Context, mod api.Module, params []uint64) e
// and https://linux.die.net/man/2/fstatat
var pathFilestatGet = newHostFunc(
wasip1.PathFilestatGetName, pathFilestatGetFn,
[]api.ValueType{i32, i32, i32, i32, i32},
[]wasm.ValueType{i32, i32, i32, i32, i32},
"fd", "flags", "path", "path_len", "result.filestat",
)
@@ -1566,7 +1571,7 @@ func pathLinkFn(_ context.Context, mod api.Module, params []uint64) experimental
// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#path_open
var pathOpen = newHostFunc(
wasip1.PathOpenName, pathOpenFn,
[]api.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32},
[]wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32},
"fd", "dirflags", "path", "path_len", "oflags", "fs_rights_base", "fs_rights_inheriting", "fdflags", "result.opened_fd",
)
+23 -13
View File
@@ -6,7 +6,6 @@ import (
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/wasip1"
"github.com/tetratelabs/wazero/internal/wasm"
@@ -39,7 +38,7 @@ import (
// See https://linux.die.net/man/3/poll
var pollOneoff = newHostFunc(
wasip1.PollOneoffName, pollOneoffFn,
[]api.ValueType{i32, i32, i32, i32},
[]wasm.ValueType{i32, i32, i32, i32},
"in", "out", "nsubscriptions", "result.nevents",
)
@@ -132,7 +131,7 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) sys.Errno
evt.errno = wasip1.ErrnoBadf
writeEvent(outBuf[outOffset:], evt)
nevents++
} else if fd != internalsys.FdStdin && file.File.IsNonblock() {
} else if fd != internalsys.FdStdin && isNonblock(file.File) {
writeEvent(outBuf[outOffset:], evt)
nevents++
} else {
@@ -174,17 +173,19 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) sys.Errno
if !ok {
return sys.EBADF
}
// Wait for the timeout to expire, or for some data to become available on Stdin.
if stdinReady, errno := stdin.File.Poll(fsapi.POLLIN, int32(timeout.Milliseconds())); errno != 0 {
return errno
} else if stdinReady {
// stdin has data ready to for reading, write back all the events
for i := range blockingStdinSubs {
evt := blockingStdinSubs[i]
evt.errno = 0
writeEvent(outBuf[nevents*32:], evt)
nevents++
// Wait for the timeout to expire, or for some data to become available on Stdin.
if p, ok := stdin.File.(sys.Pollable); ok {
if stdinReady, errno := p.Poll(sys.POLLIN, int32(timeout.Milliseconds())); errno != 0 {
return errno
} else if stdinReady {
// stdin has data ready to for reading, write back all the events
for i := range blockingStdinSubs {
evt := blockingStdinSubs[i]
evt.errno = 0
writeEvent(outBuf[nevents*32:], evt)
nevents++
}
}
}
@@ -226,6 +227,15 @@ func processClockEvent(inBuf []byte) (time.Duration, sys.Errno) {
}
}
// isNonblock returns true if the file implements PollableFile and is in
// non-blocking mode.
func isNonblock(f sys.File) bool {
if pf, ok := f.(sys.PollableFile); ok {
return pf.IsNonblock()
}
return false
}
// writeEvent writes the event corresponding to the processed subscription.
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-event-struct
func writeEvent(outBuf []byte, evt *event) {
@@ -21,7 +21,7 @@ import (
var procExit = &wasm.HostFunc{
ExportName: wasip1.ProcExitName,
Name: wasip1.ProcExitName,
ParamTypes: []api.ValueType{i32},
ParamTypes: []wasm.ValueType{i32},
ParamNames: []string{"rval"},
Code: wasm.Code{GoFunc: api.GoModuleFunc(procExitFn)},
}
@@ -41,4 +41,4 @@ func procExitFn(ctx context.Context, mod api.Module, params []uint64) {
// procRaise is stubbed and will never be supported, as it was removed.
//
// See https://github.com/WebAssembly/WASI/pull/136
var procRaise = stubFunction(wasip1.ProcRaiseName, []api.ValueType{i32}, "sig")
var procRaise = stubFunction(wasip1.ProcRaiseName, []wasm.ValueType{i32}, "sig")
@@ -34,7 +34,7 @@ import (
// buf --^
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-bufLen-size---errno
var randomGet = newHostFunc(wasip1.RandomGetName, randomGetFn, []api.ValueType{i32, i32}, "buf", "buf_len")
var randomGet = newHostFunc(wasip1.RandomGetName, randomGetFn, []wasm.ValueType{i32, i32}, "buf", "buf_len")
func randomGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -269,7 +269,7 @@ func writeOffsetsAndNullTerminatedValues(mem api.Memory, values [][]byte, offset
func newHostFunc(
name string,
goFunc wasiFunc,
paramTypes []api.ValueType,
paramTypes []wasm.ValueType,
paramNames ...string,
) *wasm.HostFunc {
return &wasm.HostFunc{
@@ -277,7 +277,7 @@ func newHostFunc(
Name: name,
ParamTypes: paramTypes,
ParamNames: paramNames,
ResultTypes: []api.ValueType{i32},
ResultTypes: []wasm.ValueType{i32},
ResultNames: []string{"errno"},
Code: wasm.Code{GoFunc: goFunc},
}
@@ -305,7 +305,7 @@ func stubFunction(name string, paramTypes []wasm.ValueType, paramNames ...string
Name: name,
ParamTypes: paramTypes,
ParamNames: paramNames,
ResultTypes: []api.ValueType{i32},
ResultTypes: []wasm.ValueType{i32},
ResultNames: []string{"errno"},
Code: wasm.Code{
GoFunc: api.GoModuleFunc(func(_ context.Context, _ api.Module, stack []uint64) { stack[0] = uint64(wasip1.ErrnoNosys) }),
+215 -6
View File
@@ -21,6 +21,7 @@ const (
controlFrameKindLoop
controlFrameKindIfWithElse
controlFrameKindIfWithoutElse
controlFrameKindTryTable
)
type (
@@ -57,7 +58,8 @@ func (c *controlFrame) asLabel() label {
case controlFrameKindFunction:
return newLabel(labelKindReturn, 0)
case controlFrameKindIfWithElse,
controlFrameKindIfWithoutElse:
controlFrameKindIfWithoutElse,
controlFrameKindTryTable:
return newLabel(labelKindContinuation, c.frameID)
}
panic(fmt.Sprintf("unreachable: a bug in interpreterir implementation: %v", c.kind))
@@ -187,6 +189,8 @@ type compiler struct {
funcs []uint32
// globals holds the global types for all declared globals in the module where the target function exists.
globals []wasm.GlobalType
// tags holds the type indexes for all declared tags in the module where the target function exists.
tags []uint32
// needSourceOffset is true if this module requires DWARF based stack trace.
needSourceOffset bool
@@ -254,6 +258,9 @@ type compilationResult struct {
LabelCallers map[label]uint32
// UsesMemory is true if this function might use memory.
UsesMemory bool
// PendingExceptionTable holds unresolved exception table entries, built during
// compilation. Labels are resolved to final PCs in lowerIR.
PendingExceptionTable []pendingExceptionTableEntry
// The following fields are per-module values, not per-function.
@@ -276,7 +283,7 @@ type compilationResult struct {
// newCompiler returns the new *compiler for the given parameters.
// Use compiler.Next function to get compilation result per function.
func newCompiler(enabledFeatures api.CoreFeatures, callFrameStackSizeInUint64 int, module *wasm.Module, ensureTermination bool) (*compiler, error) {
functions, globals, mem, tables, err := module.AllDeclarations()
functions, globals, mem, tables, tags, err := module.AllDeclarations()
if err != nil {
return nil, err
}
@@ -313,12 +320,14 @@ func newCompiler(enabledFeatures api.CoreFeatures, callFrameStackSizeInUint64 in
},
globals: globals,
funcs: functions,
tags: tags,
types: types,
ensureTermination: ensureTermination,
br: bytes.NewReader(nil),
funcTypeToSigs: funcTypeToIRSignatures{
indirectCalls: make([]*signature, len(types)),
directCalls: make([]*signature, len(types)),
callRefCalls: make([]*signature, len(types)),
wasmTypes: types,
},
needSourceOffset: module.DWARFLines != nil,
@@ -336,6 +345,7 @@ func (c *compiler) Next() (*compilationResult, error) {
c.result.Operations = c.result.Operations[:0]
c.result.IROperationSourceOffsetsInWasmBinary = c.result.IROperationSourceOffsetsInWasmBinary[:0]
c.result.UsesMemory = false
c.result.PendingExceptionTable = c.result.PendingExceptionTable[:0]
// Clears the existing entries in LabelCallers.
for frameID := uint32(0); frameID <= c.currentFrameID; frameID++ {
for k := labelKind(0); k < labelKindNum; k++ {
@@ -667,7 +677,8 @@ operatorSwitch:
// Initiate the continuation.
c.emit(newOperationLabel(continuationLabel))
case controlFrameKindBlockWithContinuationLabel,
controlFrameKindIfWithElse:
controlFrameKindIfWithElse,
controlFrameKindTryTable:
continuationLabel := newLabel(labelKindContinuation, frame.frameID)
c.result.LabelCallers[continuationLabel]++
c.emit(dropOp)
@@ -800,6 +811,95 @@ operatorSwitch:
// That means subsequent instructions in the current control frame are "unreachable"
// and can be safely removed.
c.markUnreachable()
case wasm.OpcodeThrow:
if c.unreachableState.on {
break operatorSwitch
}
// Pop the tag's param values from the stack.
if index < uint32(len(c.tags)) {
tagType := &c.types[c.tags[index]]
for i := len(tagType.Params) - 1; i >= 0; i-- {
c.stackPop()
}
}
c.emit(newOperationThrow(index))
c.markUnreachable()
case wasm.OpcodeThrowRef:
if c.unreachableState.on {
break operatorSwitch
}
// Pop the exnref from the stack.
c.stackPop()
c.emit(newOperationThrowRef())
c.markUnreachable()
case wasm.OpcodeTryTable:
c.br.Reset(c.body[c.pc+1:])
bt, num, err := wasm.DecodeBlockType(c.types, c.br, c.enabledFeatures)
if err != nil {
return fmt.Errorf("reading block type for try_table instruction: %w", err)
}
c.pc += num
if c.unreachableState.on {
c.unreachableState.depth++
// Still need to skip the catch clause bytes.
c.pc++
catchCount, catchNum, err := leb128.LoadUint32(c.body[c.pc:])
if err != nil {
return fmt.Errorf("reading catch count for try_table: %w", err)
}
c.pc += catchNum - 1
for i := uint32(0); i < catchCount; i++ {
if _, _, _, err := c.parseCatchClause(); err != nil {
return err
}
}
break operatorSwitch
}
// Read catch clause count.
c.pc++
catchCount, catchNum, err := leb128.LoadUint32(c.body[c.pc:])
if err != nil {
return fmt.Errorf("reading catch count for try_table: %w", err)
}
c.pc += catchNum - 1
// Parse catch clauses.
var pendingClauses []pendingCatchClause
for i := uint32(0); i < catchCount; i++ {
kind, tagIdx, labelIdx, err := c.parseCatchClause()
if err != nil {
return err
}
// Resolve the label from the control frame stack.
targetFrame := c.controlFrames.get(int(labelIdx))
targetFrame.ensureContinuation()
targetLabel := targetFrame.asLabel()
c.result.LabelCallers[targetLabel]++
pendingClauses = append(pendingClauses, pendingCatchClause{
kind: kind,
tagIndex: tagIdx,
targetLabel: targetLabel,
targetStackDepth: targetFrame.originalStackLenWithoutParamUint64,
})
}
// Create a control frame for the try_table block.
frameID := c.nextFrameID()
c.result.PendingExceptionTable = append(c.result.PendingExceptionTable, pendingExceptionTableEntry{
startOpIndex: len(c.result.Operations),
continuationFrameID: frameID,
clauses: pendingClauses,
})
frame := controlFrame{
frameID: frameID,
originalStackLenWithoutParam: len(c.stack) - len(bt.Params),
originalStackLenWithoutParamUint64: c.stackLenInUint64 - bt.ParamNumInUint64,
kind: controlFrameKindTryTable,
blockType: bt,
}
c.controlFrames.push(frame)
case wasm.OpcodeCall:
c.emit(
newOperationCall(index),
@@ -1616,7 +1716,18 @@ operatorSwitch:
newOperationRefFunc(index),
)
case wasm.OpcodeRefNull:
c.pc++ // Skip the type of reftype as every ref value is opaque pointer.
c.pc++
switch reftype := c.body[c.pc]; wasm.ValueType(reftype) {
case wasm.ValueTypeFuncref, wasm.ValueTypeExternref, wasm.ValueTypeExnref:
// Abstract ref types are a single byte; already skipped.
default:
// Concrete type index encoded as LEB128; skip it.
_, num, err := leb128.LoadUint32(c.body[c.pc:])
if err != nil {
return fmt.Errorf("failed to read type index for ref.null: %v", err)
}
c.pc += num - 1
}
c.emit(
newOperationConstI64(0),
)
@@ -3463,6 +3574,64 @@ operatorSwitch:
// and can be safely removed.
c.markUnreachable()
case wasm.OpcodeCallRef:
c.emit(newOperationCallRef(index))
case wasm.OpcodeReturnCallRef:
functionFrame := c.controlFrames.functionFrame()
dropRange := c.getFrameDropRange(functionFrame, false)
c.emit(newOperationReturnCallRef(index, dropRange, functionFrame.asLabel()))
c.markUnreachable()
case wasm.OpcodeRefAsNonNull:
c.emit(newOperationRefAsNonNull())
case wasm.OpcodeBrOnNull:
targetIndex, n, err := leb128.LoadUint32(c.body[c.pc+1:])
if err != nil {
return fmt.Errorf("read the target for br_on_null: %w", err)
}
c.pc += n
if c.unreachableState.on {
break operatorSwitch
}
targetFrame := c.controlFrames.get(int(targetIndex))
targetFrame.ensureContinuation()
drop := c.getFrameDropRange(targetFrame, false)
target := targetFrame.asLabel()
c.result.LabelCallers[target]++
continuationLabel := newLabel(labelKindHeader, c.nextFrameID())
c.result.LabelCallers[continuationLabel]++
c.emit(newOperationBrOnNull(target, continuationLabel, drop))
c.emit(newOperationLabel(continuationLabel))
// On fall-through (non-null), the ref is pushed back at runtime.
c.stackPush(unsignedTypeI64)
case wasm.OpcodeBrOnNonNull:
targetIndex, n, err := leb128.LoadUint32(c.body[c.pc+1:])
if err != nil {
return fmt.Errorf("read the target for br_on_non_null: %w", err)
}
c.pc += n
if c.unreachableState.on {
break operatorSwitch
}
targetFrame := c.controlFrames.get(int(targetIndex))
targetFrame.ensureContinuation()
drop := c.getFrameDropRange(targetFrame, false)
target := targetFrame.asLabel()
c.result.LabelCallers[target]++
continuationLabel := newLabel(labelKindHeader, c.nextFrameID())
c.result.LabelCallers[continuationLabel]++
c.emit(newOperationBrOnNonNull(target, continuationLabel, drop))
c.emit(newOperationLabel(continuationLabel))
default:
return fmt.Errorf("unsupported instruction in interpreterir: 0x%x", op)
}
@@ -3492,7 +3661,12 @@ func (c *compiler) applyToStack(opcode wasm.Opcode) (index uint32, err error) {
wasm.OpcodeGlobalSet,
// tail-call proposal
wasm.OpcodeTailCallReturnCall,
wasm.OpcodeTailCallReturnCallIndirect:
wasm.OpcodeTailCallReturnCallIndirect,
// exception handling - throw reads tag index
wasm.OpcodeThrow,
// typed function references
wasm.OpcodeCallRef,
wasm.OpcodeReturnCallRef:
// Assumes that we are at the opcode now so skip it before read immediates.
v, num, err := leb128.LoadUint32(c.body[c.pc+1:])
if err != nil {
@@ -3605,7 +3779,7 @@ func (c *compiler) emitDefaultValue(t wasm.ValueType) {
case wasm.ValueTypeI32:
c.stackPush(unsignedTypeI32)
c.emit(newOperationConstI32(0))
case wasm.ValueTypeI64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
case wasm.ValueTypeI64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeExnref:
c.stackPush(unsignedTypeI64)
c.emit(newOperationConstI64(0))
case wasm.ValueTypeF32:
@@ -3617,6 +3791,14 @@ func (c *compiler) emitDefaultValue(t wasm.ValueType) {
case wasm.ValueTypeV128:
c.stackPush(unsignedTypeV128)
c.emit(newOperationV128Const(0, 0))
default:
// Concrete ref types (ref $t) have variable bit patterns.
if t.IsRef() {
c.stackPush(unsignedTypeI64)
c.emit(newOperationConstI64(0))
} else {
panic(fmt.Sprintf("bug: unsupported value type for default value: 0x%x", t))
}
}
}
@@ -3673,3 +3855,30 @@ func (c *compiler) readMemoryArg(tag string) (memoryArg, error) {
c.pc += num
return memoryArg{Offset: offset, Alignment: alignment}, nil
}
// parseCatchClause parses a single catch clause from the bytecode at c.pc,
// advancing c.pc past the clause. Returns the kind, tag index (0 for catch_all
// variants), and label index.
func (c *compiler) parseCatchClause() (kind byte, tagIdx, labelIdx uint32, err error) {
var n uint64
c.pc++
kind = c.body[c.pc]
switch kind {
case wasm.CatchKindCatch, wasm.CatchKindCatchRef:
c.pc++
tagIdx, n, err = leb128.LoadUint32(c.body[c.pc:])
if err != nil {
err = fmt.Errorf("reading catch tag index: %w", err)
return
}
c.pc += n - 1
}
c.pc++
labelIdx, n, err = leb128.LoadUint32(c.body[c.pc:])
if err != nil {
err = fmt.Errorf("reading catch label index: %w", err)
return
}
c.pc += n - 1
return
}
@@ -130,6 +130,39 @@ func (e *moduleEngine) OwnsGlobals() bool { return false }
// MemoryGrown implements wasm.ModuleEngine.
func (e *moduleEngine) MemoryGrown() {}
// restorable is implemented by panic values that can restore callEngine state.
// Both *snapshot (snapshotter API) and *thrownException (exception handling)
// implement this interface.
type restorable interface {
// canRestore unwinds ce.frames to callerFrameCount and checks whether a
// handler exists at that depth. If no handler is found, the caller
// re-panics and the next outer callWithUnwind unwinds further.
canRestore(ce *callEngine, callerFrameCount int) bool
// doRestore restores the callEngine state to the given stack frame depth.
doRestore(ce *callEngine, callerFrameCount int)
}
// thrownException is the panic value for wasm exception propagation.
type thrownException struct {
exception *wasm.Exception
// Fields populated by canRestore for doRestore.
clause *exceptionTableCatchClause
values []uint64
}
func (t *thrownException) canRestore(ce *callEngine, callerFrameCount int) bool {
ce.frames = ce.frames[:callerFrameCount]
frame := ce.frames[callerFrameCount-1]
t.clause, t.values = searchExceptionTable(t.exception, frame)
return t.clause != nil
}
func (t *thrownException) doRestore(ce *callEngine, callerFrameCount int) {
frame := ce.frames[callerFrameCount-1]
ce.applyExceptionHandler(frame, t.clause, t.values)
t.clause, t.values = nil, nil
}
// callEngine holds context per moduleEngine.Call, and shared across all the
// function calls originating from the same moduleEngine.Call execution.
//
@@ -151,6 +184,94 @@ type callEngine struct {
stackIterator stackIterator
}
// matchCatchClause checks whether a single catch clause matches the given exception.
// Returns whether it matched and the values to push onto the stack.
func matchCatchClause(kind byte, clauseTag *wasm.TagInstance, exn *wasm.Exception) (matched bool, values []uint64) {
switch kind {
case wasm.CatchKindCatch:
if exn.Tag == clauseTag {
return true, slices.Clone(exn.Params)
}
case wasm.CatchKindCatchRef:
if exn.Tag == clauseTag {
values = slices.Clone(exn.Params)
values = append(values, uint64(uintptr(unsafe.Pointer(exn))))
return true, values
}
case wasm.CatchKindCatchAll:
return true, nil
case wasm.CatchKindCatchAllRef:
return true, []uint64{uint64(uintptr(unsafe.Pointer(exn)))}
}
return false, nil
}
// searchExceptionTable searches the compiled function's static exception table
// for a handler matching the given exception at the current PC. Returns the
// matched clause and catch values, or nil if no handler matches. Searches
// backwards so inner try_tables (which have higher indices) are checked first.
// This function is pure — it does not modify callEngine state.
func searchExceptionTable(exn *wasm.Exception, frame *callFrame) (*exceptionTableCatchClause, []uint64) {
table := frame.f.parent.exceptionTable
pc := frame.pc
for i := len(table) - 1; i >= 0; i-- {
entry := &table[i]
if pc < entry.startPC || pc >= entry.endPC {
continue
}
for j := range entry.clauses {
clause := &entry.clauses[j]
var clauseTag *wasm.TagInstance
if clause.kind == wasm.CatchKindCatch || clause.kind == wasm.CatchKindCatchRef {
clauseTag = frame.f.moduleInstance.Tags[clause.tagIndex]
}
matched, values := matchCatchClause(clause.kind, clauseTag, exn)
if matched {
return clause, values
}
}
}
return nil, nil
}
// applyExceptionHandler applies a matched exception table clause to the callEngine state.
func (ce *callEngine) applyExceptionHandler(frame *callFrame, clause *exceptionTableCatchClause, values []uint64) {
ce.stack = ce.stack[:frame.base-frame.f.funcType.ParamNumInUint64+clause.targetStackDepth]
ce.stack = append(ce.stack, values...)
frame.pc = clause.targetPC
}
// callWithUnwind calls the target function with support for stack unwinding
// (exception handling and snapshot restores). Returns true if the frame was
// unwound (caller should refresh frame/body/bodyLen and continue the loop).
// Returns false on normal return (caller should do frame.pc++).
func (ce *callEngine) callWithUnwind(ctx context.Context, m *wasm.ModuleInstance, tf *function) bool {
// Short-circuit: skip defer/recover overhead when neither exception
// handlers nor the snapshotter are active for the calling frame.
frame := ce.frames[len(ce.frames)-1]
if len(frame.f.parent.exceptionTable) == 0 && ctx.Value(expctxkeys.EnableSnapshotterKey{}) == nil {
ce.callFunction(ctx, m, tf)
return false
}
callerFrameCount := len(ce.frames)
caught := false
func() {
defer func() {
if r := recover(); r != nil {
if v, ok := r.(restorable); ok && v.canRestore(ce, callerFrameCount) {
v.doRestore(ce, callerFrameCount)
caught = true
return
}
panic(r)
}
}()
ce.callFunction(ctx, m, tf)
}()
return caught
}
func (e *moduleEngine) newCallEngine(compiled *function) *callEngine {
return &callEngine{f: compiled}
}
@@ -233,6 +354,7 @@ type callFrame struct {
type compiledFunction struct {
source *wasm.Module
body []unionOperation
exceptionTable []exceptionTableEntry
listener experimental.FunctionListener
offsetsInWasmBinary []uint64
hostFn interface{}
@@ -284,7 +406,11 @@ func (s *snapshot) Restore(ret []uint64) {
panic(s)
}
func (s *snapshot) doRestore() {
func (s *snapshot) canRestore(ce *callEngine, _ int) bool {
return s.ce == ce
}
func (s *snapshot) doRestore(_ *callEngine, _ int) {
ce := s.ce
ce.stack = s.stack
@@ -493,8 +619,45 @@ func (e *engine) lowerIR(ir *compilationResult, ret *compiledFunction) error {
}
case operationKindTailCallReturnCallIndirect:
e.setLabelAddress(&op.Us[1], label(op.Us[1]), labelAddressResolutions)
case operationKindBrOnNull:
e.setLabelAddress(&op.U1, label(op.U1), labelAddressResolutions)
e.setLabelAddress(&op.U2, label(op.U2), labelAddressResolutions)
case operationKindBrOnNonNull:
e.setLabelAddress(&op.U1, label(op.U1), labelAddressResolutions)
e.setLabelAddress(&op.U2, label(op.U2), labelAddressResolutions)
case operationKindReturnCallRef:
e.setLabelAddress(&op.Us[1], label(op.Us[1]), labelAddressResolutions)
}
}
// Resolve exception table entries (translate labels to PC).
if len(ir.PendingExceptionTable) > 0 {
ret.exceptionTable = make([]exceptionTableEntry, len(ir.PendingExceptionTable))
for i, pe := range ir.PendingExceptionTable {
contLabel := newLabel(labelKindContinuation, pe.continuationFrameID)
var endPC uint64
e.setLabelAddress(&endPC, contLabel, labelAddressResolutions)
clauses := make([]exceptionTableCatchClause, len(pe.clauses))
for j, clause := range pe.clauses {
var targetPC uint64
e.setLabelAddress(&targetPC, clause.targetLabel, labelAddressResolutions)
clauses[j] = exceptionTableCatchClause{
kind: clause.kind,
tagIndex: clause.tagIndex,
targetPC: targetPC,
targetStackDepth: clause.targetStackDepth,
}
}
ret.exceptionTable[i] = exceptionTableEntry{
startPC: uint64(pe.startOpIndex),
endPC: endPC,
clauses: clauses,
}
}
}
return nil
}
@@ -644,6 +807,11 @@ func (ce *callEngine) recoverOnCall(ctx context.Context, m *wasm.ModuleInstance,
panic(s)
}
// If an exception reached the top level without being caught, convert it to an uncaught exception error.
if _, ok := v.(*thrownException); ok {
v = wasmruntime.ErrRuntimeUncaughtException
}
builder := wasmdebug.NewErrorBuilder()
frameCount := len(ce.frames)
functionListeners := make([]functionListenerInvocation, 0, 16)
@@ -761,30 +929,26 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
ce.drop(op.Us[v+1])
frame.pc = op.Us[v]
case operationKindCall:
func() {
if ctx.Value(expctxkeys.EnableSnapshotterKey{}) != nil {
defer func() {
if r := recover(); r != nil {
if s, ok := r.(*snapshot); ok && s.ce == ce {
s.doRestore()
frame = ce.frames[len(ce.frames)-1]
body = frame.f.parent.body
bodyLen = uint64(len(body))
} else {
panic(r)
}
}
}()
}
ce.callFunction(ctx, f.moduleInstance, &functions[op.U1])
}()
frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, &functions[op.U1])
if frameUnwound {
frame = ce.frames[len(ce.frames)-1]
body = frame.f.parent.body
bodyLen = uint64(len(body))
continue
}
frame.pc++
case operationKindCallIndirect:
offset := ce.popValue()
table := tables[op.U2]
tf := ce.functionForOffset(table, offset, typeIDs[op.U1])
ce.callFunction(ctx, f.moduleInstance, tf)
frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, tf)
if frameUnwound {
frame = ce.frames[len(ce.frames)-1]
body = frame.f.parent.body
bodyLen = uint64(len(body))
continue
}
frame.pc++
case operationKindDrop:
ce.drop(op.U1)
@@ -973,7 +1137,10 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
case operationKindNe:
var b bool
switch unsignedType(op.B1) {
case unsignedTypeI32, unsignedTypeI64:
case unsignedTypeI32:
v2, v1 := ce.popValue(), ce.popValue()
b = uint32(v1) != uint32(v2)
case unsignedTypeI64:
v2, v1 := ce.popValue(), ce.popValue()
b = v1 != v2
case unsignedTypeF32:
@@ -990,7 +1157,11 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
}
frame.pc++
case operationKindEqz:
if ce.popValue() == 0 {
v := ce.popValue()
if unsignedInt(op.B1) == unsignedInt32 {
v = uint64(uint32(v))
}
if v == 0 {
ce.pushValue(1)
} else {
ce.pushValue(0)
@@ -1005,7 +1176,9 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
b = int32(v1) < int32(v2)
case signedTypeInt64:
b = int64(v1) < int64(v2)
case signedTypeUint32, signedTypeUint64:
case signedTypeUint32:
b = uint32(v1) < uint32(v2)
case signedTypeUint64:
b = v1 < v2
case signedTypeFloat32:
b = math.Float32frombits(uint32(v1)) < math.Float32frombits(uint32(v2))
@@ -1027,7 +1200,9 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
b = int32(v1) > int32(v2)
case signedTypeInt64:
b = int64(v1) > int64(v2)
case signedTypeUint32, signedTypeUint64:
case signedTypeUint32:
b = uint32(v1) > uint32(v2)
case signedTypeUint64:
b = v1 > v2
case signedTypeFloat32:
b = math.Float32frombits(uint32(v1)) > math.Float32frombits(uint32(v2))
@@ -1049,7 +1224,9 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
b = int32(v1) <= int32(v2)
case signedTypeInt64:
b = int64(v1) <= int64(v2)
case signedTypeUint32, signedTypeUint64:
case signedTypeUint32:
b = uint32(v1) <= uint32(v2)
case signedTypeUint64:
b = v1 <= v2
case signedTypeFloat32:
b = math.Float32frombits(uint32(v1)) <= math.Float32frombits(uint32(v2))
@@ -1071,7 +1248,9 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
b = int32(v1) >= int32(v2)
case signedTypeInt64:
b = int64(v1) >= int64(v2)
case signedTypeUint32, signedTypeUint64:
case signedTypeUint32:
b = uint32(v1) >= uint32(v2)
case signedTypeUint64:
b = v1 >= v2
case signedTypeFloat32:
b = math.Float32frombits(uint32(v1)) >= math.Float32frombits(uint32(v2))
@@ -1166,6 +1345,10 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
v2, v1 := ce.popValue(), ce.popValue()
switch t {
case signedTypeFloat32, signedTypeFloat64: // not integers
case signedTypeInt32, signedTypeUint32:
if uint32(v2) == 0 {
panic(wasmruntime.ErrRuntimeIntegerDivideByZero)
}
default:
if v2 == 0 {
panic(wasmruntime.ErrRuntimeIntegerDivideByZero)
@@ -1203,8 +1386,15 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
frame.pc++
case operationKindRem:
v2, v1 := ce.popValue(), ce.popValue()
if v2 == 0 {
panic(wasmruntime.ErrRuntimeIntegerDivideByZero)
switch signedInt(op.B1) {
case signedInt32, signedUint32:
if uint32(v2) == 0 {
panic(wasmruntime.ErrRuntimeIntegerDivideByZero)
}
default:
if v2 == 0 {
panic(wasmruntime.ErrRuntimeIntegerDivideByZero)
}
}
switch signedInt(op.B1) {
case signedInt32:
@@ -4346,6 +4536,35 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
memoryInst.Mux.Unlock()
ce.pushValue(uint64(old))
frame.pc++
case operationKindThrow:
tagIndex := uint32(op.U1)
tag := moduleInst.Tags[tagIndex]
paramCount := len(tag.Type.Params)
params := make([]uint64, paramCount)
for i := paramCount - 1; i >= 0; i-- {
params[i] = ce.popValue()
}
exn := &wasm.Exception{Tag: tag, Params: params}
if clause, values := searchExceptionTable(exn, frame); clause != nil {
ce.applyExceptionHandler(frame, clause, values)
continue
}
panic(&thrownException{exception: exn})
case operationKindThrowRef:
v := ce.popValue()
if v == 0 {
panic(wasmruntime.ErrRuntimeNullReference) // throw_ref on null exnref traps
}
// Read the Exception pointer directly from the uint64 value to avoid
// conversion from uintptr into unsafe.Pointer, which triggers checkptr.
exn := *(**wasm.Exception)(unsafe.Pointer(&v))
if clause, values := searchExceptionTable(exn, frame); clause != nil {
ce.applyExceptionHandler(frame, clause, values)
continue
}
panic(&thrownException{exception: exn})
case operationKindTailCallReturnCall:
f := &functions[op.U1]
ce.dropForTailCall(frame, f)
@@ -4361,7 +4580,13 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
// For details, see internal/engine/RATIONALE.md
if tf.moduleInstance != f.moduleInstance {
// Revert to a normal call.
ce.callFunction(ctx, f.moduleInstance, tf)
frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, tf)
if frameUnwound {
frame = ce.frames[len(ce.frames)-1]
body = frame.f.parent.body
bodyLen = uint64(len(body))
continue
}
// Return
ce.drop(op.Us[0])
// Jump to the function frame (return)
@@ -4372,6 +4597,73 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
ce.dropForTailCall(frame, tf)
body, bodyLen = ce.resetPc(frame, tf)
case operationKindCallRef:
ref := ce.popValue()
if ref == 0 {
panic(wasmruntime.ErrRuntimeNullReference)
}
tf := functionFromUintptr(uintptr(ref))
frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, tf)
if frameUnwound {
frame = ce.frames[len(ce.frames)-1]
body = frame.f.parent.body
bodyLen = uint64(len(body))
continue
}
frame.pc++
case operationKindReturnCallRef:
ref := ce.popValue()
if ref == 0 {
panic(wasmruntime.ErrRuntimeNullReference)
}
tf := functionFromUintptr(uintptr(ref))
if tf.moduleInstance != f.moduleInstance {
frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, tf)
if frameUnwound {
frame = ce.frames[len(ce.frames)-1]
body = frame.f.parent.body
bodyLen = uint64(len(body))
continue
}
ce.drop(op.Us[0])
frame.pc = op.Us[1]
continue
}
ce.dropForTailCall(frame, tf)
body, bodyLen = ce.resetPc(frame, tf)
case operationKindRefAsNonNull:
ref := ce.popValue()
if ref == 0 {
panic(wasmruntime.ErrRuntimeNullReference)
}
ce.pushValue(ref)
frame.pc++
case operationKindBrOnNull:
ref := ce.popValue()
if ref == 0 {
ce.drop(op.U3)
frame.pc = op.U1
} else {
ce.pushValue(ref)
frame.pc = op.U2
}
case operationKindBrOnNonNull:
ref := ce.popValue()
if ref != 0 {
ce.drop(op.U3)
ce.pushValue(ref)
frame.pc = op.U1
} else {
frame.pc = op.U2
}
default:
frame.pc++
}
@@ -4623,7 +4915,9 @@ func (ce *callEngine) callNativeFuncWithListener(ctx context.Context, m *wasm.Mo
// popMemoryOffset takes a memory offset off the stack for use in load and store instructions.
// As the top of stack value is 64-bit, this ensures it is in range before returning it.
func (ce *callEngine) popMemoryOffset(op *unionOperation) uint32 {
offset := op.U2 + ce.popValue()
// Memory addresses are i32; mask to 32 bits to ignore any
// garbage in the upper bits of the uint64 stack slot.
offset := op.U2 + uint64(uint32(ce.popValue()))
if offset > math.MaxUint32 {
panic(wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess)
}
@@ -449,6 +449,20 @@ func (o operationKind) String() (ret string) {
ret = "operationKindTailCallReturnCall"
case operationKindTailCallReturnCallIndirect:
ret = "operationKindTailCallReturnCallIndirect"
case operationKindThrow:
ret = "operationKindThrow"
case operationKindThrowRef:
ret = "operationKindThrowRef"
case operationKindCallRef:
ret = "operationKindCallRef"
case operationKindReturnCallRef:
ret = "operationKindReturnCallRef"
case operationKindRefAsNonNull:
ret = "operationKindRefAsNonNull"
case operationKindBrOnNull:
ret = "operationKindBrOnNull"
case operationKindBrOnNonNull:
ret = "operationKindBrOnNonNull"
default:
panic(fmt.Errorf("unknown operation %d", o))
}
@@ -777,6 +791,22 @@ const (
// operationKindTailCallReturnCallIndirect is the Kind for newOperationKindTailCallReturnCallIndirect.
operationKindTailCallReturnCallIndirect
// operationKindThrow is the Kind for throw instruction.
operationKindThrow
// operationKindThrowRef is the Kind for throw_ref instruction.
operationKindThrowRef
// operationKindCallRef is the Kind for call_ref instruction.
operationKindCallRef
// operationKindReturnCallRef is the Kind for return_call_ref instruction.
operationKindReturnCallRef
// operationKindRefAsNonNull is the Kind for ref.as_non_null instruction.
operationKindRefAsNonNull
// operationKindBrOnNull is the Kind for br_on_null instruction.
operationKindBrOnNull
// operationKindBrOnNonNull is the Kind for br_on_non_null instruction.
operationKindBrOnNonNull
// operationKindEnd is always placed at the bottom of this iota definition to be used in the test.
operationKindEnd
)
@@ -1112,6 +1142,27 @@ func (o unionOperation) String() string {
case operationKindTailCallReturnCallIndirect:
return fmt.Sprintf("%s %d %d", o.Kind, o.U1, o.U2)
case operationKindThrow:
return fmt.Sprintf("%s %d", o.Kind, o.U1)
case operationKindThrowRef:
return o.Kind.String()
case operationKindCallRef:
return fmt.Sprintf("%s %d", o.Kind, o.U1)
case operationKindReturnCallRef:
return fmt.Sprintf("%s %d", o.Kind, o.U1)
case operationKindRefAsNonNull:
return o.Kind.String()
case operationKindBrOnNull:
return fmt.Sprintf("%s %s %s", o.Kind, label(o.U1).String(), label(o.U2).String())
case operationKindBrOnNonNull:
return fmt.Sprintf("%s %s %s", o.Kind, label(o.U1).String(), label(o.U2).String())
default:
panic(fmt.Sprintf("TODO: %v", o.Kind))
}
@@ -2843,3 +2894,85 @@ func newOperationTailCallReturnCall(functionIndex uint32) unionOperation {
func newOperationTailCallReturnCallIndirect(typeIndex, tableIndex uint32, dropDepth inclusiveRange, l label) unionOperation {
return unionOperation{Kind: operationKindTailCallReturnCallIndirect, U1: uint64(typeIndex), U2: uint64(tableIndex), Us: []uint64{dropDepth.AsU64(), uint64(l)}}
}
// newOperationThrow is a constructor for unionOperation with operationKindThrow.
// U1 stores the tag index.
func newOperationThrow(tagIndex uint32) unionOperation {
return unionOperation{Kind: operationKindThrow, U1: uint64(tagIndex)}
}
// newOperationThrowRef is a constructor for unionOperation with operationKindThrowRef.
func newOperationThrowRef() unionOperation {
return unionOperation{Kind: operationKindThrowRef}
}
// newOperationCallRef is a constructor for operationKindCallRef.
// U1 = type index.
func newOperationCallRef(typeIndex uint32) unionOperation {
return unionOperation{Kind: operationKindCallRef, U1: uint64(typeIndex)}
}
// newOperationReturnCallRef is a constructor for operationKindReturnCallRef.
// U1 = type index, U2 = table index (unused), Us = [dropDepth, label].
func newOperationReturnCallRef(typeIndex uint32, dropDepth inclusiveRange, l label) unionOperation {
return unionOperation{Kind: operationKindReturnCallRef, U1: uint64(typeIndex), Us: []uint64{dropDepth.AsU64(), uint64(l)}}
}
// newOperationRefAsNonNull is a constructor for operationKindRefAsNonNull.
func newOperationRefAsNonNull() unionOperation {
return unionOperation{Kind: operationKindRefAsNonNull}
}
// newOperationBrOnNull is a constructor for operationKindBrOnNull.
// If ref is null, branch to U1 (thenTarget) with drop U3; otherwise continue at U2 (elseTarget).
func newOperationBrOnNull(thenTarget, elseTarget label, thenDrop inclusiveRange) unionOperation {
return unionOperation{
Kind: operationKindBrOnNull,
U1: uint64(thenTarget),
U2: uint64(elseTarget),
U3: thenDrop.AsU64(),
}
}
// newOperationBrOnNonNull is a constructor for operationKindBrOnNonNull.
// If ref is non-null, branch to U1 (thenTarget) with drop U3; otherwise continue at U2 (elseTarget).
func newOperationBrOnNonNull(thenTarget, elseTarget label, thenDrop inclusiveRange) unionOperation {
return unionOperation{
Kind: operationKindBrOnNonNull,
U1: uint64(thenTarget),
U2: uint64(elseTarget),
U3: thenDrop.AsU64(),
}
}
// exceptionTableEntry represents one try_table's exception handling scope.
// Built at compile time and stored per compiledFunction.
type exceptionTableEntry struct {
startPC uint64 // first PC inside the try_table body
endPC uint64 // PC of continuation label (exclusive)
clauses []exceptionTableCatchClause
}
// exceptionTableCatchClause is a single catch clause within an exception table entry.
type exceptionTableCatchClause struct {
kind byte // CatchKindCatch, CatchKindCatchRef, CatchKindCatchAll, CatchKindCatchAllRef
tagIndex uint32 // tag index for catch/catch_ref
targetPC uint64 // resolved PC to jump to on match
targetStackDepth int // = targetFrame.originalStackLenWithoutParamUint64
}
// pendingExceptionTableEntry is an unresolved exception table entry built during compilation.
// Labels are resolved to final PCs in lowerIR.
type pendingExceptionTableEntry struct {
startOpIndex int // index in Operations[] of the first instruction inside the try_table body
continuationFrameID uint32
clauses []pendingCatchClause
}
// pendingCatchClause is an unresolved catch clause within a pending exception table entry.
type pendingCatchClause struct {
kind byte
tagIndex uint32
targetLabel label // unresolved label, resolved in lowerIR
targetStackDepth int // = targetFrame.originalStackLenWithoutParamUint64
}
@@ -268,6 +268,9 @@ func (c *compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature
return signature_I32_None, nil
case wasm.OpcodeElse, wasm.OpcodeEnd, wasm.OpcodeBr:
return signature_None_None, nil
case wasm.OpcodeThrow, wasm.OpcodeThrowRef, wasm.OpcodeTryTable:
// Stack manipulation handled dynamically by the compiler.
return signature_None_None, nil
case wasm.OpcodeBrIf, wasm.OpcodeBrTable:
return signature_I32_None, nil
case wasm.OpcodeReturn:
@@ -276,6 +279,17 @@ func (c *compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature
return c.funcTypeToSigs.get(c.funcs[index], false /* direct */), nil
case wasm.OpcodeCallIndirect, wasm.OpcodeTailCallReturnCallIndirect:
return c.funcTypeToSigs.get(index, true /* call_indirect */), nil
case wasm.OpcodeCallRef, wasm.OpcodeReturnCallRef:
return c.funcTypeToSigs.getCallRef(index), nil
case wasm.OpcodeRefAsNonNull:
// Pop a ref (i64), push it back (i64). Traps if null at runtime.
return signature_I64_I64, nil
case wasm.OpcodeBrOnNull:
// Pop a ref (i64). If null, branch; if non-null, push ref back.
return signature_I64_None, nil
case wasm.OpcodeBrOnNonNull:
// Pop a ref (i64). If non-null, push and branch; if null, fall through.
return signature_I64_None, nil
case wasm.OpcodeDrop:
return signature_Unknown_None, nil
case wasm.OpcodeSelect, wasm.OpcodeTypedSelect:
@@ -650,6 +664,7 @@ func (c *compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature
type funcTypeToIRSignatures struct {
directCalls []*signature
indirectCalls []*signature
callRefCalls []*signature
wasmTypes []wasm.FunctionType
}
@@ -694,13 +709,36 @@ func (f *funcTypeToIRSignatures) get(typeIndex wasm.Index, indirect bool) *signa
return sig
}
// getCallRef returns the *signature for call_ref, which is like a direct call
// but with an extra i64 input for the funcref operand.
func (f *funcTypeToIRSignatures) getCallRef(typeIndex wasm.Index) *signature {
if sig := f.callRefCalls[typeIndex]; sig != nil {
return sig
}
tp := &f.wasmTypes[typeIndex]
sig := &signature{
in: make([]unsignedType, 0, len(tp.Params)+1),
out: make([]unsignedType, 0, len(tp.Results)),
}
for _, vt := range tp.Params {
sig.in = append(sig.in, wasmValueTypeTounsignedType(vt))
}
sig.in = append(sig.in, unsignedTypeI64) // funcref operand
for _, vt := range tp.Results {
sig.out = append(sig.out, wasmValueTypeTounsignedType(vt))
}
f.callRefCalls[typeIndex] = sig
return sig
}
func wasmValueTypeTounsignedType(vt wasm.ValueType) unsignedType {
switch vt {
case wasm.ValueTypeI32:
return unsignedTypeI32
case wasm.ValueTypeI64,
// From interpreterir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
wasm.ValueTypeExternref, wasm.ValueTypeFuncref,
wasm.ValueTypeExnref:
return unsignedTypeI64
case wasm.ValueTypeF32:
return unsignedTypeF32
@@ -708,6 +746,11 @@ func wasmValueTypeTounsignedType(vt wasm.ValueType) unsignedType {
return unsignedTypeF64
case wasm.ValueTypeV128:
return unsignedTypeV128
default:
// Concrete ref types (ref $t) have variable bit patterns.
if vt.IsRef() {
return unsignedTypeI64
}
}
panic("unreachable")
}
@@ -718,7 +761,8 @@ func wasmValueTypeToUnsignedOutSignature(vt wasm.ValueType) *signature {
return signature_None_I32
case wasm.ValueTypeI64,
// From interpreterir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
wasm.ValueTypeExternref, wasm.ValueTypeFuncref,
wasm.ValueTypeExnref:
return signature_None_I64
case wasm.ValueTypeF32:
return signature_None_F32
@@ -726,6 +770,10 @@ func wasmValueTypeToUnsignedOutSignature(vt wasm.ValueType) *signature {
return signature_None_F64
case wasm.ValueTypeV128:
return signature_None_V128
default:
if vt.IsRef() {
return signature_None_I64
}
}
panic("unreachable")
}
@@ -736,7 +784,8 @@ func wasmValueTypeToUnsignedInSignature(vt wasm.ValueType) *signature {
return signature_I32_None
case wasm.ValueTypeI64,
// From interpreterir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
wasm.ValueTypeExternref, wasm.ValueTypeFuncref,
wasm.ValueTypeExnref:
return signature_I64_None
case wasm.ValueTypeF32:
return signature_F32_None
@@ -744,6 +793,10 @@ func wasmValueTypeToUnsignedInSignature(vt wasm.ValueType) *signature {
return signature_F64_None
case wasm.ValueTypeV128:
return signature_V128_None
default:
if vt.IsRef() {
return signature_I64_None
}
}
panic("unreachable")
}
@@ -754,7 +807,8 @@ func wasmValueTypeToUnsignedInOutSignature(vt wasm.ValueType) *signature {
return signature_I32_I32
case wasm.ValueTypeI64,
// At interpreterir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
wasm.ValueTypeExternref, wasm.ValueTypeFuncref,
wasm.ValueTypeExnref:
return signature_I64_I64
case wasm.ValueTypeF32:
return signature_F32_F32
@@ -762,6 +816,10 @@ func wasmValueTypeToUnsignedInOutSignature(vt wasm.ValueType) *signature {
return signature_F64_F64
case wasm.ValueTypeV128:
return signature_V128_V128
default:
if vt.IsRef() {
return signature_I64_I64
}
}
panic("unreachable")
}
@@ -370,14 +370,14 @@ func (m *machine) resolveAddressingMode(arg0offset, ret0offset int64, i *instruc
// resolveRelativeAddresses resolves the relative addresses before encoding.
func (m *machine) resolveRelativeAddresses(ctx context.Context) {
for {
if len(m.unresolvedAddressModes) > 0 {
arg0offset, ret0offset := m.arg0OffsetFromSP(), m.ret0OffsetFromSP()
for _, i := range m.unresolvedAddressModes {
m.resolveAddressingMode(arg0offset, ret0offset, i)
}
if len(m.unresolvedAddressModes) > 0 {
arg0offset, ret0offset := m.arg0OffsetFromSP(), m.ret0OffsetFromSP()
for _, i := range m.unresolvedAddressModes {
m.resolveAddressingMode(arg0offset, ret0offset, i)
}
m.unresolvedAddressModes = m.unresolvedAddressModes[:0]
}
for {
// Reuse the slice to gather the unresolved conditional branches.
m.condBrRelocs = m.condBrRelocs[:0]
+188 -12
View File
@@ -43,6 +43,29 @@ type (
execCtxPtr uintptr
numberOfResults int
stackIteratorImpl stackIterator
// tryHandlers is the stack of active try_table exception handlers,
// used to match catch clauses when a throw exits to the dispatch loop.
tryHandlers []tryHandler
// pendingException holds the most recently caught exception, so handler
// code can read its params after re-entry.
pendingException *wasm.Exception
}
// tryHandler records the state at a try_table entry for exception handling.
// On match, we restore the stack to the checkpoint state and re-enter at returnAddress.
tryHandler struct {
// Cloned stack and state from the try_table entry checkpoint,
// using the same approach as experimental.Snapshot.
sp, fp, top uintptr
returnAddress *byte
savedRegisters [64][2]uint64
stack []byte // cloned stack
// catchClauses describes what exceptions this handler catches.
catchClauses []wazevoapi.CatchClauseInstance
// moduleInstance is the module that set up this try handler.
// Used for tag matching in doHandleException (the tag index in
// catch clauses is relative to this module's tag index space).
moduleInstance *wasm.ModuleInstance
}
// executionContext is the struct to be read/written by assembly functions.
@@ -90,6 +113,29 @@ type (
memoryWait64TrampolineAddress *byte
// memoryNotifyTrampolineAddress holds the address of the memory_notify trampoline function.
memoryNotifyTrampolineAddress *byte
// throwAllocTrampolineAddress holds the address of the throw-alloc trampoline:
// phase 1 of throw, which allocates the Exception heap object.
throwAllocTrampolineAddress *byte
// throwTrampolineAddress holds the address of the throw/throw_ref trampoline function.
throwTrampolineAddress *byte
// tryTableEnterTrampolineAddress holds the address of the try_table enter trampoline function.
tryTableEnterTrampolineAddress *byte
// tryTableLeaveTrampolineAddress holds the address of the try_table leave trampoline function.
tryTableLeaveTrampolineAddress *byte
// exceptionPtr holds the pointer to the Exception struct,
// used on the throw side (throwAlloc stores the new Exception)
// and on the catch side (catch_ref/catch_all_ref retrieve the exnref).
exceptionPtr uintptr
// exceptionParamsPtr points into exceptionPtr's Params slice
// backing array. On the throw side, throwAlloc sets it so compiled
// code can store params at [ptr + i*8]. On the catch side, compiled
// handler blocks load params from the same pointer.
exceptionParamsPtr uintptr
// caughtExceptionClauseIdx is set by the dispatch loop to -1 on
// TryTableEnter (normal path) or to the matched catch clause index
// when an exception is caught. Compiled code loads this from execCtx
// after the trampoline call to decide which handler to dispatch to.
caughtExceptionClauseIdx int64
}
)
@@ -217,6 +263,9 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6
}
}
// Clear any stale try_table handlers from a previous call.
c.tryHandlers = c.tryHandlers[:0]
var paramResultPtr *uint64
if len(paramResultStack) > 0 {
paramResultPtr = &paramResultStack[0]
@@ -236,21 +285,25 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6
var listeners []listenerForAbort
builder := wasmdebug.NewErrorBuilder()
def, lsn := c.addFrame(builder, uintptr(unsafe.Pointer(c.execCtx.goCallReturnAddress)))
if lsn != nil {
listeners = append(listeners, listenerForAbort{def, lsn})
}
returnAddrs := unwindStack(
uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)),
c.execCtx.framePointerBeforeGoCall,
c.stackTop,
nil,
)
for _, retAddr := range returnAddrs[:len(returnAddrs)-1] { // the last return addr is the trampoline, so we skip it.
def, lsn = c.addFrame(builder, retAddr)
if c.execCtx.stackPointerBeforeGoCall != nil {
def, lsn := c.addFrame(builder, uintptr(unsafe.Pointer(c.execCtx.goCallReturnAddress)))
if lsn != nil {
listeners = append(listeners, listenerForAbort{def, lsn})
}
returnAddrs := unwindStack(
uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)),
c.execCtx.framePointerBeforeGoCall,
c.stackTop,
nil,
)
if len(returnAddrs) > 1 {
for _, retAddr := range returnAddrs[:len(returnAddrs)-1] { // the last return addr is the trampoline, so we skip it.
def, lsn = c.addFrame(builder, retAddr)
if lsn != nil {
listeners = append(listeners, listenerForAbort{def, lsn})
}
}
}
}
err = builder.FromRecovered(r)
@@ -266,6 +319,7 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6
if err != nil {
// Ensures that we can reuse this callEngine even after an error.
c.execCtx.exitCode = wazevoapi.ExitCodeOK
c.tryHandlers = c.tryHandlers[:0]
}
}()
@@ -505,12 +559,134 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6
panic(wasmruntime.ErrRuntimeInvalidConversionToInteger)
case wazevoapi.ExitCodeUnalignedAtomic:
panic(wasmruntime.ErrRuntimeUnalignedAtomic)
case wazevoapi.ExitCodeThrowAlloc:
// Allocate the Exception heap object sized exactly to the tag's
// param count. Sets exceptionParamsPtr so compiled code can
// store params, and returns the exnref via the stack slot.
s := goCallStackView(c.execCtx.stackPointerBeforeGoCall)
tagIndex := int(s[0])
mod := c.callerModuleInstance()
tag := mod.Tags[tagIndex]
nParams := len(tag.Type.Params)
exn := &wasm.Exception{Tag: tag, Params: make([]uint64, nParams)}
c.pendingException = exn // GC root: keeps exn alive while compiled code writes params
if nParams > 0 {
c.execCtx.exceptionParamsPtr = uintptr(unsafe.Pointer(&exn.Params[0]))
}
// Return the exnref to compiled code via the stack slot.
s[0] = uint64(uintptr(unsafe.Pointer(exn)))
c.execCtx.exitCode = wazevoapi.ExitCodeOK
afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr,
uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall)
case wazevoapi.ExitCodeThrow:
// Throw trampoline: (execCtx, exnref) → ().
// Reads the exnref from the stack, searches for a matching handler.
s := goCallStackView(c.execCtx.stackPointerBeforeGoCall)
// Read the Exception pointer directly from the uint64 value to avoid
// conversion from uintptr into unsafe.Pointer, which triggers checkptr.
exn := *(**wasm.Exception)(unsafe.Pointer(&s[0]))
if !c.doHandleException(exn) {
panic(wasmruntime.ErrRuntimeUncaughtException)
}
if len(exn.Params) > 0 {
c.execCtx.exceptionParamsPtr = uintptr(unsafe.Pointer(&exn.Params[0]))
}
c.execCtx.exceptionPtr = uintptr(unsafe.Pointer(exn))
c.execCtx.exitCode = wazevoapi.ExitCodeOK
afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr,
uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall)
case wazevoapi.ExitCodeNullReference:
panic(wasmruntime.ErrRuntimeNullReference)
case wazevoapi.ExitCodeTryTableEnter:
// Save current state as a try handler checkpoint using stack cloning
// (same approach as experimental.Snapshot).
// The encoded exit code (with tryTableID in upper bits) is on the
// Go call stack as the second trampoline argument, not in execCtx.exitCode.
tryTableEnterStack := goCallStackView(c.execCtx.stackPointerBeforeGoCall)
catchClauseTableIdx := wazevoapi.TryTableIDFromExitCode(wazevoapi.ExitCode(tryTableEnterStack[0]))
mod := c.callerModuleInstance()
me := mod.Engine.(*moduleEngine)
clauses := me.parent.catchClauseTable[catchClauseTableIdx]
returnAddress := c.execCtx.goCallReturnAddress
oldTop, oldSp := c.stackTop, uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall))
newSP, newFP, newTop, newStack := c.cloneStack(uintptr(len(c.stack)) + 16)
adjustClonedStack(oldSp, oldTop, newSP, newFP, newTop)
c.tryHandlers = append(c.tryHandlers, tryHandler{
sp: newSP,
fp: newFP,
top: newTop,
returnAddress: returnAddress,
savedRegisters: c.execCtx.savedRegisters,
stack: newStack,
catchClauses: clauses,
moduleInstance: mod,
})
// Set clauseIdx = -1 (no exception) in execCtx for the compiled code
// to read after the trampoline returns.
c.execCtx.caughtExceptionClauseIdx = -1
c.execCtx.exitCode = wazevoapi.ExitCodeOK
afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr,
uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall)
case wazevoapi.ExitCodeTryTableLeave:
// Pop the most recent try handler.
if len(c.tryHandlers) > 0 {
c.tryHandlers = c.tryHandlers[:len(c.tryHandlers)-1]
}
c.execCtx.exitCode = wazevoapi.ExitCodeOK
afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr,
uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall)
default:
panic("BUG")
}
}
}
// doHandleException tries to match the given exception against active try handlers.
// If a match is found, it restores the execution state to the handler's checkpoint
// (like snapshot.doRestore) and writes the matched clause index as the trampoline
// return value. Returns true if handled.
func (c *callEngine) doHandleException(exn *wasm.Exception) bool {
// Search try handlers from innermost (last) to outermost (first).
for i := len(c.tryHandlers) - 1; i >= 0; i-- {
h := &c.tryHandlers[i]
for clauseIdx, clause := range h.catchClauses {
// Use the module that set up the handler (not the one that threw)
// because clause.TagIndex is relative to that module's tag space.
mod := h.moduleInstance
matched := false
switch clause.Kind {
case wasm.CatchKindCatch, wasm.CatchKindCatchRef:
matched = mod.Tags[clause.TagIndex] == exn.Tag
case wasm.CatchKindCatchAll, wasm.CatchKindCatchAllRef:
matched = true
}
if matched {
// Pop all handlers at and above this one.
c.tryHandlers = c.tryHandlers[:i]
// Store the caught exception so handler code can read params.
c.pendingException = exn
// Restore the cloned stack (like snapshot.doRestore).
spp := *(**uint64)(unsafe.Pointer(&h.sp))
c.stack = h.stack
c.stackTop = h.top
ec := &c.execCtx
ec.stackBottomPtr = &c.stack[0]
ec.stackPointerBeforeGoCall = spp
ec.framePointerBeforeGoCall = h.fp
ec.goCallReturnAddress = h.returnAddress
ec.savedRegisters = h.savedRegisters
// Set the matched clause index in execCtx for compiled code to read.
ec.caughtExceptionClauseIdx = int64(clauseIdx)
return true
}
}
}
return false
}
func (c *callEngine) callerModuleInstance() *wasm.ModuleInstance {
return moduleInstanceFromOpaquePtr(c.execCtx.callerModuleContextPtr)
}
+67 -3
View File
@@ -67,7 +67,16 @@ type (
memoryWait64Address *byte
// memoryNotifyAddress is the address of memory.notify builtin function
memoryNotifyAddress *byte
listenerTrampolines listenerTrampolines
// throwAllocTrampolineAddress is the address of the throw-alloc trampoline:
// phase 1 of throw, which allocates the Exception heap object.
throwAllocTrampolineAddress *byte
// throwTrampolineAddress is the address of the throw/throw_ref trampoline function.
throwTrampolineAddress *byte
// tryTableEnterAddress is the address of try_table enter trampoline.
tryTableEnterAddress *byte
// tryTableLeaveAddress is the address of try_table leave trampoline.
tryTableLeaveAddress *byte
listenerTrampolines listenerTrampolines
}
listenerTrampolines = map[*wasm.FunctionType]struct {
@@ -93,6 +102,9 @@ type (
offsets wazevoapi.ModuleContextOffsetData
sharedFunctions *sharedFunctions
sourceMap sourceMap
// catchClauseTable stores catch clause info for each try_table,
// indexed by a try_table ID assigned during compilation.
catchClauseTable [][]wazevoapi.CatchClauseInstance
}
executables struct {
@@ -269,6 +281,7 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene
relocator.appendFunction(fctx, module, cm, i, fidx, body, relsPerFunc, be.SourceOffsetInfo())
}
cm.catchClauseTable = fe.CatchClauseTable()
} else {
// Compile with N worker goroutines.
// Collect compiled functions across workers in a slice,
@@ -287,6 +300,10 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
// Catch clause table IDs are baked into compiled machine code, so all
// workers must share a single table to ensure globally unique IDs.
sharedCCT := frontend.NewSharedCatchClauseTable()
var count atomic.Uint32
var wg sync.WaitGroup
wg.Add(workers)
@@ -299,7 +316,9 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene
machine := newMachine()
ssaBuilder := ssa.NewBuilder()
be := backend.NewCompiler(ctx, machine, ssaBuilder)
fe := frontend.NewFrontendCompiler(module, ssaBuilder, &cm.offsets, ensureTermination, withListener, needSourceInfo)
fe := frontend.NewFrontendCompiler(
module, ssaBuilder, &cm.offsets, ensureTermination, withListener, needSourceInfo).
WithCatchClauseTable(sharedCCT)
for {
if err := ctx.Err(); err != nil {
@@ -346,6 +365,7 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene
fn := &compiledFuncs[i]
relocator.appendFunction(fn.fctx, module, cm, fn.fnum, fn.fidx, fn.body, fn.relsPerFunc, fn.offsPerFunc)
}
cm.catchClauseTable = sharedCCT.Table()
}
// Allocate executable memory and then copy the generated machine code.
@@ -733,7 +753,7 @@ func (e *engine) NewModuleEngine(m *wasm.Module, mi *wasm.ModuleInstance) (wasm.
}
func (e *engine) compileSharedFunctions() {
var sizes [8]int
var sizes [12]int
var trampolines []byte
addTrampoline := func(i int, buf []byte) {
@@ -801,6 +821,38 @@ func (e *engine) compileSharedFunctions() {
Results: []ssa.Type{ssa.TypeI32},
}, false))
e.be.Init()
addTrampoline(8,
e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeThrowAlloc, &ssa.Signature{
// exec context, tag index → exnref
Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64},
Results: []ssa.Type{ssa.TypeI64},
}, false))
e.be.Init()
addTrampoline(9,
e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeThrow, &ssa.Signature{
// exec context, exnref
Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64},
Results: []ssa.Type{},
}, false))
e.be.Init()
addTrampoline(10,
e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeTryTableEnter, &ssa.Signature{
// exec context, catch clause info (encoded)
Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64},
Results: []ssa.Type{},
}, false))
e.be.Init()
addTrampoline(11,
e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeTryTableLeave, &ssa.Signature{
// exec context
Params: []ssa.Type{ssa.TypeI64},
Results: []ssa.Type{},
}, false))
fns := &sharedFunctions{
executable: mmapExecutable(trampolines),
listenerTrampolines: make(listenerTrampolines),
@@ -823,6 +875,14 @@ func (e *engine) compileSharedFunctions() {
fns.memoryWait64Address = &fns.executable[offset]
offset += sizes[6]
fns.memoryNotifyAddress = &fns.executable[offset]
offset += sizes[7]
fns.throwAllocTrampolineAddress = &fns.executable[offset]
offset += sizes[8]
fns.throwTrampolineAddress = &fns.executable[offset]
offset += sizes[9]
fns.tryTableEnterAddress = &fns.executable[offset]
offset += sizes[10]
fns.tryTableLeaveAddress = &fns.executable[offset]
if wazevoapi.PerfMapEnabled {
wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryGrowAddress)), uint64(sizes[0]), "memory_grow_trampoline")
@@ -833,6 +893,10 @@ func (e *engine) compileSharedFunctions() {
wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryWait32Address)), uint64(sizes[5]), "memory_wait32_trampoline")
wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryWait64Address)), uint64(sizes[6]), "memory_wait64_trampoline")
wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryNotifyAddress)), uint64(sizes[7]), "memory_notify_trampoline")
wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.throwAllocTrampolineAddress)), uint64(sizes[8]), "throw_alloc_trampoline")
wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.throwTrampolineAddress)), uint64(sizes[9]), "throw_trampoline")
wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.tryTableEnterAddress)), uint64(sizes[10]), "try_table_enter_trampoline")
wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.tryTableLeaveAddress)), uint64(sizes[11]), "try_table_leave_trampoline")
}
e.sharedFunctions = fns
@@ -182,6 +182,16 @@ func serializeCompiledModule(wazeroVersion string, cm *compiledModule) io.Reader
} else {
buf.WriteByte(0) // indicates that source map is not present.
}
// Catch clause table: number of try_tables (4 bytes), then for each:
// clause count (4 bytes), then for each clause: kind (1 byte) + tagIndex (4 bytes).
buf.Write(u32.LeBytes(uint32(len(cm.catchClauseTable))))
for _, clauses := range cm.catchClauseTable {
buf.Write(u32.LeBytes(uint32(len(clauses))))
for _, c := range clauses {
buf.WriteByte(c.Kind)
buf.Write(u32.LeBytes(c.TagIndex))
}
}
return bytes.NewReader(buf.Bytes())
}
@@ -291,6 +301,32 @@ func deserializeCompiledModule(wazeroVersion string, reader io.ReadCloser) (cm *
sm.executableOffsets = append(sm.executableOffsets, uintptr(executableRelativeOffset)+executableOffset)
}
}
// Catch clause table.
if _, err = io.ReadFull(reader, eightBytes[:4]); err != nil {
// Treat old cache entries without catch clause data as stale (trigger recompile).
return nil, true, nil
}
tableLen := binary.LittleEndian.Uint32(eightBytes[:4])
if tableLen > 0 {
cm.catchClauseTable = make([][]wazevoapi.CatchClauseInstance, tableLen)
for i := uint32(0); i < tableLen; i++ {
if _, err = io.ReadFull(reader, eightBytes[:4]); err != nil {
return nil, false, fmt.Errorf("compilationcache: error reading catch clause count for try_table[%d]: %v", i, err)
}
clauseCount := binary.LittleEndian.Uint32(eightBytes[:4])
clauses := make([]wazevoapi.CatchClauseInstance, clauseCount)
for j := uint32(0); j < clauseCount; j++ {
if _, err = io.ReadFull(reader, eightBytes[:5]); err != nil {
return nil, false, fmt.Errorf("compilationcache: error reading catch clause[%d][%d]: %v", i, j, err)
}
clauses[j] = wazevoapi.CatchClauseInstance{
Kind: eightBytes[0],
TagIndex: binary.LittleEndian.Uint32(eightBytes[1:5]),
}
}
cm.catchClauseTable[i] = clauses
}
}
return
}
@@ -4,6 +4,7 @@ package frontend
import (
"bytes"
"math"
"sync"
"github.com/tetratelabs/wazero/internal/engine/wazevo/ssa"
"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
@@ -62,6 +63,20 @@ type Compiler struct {
execCtxPtrValue, moduleCtxPtrValue ssa.Value
// throwAllocSig is the signature for the throw-alloc trampoline:
// (execCtx, tagIndex) → (exnref). Allocates the Exception and returns
// its pointer so compiled code can pass it to the throw trampoline.
throwAllocSig ssa.Signature
// throwSig is the signature for the throw/throw_ref trampoline:
// (execCtx, exnref) → (). Searches for a matching handler and restores.
throwSig ssa.Signature
// tryTableEnterSig is the signature for the try_table enter trampoline.
tryTableEnterSig ssa.Signature
// tryTableLeaveSig is the signature for the try_table leave trampoline.
tryTableLeaveSig ssa.Signature
// catchClauseTable accumulates catch clause info for each try_table during compilation.
catchClauseTable catchClauseTable
// Following are reused for the known safe bounds analysis.
pointers []int
@@ -95,12 +110,75 @@ func NewFrontendCompiler(m *wasm.Module, ssaBuilder ssa.Builder, offset *wazevoa
offset: offset,
ensureTermination: ensureTermination,
needSourceOffsetInfo: sourceInfo,
catchClauseTable: &localCatchClauseTable{},
varLengthKnownSafeBoundWithIDPool: wazevoapi.NewVarLengthPool[knownSafeBoundWithID](),
}
c.declareSignatures(listenerOn)
return c
}
// catchClauseTable accumulates catch clause entries during compilation.
type catchClauseTable interface {
Append(clauses []wazevoapi.CatchClauseInstance) int
Table() [][]wazevoapi.CatchClauseInstance
}
// localCatchClauseTable is the single-threaded implementation.
type localCatchClauseTable struct {
table [][]wazevoapi.CatchClauseInstance
}
func (t *localCatchClauseTable) Append(clauses []wazevoapi.CatchClauseInstance) int {
id := len(t.table)
t.table = append(t.table, clauses)
return id
}
func (t *localCatchClauseTable) Table() [][]wazevoapi.CatchClauseInstance {
return t.table
}
// SharedCatchClauseTable is the thread-safe implementation for parallel compilation.
type SharedCatchClauseTable struct {
mu sync.Mutex
table [][]wazevoapi.CatchClauseInstance
finalized bool
}
// NewSharedCatchClauseTable creates a new SharedCatchClauseTable.
func NewSharedCatchClauseTable() *SharedCatchClauseTable {
return &SharedCatchClauseTable{}
}
func (s *SharedCatchClauseTable) Append(clauses []wazevoapi.CatchClauseInstance) int {
if s.finalized {
panic("already finalized")
}
s.mu.Lock()
id := len(s.table)
s.table = append(s.table, clauses)
s.mu.Unlock()
return id
}
func (s *SharedCatchClauseTable) Table() [][]wazevoapi.CatchClauseInstance {
s.finalized = true
return s.table
}
// WithCatchClauseTable replaces the catch clause table implementation.
// Used by the parallel compilation path to share a single mutex-protected
// table across workers.
func (c *Compiler) WithCatchClauseTable(t catchClauseTable) *Compiler {
c.catchClauseTable = t
return c
}
// CatchClauseTable returns the accumulated catch clause table.
func (c *Compiler) CatchClauseTable() [][]wazevoapi.CatchClauseInstance {
return c.catchClauseTable.Table()
}
func (c *Compiler) declareSignatures(listenerOn bool) {
m := c.m
c.signatures = make(map[*wasm.FunctionType]*ssa.Signature, len(m.TypeSection)+2)
@@ -194,6 +272,34 @@ func (c *Compiler) declareSignatures(listenerOn bool) {
Results: []ssa.Type{ssa.TypeI32},
}
c.ssaBuilder.DeclareSignature(&c.memoryNotifySig)
c.throwAllocSig = ssa.Signature{
ID: c.memoryNotifySig.ID + 1,
Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI64 /* tag index */},
Results: []ssa.Type{ssa.TypeI64 /* exnref */},
}
c.ssaBuilder.DeclareSignature(&c.throwAllocSig)
c.throwSig = ssa.Signature{
ID: c.throwAllocSig.ID + 1,
Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI64 /* exnref */},
Results: []ssa.Type{},
}
c.ssaBuilder.DeclareSignature(&c.throwSig)
c.tryTableEnterSig = ssa.Signature{
ID: c.throwSig.ID + 1,
Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI64 /* encoded exit code */},
Results: []ssa.Type{},
}
c.ssaBuilder.DeclareSignature(&c.tryTableEnterSig)
c.tryTableLeaveSig = ssa.Signature{
ID: c.tryTableEnterSig.ID + 1,
Params: []ssa.Type{ssa.TypeI64 /* exec context */},
Results: []ssa.Type{},
}
c.ssaBuilder.DeclareSignature(&c.tryTableLeaveSig)
}
// SignatureForWasmFunctionType returns the ssa.Signature for the given wasm.FunctionType.
@@ -234,7 +340,8 @@ func (c *Compiler) Init(idx, typIndex wasm.Index, typ *wasm.FunctionType, localT
// Note: this assumes 64-bit platform (I believe we won't have 32-bit backend ;)).
const executionContextPtrTyp, moduleContextPtrTyp = ssa.TypeI64, ssa.TypeI64
// LowerToSSA lowers the current function to SSA function which will be held by ssaBuilder.
// LowerToSSA lowers the current function to SSA IR which will be held by ssaBuilder.
//
// After calling this, the caller will be able to access the SSA info in *Compiler.ssaBuilder.
//
// Note that this only does the naive lowering, and do not do any optimization, instead the caller is expected to do so.
@@ -340,23 +447,7 @@ func (c *Compiler) declareNecessaryVariables() {
}
func (c *Compiler) declareWasmGlobal(typ wasm.ValueType, mutable bool) {
var st ssa.Type
switch typ {
case wasm.ValueTypeI32:
st = ssa.TypeI32
case wasm.ValueTypeI64,
// Both externref and funcref are represented as I64 since we only support 64-bit platforms.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
st = ssa.TypeI64
case wasm.ValueTypeF32:
st = ssa.TypeF32
case wasm.ValueTypeF64:
st = ssa.TypeF64
case wasm.ValueTypeV128:
st = ssa.TypeV128
default:
panic("TODO: " + wasm.ValueTypeName(typ))
}
st := WasmTypeToSSAType(typ)
v := c.ssaBuilder.DeclareVariable(st)
index := wasm.Index(len(c.globalVariables))
c.globalVariables = append(c.globalVariables, v)
@@ -372,8 +463,9 @@ func WasmTypeToSSAType(vt wasm.ValueType) ssa.Type {
case wasm.ValueTypeI32:
return ssa.TypeI32
case wasm.ValueTypeI64,
// Both externref and funcref are represented as I64 since we only support 64-bit platforms.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
// externref, funcref, and exnref are represented as I64 since we only support 64-bit platforms.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref,
wasm.ValueTypeExnref:
return ssa.TypeI64
case wasm.ValueTypeF32:
return ssa.TypeF32
@@ -382,6 +474,10 @@ func WasmTypeToSSAType(vt wasm.ValueType) ssa.Type {
case wasm.ValueTypeV128:
return ssa.TypeV128
default:
// Concrete ref types (ref $t) have variable bit patterns.
if vt.IsRef() {
return ssa.TypeI64
}
panic("TODO: " + wasm.ValueTypeName(vt))
}
}
@@ -65,6 +65,7 @@ const (
controlFrameKindIfWithElse
controlFrameKindIfWithoutElse
controlFrameKindBlock
controlFrameKindTryTable
)
// String implements fmt.Stringer for debugging.
@@ -80,6 +81,8 @@ func (k controlFrameKind) String() string {
return "if_without_else"
case controlFrameKindBlock:
return "block"
case controlFrameKindTryTable:
return "try_table"
default:
panic(k)
}
@@ -1443,6 +1446,11 @@ func (c *Compiler) lowerCurrentOpcode() {
unreachable := state.unreachable
if !unreachable {
// For try_table, emit the leave trampoline before the jump to the following block.
if ctrl.kind == controlFrameKindTryTable {
c.emitTryTableLeave()
}
// Top n-th args will be used as a result of the current control frame.
args := c.nPeekDup(len(ctrl.blockType.Results))
@@ -1477,6 +1485,7 @@ func (c *Compiler) lowerCurrentOpcode() {
break
}
c.emitTryTableLeaves(int(labelIndex))
targetBlk, argNum := state.brTargetArgNumFor(labelIndex)
args := c.nPeekDup(argNum)
c.insertJumpToBlock(args, targetBlk)
@@ -1494,6 +1503,21 @@ func (c *Compiler) lowerCurrentOpcode() {
targetBlk, argNum := state.brTargetArgNumFor(labelIndex)
args := c.nPeekDup(argNum)
var sealTargetBlk bool
// If the branch exits any try_table frames, emit TryTableLeave
// calls in a trampoline block that only runs on the taken path.
if c.branchExitsTryTable(int(labelIndex)) {
current := builder.CurrentBlock()
trampolineBlk := builder.AllocateBasicBlock()
builder.SetCurrentBlock(trampolineBlk)
c.emitTryTableLeaves(int(labelIndex))
c.insertJumpToBlock(args, targetBlk)
builder.SetCurrentBlock(current)
targetBlk = trampolineBlk
sealTargetBlk = true
args = ssa.ValuesNil
}
if c.needListener && targetBlk.ReturnBlock() { // In this case, we have to call the listener before returning.
// Save the currently active block.
current := builder.CurrentBlock()
@@ -1559,6 +1583,7 @@ func (c *Compiler) lowerCurrentOpcode() {
if state.unreachable {
break
}
c.emitTryTableLeaves(len(state.controlFrames))
if c.needListener {
c.callListenerAfter()
}
@@ -3355,7 +3380,12 @@ func (c *Compiler) lowerCurrentOpcode() {
state.push(refFuncRet)
case wasm.OpcodeRefNull:
c.loweringState.pc++ // skips the reference type as we treat both of them as i64(0).
switch reftype := c.wasmFunctionBody[c.loweringState.pc+1]; wasm.ValueType(reftype) {
case wasm.ValueTypeFuncref, wasm.ValueTypeExternref, wasm.ValueTypeExnref:
c.loweringState.pc++
default:
c.readI32u()
}
if state.unreachable {
break
}
@@ -3399,6 +3429,9 @@ func (c *Compiler) lowerCurrentOpcode() {
if state.unreachable {
break
}
// Per spec, return_call leaves the current frame, so all enclosing
// try_table handlers must be popped before the tail call.
c.emitTryTableLeaves(len(c.state().controlFrames))
_, _ = typeIndex, tableIndex
c.lowerTailCallReturnCallIndirect(typeIndex, tableIndex)
state.unreachable = true
@@ -3408,9 +3441,396 @@ func (c *Compiler) lowerCurrentOpcode() {
if state.unreachable {
break
}
// Per spec, return_call leaves the current frame, so all enclosing
// try_table handlers must be popped before the tail call.
c.emitTryTableLeaves(len(c.state().controlFrames))
c.lowerTailCallReturnCall(fnIndex)
state.unreachable = true
case wasm.OpcodeThrow:
tagIndex := c.readI32u()
if state.unreachable {
break
}
tagType := c.resolveTagType(tagIndex)
// Pop the tag's param values from the stack.
var throwParams []ssa.Value
if tagType != nil {
throwParams = make([]ssa.Value, len(tagType.Params))
for i := len(tagType.Params) - 1; i >= 0; i-- {
throwParams[i] = state.pop()
}
}
c.storeCallerModuleContext()
tagIdxVal := builder.AllocateInstruction().AsIconst64(uint64(tagIndex)).Insert(builder).Return()
// We need to store the throwParams in the exception and then throw it.
// However, each exception might have a variable number of parameters,
// so we let Go allocate the reference on the heap.
// The Go side allocates the Exception object (Params sized to nParams)
// and stores the pointer to the backing-array into execCtx.exceptionParamsPtr.
throwAllocPtr := builder.AllocateInstruction().
AsLoad(c.execCtxPtrValue,
wazevoapi.ExecutionContextOffsetThrowAllocTrampolineAddress.U32(),
ssa.TypeI64,
).Insert(builder).Return()
throwAllocArgs := c.allocateVarLengthValues(2, c.execCtxPtrValue, tagIdxVal)
exnref := builder.AllocateInstruction().
AsCallIndirect(throwAllocPtr, &c.throwAllocSig, throwAllocArgs).
Insert(builder).Return()
// Reload memory pointers invalidated by the Go call.
c.reloadAfterCall()
// We can now store each param directly into Exception.Params using the pointer
// stored into execCtx.exceptionParamsPtr.
if len(throwParams) > 0 {
paramsPtr := builder.AllocateInstruction().
AsLoad(c.execCtxPtrValue,
wazevoapi.ExecutionContextOffsetExceptionParamsPtr.U32(),
ssa.TypeI64,
).Insert(builder).Return()
for i, v := range throwParams {
switch v.Type() {
case ssa.TypeF32:
v = builder.AllocateInstruction().AsBitcast(v, ssa.TypeI32).Insert(builder).Return()
case ssa.TypeF64:
v = builder.AllocateInstruction().AsBitcast(v, ssa.TypeI64).Insert(builder).Return()
}
builder.AllocateInstruction().
AsStore(ssa.OpcodeStore, v, paramsPtr, uint32(i)*8).
Insert(builder)
}
}
// We return again control to Go to search and dispatch to a matching catch clause.
c.emitThrow(exnref)
state.unreachable = true
case wasm.OpcodeThrowRef:
if state.unreachable {
break
}
exnref := state.pop()
// Check for null exnref.
zero := builder.AllocateInstruction().AsIconst64(0).Insert(builder).Return()
isNull := builder.AllocateInstruction()
isNull.AsIcmp(exnref, zero, ssa.IntegerCmpCondEqual)
builder.InsertInstruction(isNull)
exitIfNull := builder.AllocateInstruction()
exitIfNull.AsExitIfTrueWithCode(c.execCtxPtrValue, isNull.Return(), wazevoapi.ExitCodeNullReference)
builder.InsertInstruction(exitIfNull)
c.storeCallerModuleContext()
c.emitThrow(exnref)
state.unreachable = true
case wasm.OpcodeTryTable:
bt := c.readBlockType()
if state.unreachable {
state.unreachableDepth++
// Still need to skip the catch clause bytes in the unreachable case.
c.skipTryTableCatchClauses()
break
}
// Parse catch clauses.
c.loweringState.pc++
catchCount, catchNum, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:])
c.loweringState.pc += int(catchNum) - 1
var catchClauses []catchClause
for i := uint32(0); i < catchCount; i++ {
c.loweringState.pc++
kind := c.wasmFunctionBody[c.loweringState.pc]
var tagIdx uint32
switch kind {
case wasm.CatchKindCatch, wasm.CatchKindCatchRef:
c.loweringState.pc++
var n uint64
tagIdx, n, _ = leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:])
c.loweringState.pc += int(n) - 1
case wasm.CatchKindCatchAll, wasm.CatchKindCatchAllRef:
// No tagIdx for catch_all variants.
}
c.loweringState.pc++
labelIdx, n, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:])
c.loweringState.pc += int(n) - 1
catchClauses = append(catchClauses, catchClause{kind: kind, tagIndex: tagIdx, labelIdx: labelIdx})
}
// Register catch clauses in the table and get the try_table ID.
var clauseInstances []wazevoapi.CatchClauseInstance
for _, cc := range catchClauses {
clauseInstances = append(clauseInstances, wazevoapi.CatchClauseInstance{
Kind: cc.kind,
TagIndex: cc.tagIndex,
})
}
tryTableID := c.catchClauseTable.Append(clauseInstances)
// Allocate the following block (after try_table end) and body block.
followingBlk := builder.AllocateBasicBlock()
c.addBlockParamsFromWasmTypes(bt.Results, followingBlk)
bodyBlk := builder.AllocateBasicBlock()
if len(catchClauses) > 0 {
// Store the caller module context so the dispatch loop can find the module.
c.storeCallerModuleContext()
// For each catch clause, create a handler block that loads exception
// params and jumps to the wasm target label.
// NOTE: catch clause label indices do NOT include the try_table itself
// (the try_table is pushed onto the control stack after the catch clauses
// are processed, per the spec). So we resolve labels BEFORE pushing.
varPool := builder.VarLengthPool()
targets := varPool.Allocate(len(catchClauses) + 1) // +1 for bodyBlk
currentBlk := builder.CurrentBlock()
for _, cc := range catchClauses {
handlerBlk := builder.AllocateBasicBlock()
builder.SetCurrentBlock(handlerBlk)
c.reloadAfterCall()
// Resolve the wasm target label.
targetBlk, _ := state.brTargetArgNumFor(cc.labelIdx)
// Load exception params and jump to wasm target.
var brArgs []ssa.Value
switch cc.kind {
case wasm.CatchKindCatch:
if tagType := c.resolveTagType(cc.tagIndex); tagType != nil {
brArgs = c.loadExceptionParams(tagType)
}
case wasm.CatchKindCatchRef:
if tagType := c.resolveTagType(cc.tagIndex); tagType != nil {
brArgs = c.loadExceptionParams(tagType)
}
brArgs = append(brArgs, c.loadExnRef())
case wasm.CatchKindCatchAll:
// No values.
case wasm.CatchKindCatchAllRef:
brArgs = append(brArgs, c.loadExnRef())
}
jmpArgs := c.allocateVarLengthValues(len(brArgs), brArgs...)
c.insertJumpToBlock(jmpArgs, targetBlk)
targets = targets.Append(varPool, ssa.Value(handlerBlk.ID()))
}
// Last target is the body block (default for clauseIdx == -1 / out of range).
targets = targets.Append(varPool, ssa.Value(bodyBlk.ID()))
// Back to the original block: call the try_table enter trampoline,
// then dispatch on the caught clause index.
builder.SetCurrentBlock(currentBlk)
encodedExitCode := uint64(wazevoapi.ExitCodeTryTableEnter | wazevoapi.ExitCode(tryTableID<<8))
// Load trampoline address from execCtx.
enterPtr := builder.AllocateInstruction().
AsLoad(c.execCtxPtrValue,
wazevoapi.ExecutionContextOffsetTryTableEnterTrampolineAddress.U32(),
ssa.TypeI64,
).Insert(builder).Return()
// Call the trampoline: (execCtx, encodedExitCode) -> ().
exitCodeVal := builder.AllocateInstruction().AsIconst64(encodedExitCode).Insert(builder).Return()
args := c.allocateVarLengthValues(2, c.execCtxPtrValue, exitCodeVal)
builder.AllocateInstruction().
AsCallIndirect(enterPtr, &c.tryTableEnterSig, args).
Insert(builder)
// Load the caught clause index written by the dispatch loop.
clauseIdx := builder.AllocateInstruction().
AsLoad(c.execCtxPtrValue,
wazevoapi.ExecutionContextOffsetCaughtExceptionClauseIdx.U32(),
ssa.TypeI64,
).Insert(builder).Return()
// Dispatch to handler blocks or body block via br_table.
brTable := builder.AllocateInstruction()
brTable.AsBrTable(clauseIdx, targets)
builder.InsertInstruction(brTable)
// Seal handler blocks after BrTable is inserted (so predecessors are registered).
for _, targetID := range targets.View() {
blk := builder.BasicBlock(ssa.BasicBlockID(targetID))
if !blk.Sealed() {
builder.Seal(blk)
}
}
} else {
// No catch clauses — try_table acts as a plain block.
// Jump directly to body without entering exception handling.
c.insertJumpToBlock(ssa.ValuesNil, bodyBlk)
}
if !bodyBlk.Sealed() {
builder.Seal(bodyBlk)
}
builder.SetCurrentBlock(bodyBlk)
if len(catchClauses) > 0 {
// Body block is entered after the trampoline call, so we need to reload.
c.reloadAfterCall()
}
// Push the try_table control frame AFTER resolving catch labels.
state.ctrlPush(controlFrame{
kind: controlFrameKindTryTable,
originalStackLenWithoutParam: len(state.values) - len(bt.Params),
followingBlock: followingBlk,
blockType: bt,
})
case wasm.OpcodeRefAsNonNull:
if state.unreachable {
break
}
r := state.pop()
zero := builder.AllocateInstruction().AsIconst64(0).Insert(builder)
checkNull := builder.AllocateInstruction().
AsIcmp(r, zero.Return(), ssa.IntegerCmpCondEqual).
Insert(builder).Return()
exitIfNull := builder.AllocateInstruction()
exitIfNull.AsExitIfTrueWithCode(c.execCtxPtrValue, checkNull, wazevoapi.ExitCodeNullReference)
builder.InsertInstruction(exitIfNull)
state.push(r)
case wasm.OpcodeBrOnNull:
labelIndex := c.readI32u()
if state.unreachable {
break
}
r := state.pop()
zero := builder.AllocateInstruction().AsIconst64(0).Insert(builder)
isNull := builder.AllocateInstruction().
AsIcmp(r, zero.Return(), ssa.IntegerCmpCondEqual).
Insert(builder).Return()
targetBlk, argNum := state.brTargetArgNumFor(labelIndex)
args := c.nPeekDup(argNum)
var sealTargetBlk bool
if c.branchExitsTryTable(int(labelIndex)) {
current := builder.CurrentBlock()
trampolineBlk := builder.AllocateBasicBlock()
builder.SetCurrentBlock(trampolineBlk)
c.emitTryTableLeaves(int(labelIndex))
c.insertJumpToBlock(args, targetBlk)
builder.SetCurrentBlock(current)
targetBlk = trampolineBlk
sealTargetBlk = true
args = ssa.ValuesNil
}
if c.needListener && targetBlk.ReturnBlock() {
current := builder.CurrentBlock()
targetBlk = builder.AllocateBasicBlock()
builder.SetCurrentBlock(targetBlk)
sealTargetBlk = true
c.callListenerAfter()
instr := builder.AllocateInstruction()
instr.AsReturn(args)
builder.InsertInstruction(instr)
args = ssa.ValuesNil
builder.SetCurrentBlock(current)
}
brnz := builder.AllocateInstruction()
brnz.AsBrnz(isNull, args, targetBlk)
builder.InsertInstruction(brnz)
if sealTargetBlk {
builder.Seal(targetBlk)
}
// Fall-through: ref is non-null, push it back.
elseBlk := builder.AllocateBasicBlock()
c.insertJumpToBlock(ssa.ValuesNil, elseBlk)
builder.Seal(elseBlk)
builder.SetCurrentBlock(elseBlk)
state.push(r)
case wasm.OpcodeBrOnNonNull:
labelIndex := c.readI32u()
if state.unreachable {
break
}
r := state.pop()
zero := builder.AllocateInstruction().AsIconst64(0).Insert(builder)
isNonNull := builder.AllocateInstruction().
AsIcmp(r, zero.Return(), ssa.IntegerCmpCondNotEqual).
Insert(builder).Return()
// When non-null, branch to label with args + the non-null ref.
targetBlk, argNum := state.brTargetArgNumFor(labelIndex)
// The branch delivers argNum-1 values from the stack plus the ref.
// The ref is the last value delivered to the label target.
args := c.nPeekDup(argNum - 1)
args = args.Append(builder.VarLengthPool(), r)
var sealTargetBlk bool
if c.branchExitsTryTable(int(labelIndex)) {
current := builder.CurrentBlock()
trampolineBlk := builder.AllocateBasicBlock()
builder.SetCurrentBlock(trampolineBlk)
c.emitTryTableLeaves(int(labelIndex))
c.insertJumpToBlock(args, targetBlk)
builder.SetCurrentBlock(current)
targetBlk = trampolineBlk
sealTargetBlk = true
args = ssa.ValuesNil
}
if c.needListener && targetBlk.ReturnBlock() {
current := builder.CurrentBlock()
targetBlk = builder.AllocateBasicBlock()
builder.SetCurrentBlock(targetBlk)
sealTargetBlk = true
c.callListenerAfter()
instr := builder.AllocateInstruction()
instr.AsReturn(args)
builder.InsertInstruction(instr)
args = ssa.ValuesNil
builder.SetCurrentBlock(current)
}
brnz := builder.AllocateInstruction()
brnz.AsBrnz(isNonNull, args, targetBlk)
builder.InsertInstruction(brnz)
if sealTargetBlk {
builder.Seal(targetBlk)
}
// Fall-through: ref is null, nothing extra pushed.
elseBlk := builder.AllocateBasicBlock()
c.insertJumpToBlock(ssa.ValuesNil, elseBlk)
builder.Seal(elseBlk)
builder.SetCurrentBlock(elseBlk)
case wasm.OpcodeCallRef:
typeIndex := c.readI32u()
if state.unreachable {
break
}
c.lowerCallRef(typeIndex)
case wasm.OpcodeReturnCallRef:
typeIndex := c.readI32u()
if state.unreachable {
break
}
c.emitTryTableLeaves(len(c.state().controlFrames))
c.lowerTailCallReturnCallRef(typeIndex)
state.unreachable = true
default:
panic("TODO: unsupported in wazevo yet: " + wasm.InstructionName(op))
}
@@ -3715,6 +4135,91 @@ func (c *Compiler) lowerTailCallReturnCallIndirect(typeIndex, tableIndex uint32)
c.lowerReturn(builder)
}
func (c *Compiler) prepareCallRef(typeIndex uint32) (ssa.Value, *wasm.FunctionType, ssa.Values) {
builder := c.ssaBuilder
state := c.state()
functionInstancePtr := state.pop()
// Check if it is not the null pointer.
zero := builder.AllocateInstruction()
zero.AsIconst64(0)
builder.InsertInstruction(zero)
checkNull := builder.AllocateInstruction()
checkNull.AsIcmp(functionInstancePtr, zero.Return(), ssa.IntegerCmpCondEqual)
builder.InsertInstruction(checkNull)
exitIfNull := builder.AllocateInstruction()
exitIfNull.AsExitIfTrueWithCode(c.execCtxPtrValue, checkNull.Return(), wazevoapi.ExitCodeNullReference)
builder.InsertInstruction(exitIfNull)
// Load the executable and moduleContextOpaquePtr from the function instance.
loadExecutablePtr := builder.AllocateInstruction()
loadExecutablePtr.AsLoad(functionInstancePtr, wazevoapi.FunctionInstanceExecutableOffset, ssa.TypeI64)
builder.InsertInstruction(loadExecutablePtr)
executablePtr := loadExecutablePtr.Return()
loadModuleContextOpaquePtr := builder.AllocateInstruction()
loadModuleContextOpaquePtr.AsLoad(functionInstancePtr, wazevoapi.FunctionInstanceModuleContextOpaquePtrOffset, ssa.TypeI64)
builder.InsertInstruction(loadModuleContextOpaquePtr)
moduleContextOpaquePtr := loadModuleContextOpaquePtr.Return()
typ := &c.m.TypeSection[typeIndex]
tail := len(state.values) - len(typ.Params)
vs := state.values[tail:]
state.values = state.values[:tail]
args := c.allocateVarLengthValues(2+len(vs), c.execCtxPtrValue, moduleContextOpaquePtr)
args = args.Append(builder.VarLengthPool(), vs...)
c.storeCallerModuleContext()
return executablePtr, typ, args
}
func (c *Compiler) lowerCallRef(typeIndex uint32) {
builder := c.ssaBuilder
state := c.state()
executablePtr, typ, args := c.prepareCallRef(typeIndex)
call := builder.AllocateInstruction()
call.AsCallIndirect(executablePtr, c.signatures[typ], args)
builder.InsertInstruction(call)
first, rest := call.Returns()
if first.Valid() {
state.push(first)
}
for _, v := range rest {
state.push(v)
}
c.reloadAfterCall()
}
func (c *Compiler) lowerTailCallReturnCallRef(typeIndex uint32) {
builder := c.ssaBuilder
state := c.state()
executablePtr, typ, args := c.prepareCallRef(typeIndex)
call := builder.AllocateInstruction()
call.AsTailCallReturnCallIndirect(executablePtr, c.signatures[typ], args)
builder.InsertInstruction(call)
// In a proper tail call, the following code is unreachable since execution
// transfers to the callee. However, sometimes the backend might need to fall back to
// a regular call, so we include return handling and let the backend delete it
// when redundant.
// For details, see internal/engine/RATIONALE.md
first, rest := call.Returns()
if first.Valid() {
state.push(first)
}
for _, v := range rest {
state.push(v)
}
c.reloadAfterCall()
c.lowerReturn(builder)
}
// memOpSetup inserts the bounds check and calculates the address of the memory operation (loads/stores).
func (c *Compiler) memOpSetup(baseAddr ssa.Value, constOffset, operationSizeInBytes uint64) (address ssa.Value) {
address = ssa.ValueInvalid
@@ -4026,6 +4531,185 @@ func (c *Compiler) storeCallerModuleContext() {
builder.InsertInstruction(store)
}
// resolveTagType returns the FunctionType for the tag at the given module-local index.
func (c *Compiler) resolveTagType(tagIndex uint32) *wasm.FunctionType {
if tagIndex < c.m.ImportTagCount {
cur := uint32(0)
for i := range c.m.ImportSection {
imp := &c.m.ImportSection[i]
if imp.Type != wasm.ExternTypeTag {
continue
}
if tagIndex == cur {
return &c.m.TypeSection[imp.DescTag]
}
cur++
}
} else {
tagSectionIdx := tagIndex - c.m.ImportTagCount
if tagSectionIdx < uint32(len(c.m.TagSection)) {
typeIdx := c.m.TagSection[tagSectionIdx].Type
return &c.m.TypeSection[typeIdx]
}
}
return nil
}
// emitThrow emits a call to the shared throw trampoline with the given exnref,
// followed by an unreachable exit (throw never returns).
func (c *Compiler) emitThrow(exnref ssa.Value) {
builder := c.ssaBuilder
throwPtr := builder.AllocateInstruction().
AsLoad(c.execCtxPtrValue,
wazevoapi.ExecutionContextOffsetThrowTrampolineAddress.U32(),
ssa.TypeI64,
).Insert(builder).Return()
throwArgs := c.allocateVarLengthValues(2, c.execCtxPtrValue, exnref)
builder.AllocateInstruction().
AsCallIndirect(throwPtr, &c.throwSig, throwArgs).
Insert(builder)
exit := builder.AllocateInstruction()
exit.AsExitWithCode(c.execCtxPtrValue, wazevoapi.ExitCodeUnreachable)
builder.InsertInstruction(exit)
}
// emitTryTableLeave emits a trampoline call to pop the try handler in the dispatch loop.
func (c *Compiler) emitTryTableLeave() {
builder := c.ssaBuilder
c.storeCallerModuleContext()
leavePtr := builder.AllocateInstruction().
AsLoad(c.execCtxPtrValue,
wazevoapi.ExecutionContextOffsetTryTableLeaveTrampolineAddress.U32(),
ssa.TypeI64,
).Insert(builder).Return()
args := c.allocateVarLengthValues(1, c.execCtxPtrValue)
builder.AllocateInstruction().
AsCallIndirect(leavePtr, &c.tryTableLeaveSig, args).
Insert(builder)
}
// branchExitsTryTable returns true if a branch to the given depth would
// exit at least one try_table frame.
func (c *Compiler) branchExitsTryTable(depth int) bool {
state := c.state()
tail := len(state.controlFrames) - 1
for i := 0; i < depth; i++ {
if state.controlFrames[tail-i].kind == controlFrameKindTryTable {
return true
}
}
return false
}
// emitTryTableLeaves emits TryTableLeave calls for try_table frames
// that would be exited by a branch to the given depth.
func (c *Compiler) emitTryTableLeaves(depth int) {
state := c.state()
tail := len(state.controlFrames) - 1
for i := 0; i < depth; i++ {
if state.controlFrames[tail-i].kind == controlFrameKindTryTable {
c.emitTryTableLeave()
}
}
}
// catchClause holds a parsed catch clause from a try_table instruction.
type catchClause struct {
kind byte
tagIndex uint32
labelIdx uint32
}
// loadExceptionParams loads the exception params from the caught Exception's
// Params slice. The dispatch loop sets execCtx.exceptionParamsPtr to the
// slice's backing-array pointer after matching a handler. We load that pointer
// and then read each param from [ptr + i*8], mirroring the stores emitted by
// the throw lowering. Float params were bitcast to integers at the throw site,
// so we load as integer and bitcast back to the original type.
func (c *Compiler) loadExceptionParams(tagType *wasm.FunctionType) []ssa.Value {
if len(tagType.Params) == 0 {
return nil
}
builder := c.ssaBuilder
// Load the pointer to the caught Exception's Params backing array.
paramsPtr := builder.AllocateInstruction().
AsLoad(c.execCtxPtrValue,
wazevoapi.ExecutionContextOffsetExceptionParamsPtr.U32(),
ssa.TypeI64,
).Insert(builder).Return()
var values []ssa.Value
for i, vt := range tagType.Params {
offset := uint32(i) * 8
ssaType := WasmTypeToSSAType(vt)
switch ssaType {
case ssa.TypeF32:
// Stored as i32 at throw site; bitcast back to f32.
raw := builder.AllocateInstruction().
AsLoad(paramsPtr, offset, ssa.TypeI32).
Insert(builder).Return()
val := builder.AllocateInstruction().AsBitcast(raw, ssa.TypeF32).Insert(builder).Return()
values = append(values, val)
case ssa.TypeF64:
// Stored as i64 at throw site; bitcast back to f64.
raw := builder.AllocateInstruction().
AsLoad(paramsPtr, offset, ssa.TypeI64).
Insert(builder).Return()
val := builder.AllocateInstruction().AsBitcast(raw, ssa.TypeF64).Insert(builder).Return()
values = append(values, val)
default:
val := builder.AllocateInstruction().
AsLoad(paramsPtr, offset, ssaType).
Insert(builder).Return()
values = append(values, val)
}
}
return values
}
// loadExnRef loads the exnref (pointer to Exception) from the executionContext.
// The dispatch loop writes it to exceptionPtr after matching a handler.
func (c *Compiler) loadExnRef() ssa.Value {
builder := c.ssaBuilder
return builder.AllocateInstruction().
AsLoad(c.execCtxPtrValue,
wazevoapi.ExecutionContextOffsetExceptionPtr.U32(),
ssa.TypeI64,
).Insert(builder).Return()
}
// skipTryTableCatchClauses advances the bytecode PC past the catch clauses
// of a try_table instruction. This is used both in reachable and unreachable states.
func (c *Compiler) skipTryTableCatchClauses() {
c.loweringState.pc++
catchCount, catchNum, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:])
c.loweringState.pc += int(catchNum) - 1
for i := uint32(0); i < catchCount; i++ {
c.loweringState.pc++
kind := c.wasmFunctionBody[c.loweringState.pc]
switch kind {
case wasm.CatchKindCatch, wasm.CatchKindCatchRef:
// Read tag index.
c.loweringState.pc++
_, n, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:])
c.loweringState.pc += int(n) - 1
// Read label index.
c.loweringState.pc++
_, n, _ = leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:])
c.loweringState.pc += int(n) - 1
case wasm.CatchKindCatchAll, wasm.CatchKindCatchAllRef:
// Read label index.
c.loweringState.pc++
_, n, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:])
c.loweringState.pc += int(n) - 1
}
}
}
func (c *Compiler) readByte() byte {
v := c.wasmFunctionBody[c.loweringState.pc+1]
c.loweringState.pc++
@@ -4181,6 +4865,7 @@ func (c *Compiler) lowerBrTable(labels []uint32, index ssa.Value) {
targetBlk, _ := state.brTargetArgNumFor(l)
trampoline := builder.AllocateBasicBlock()
builder.SetCurrentBlock(trampoline)
c.emitTryTableLeaves(int(l))
c.insertJumpToBlock(args, targetBlk)
trampolineBlockIDs = trampolineBlockIDs.Append(builder.VarLengthPool(), ssa.Value(trampoline.ID()))
}
@@ -1,13 +1,17 @@
package wazevo
import (
"context"
"encoding/binary"
"fmt"
"unsafe"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
"github.com/tetratelabs/wazero/internal/internalapi"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wasmdebug"
"github.com/tetratelabs/wazero/internal/wasmruntime"
)
@@ -132,6 +136,14 @@ func (m *moduleEngine) setupOpaque() {
}
}
if tagOffset := offsets.TagsBegin; tagOffset >= 0 {
for _, tag := range inst.Tags {
binary.LittleEndian.PutUint64(opaque[tagOffset:],
uint64(uintptr(unsafe.Pointer(tag))))
tagOffset += 8
}
}
if beforeListenerOffset := offsets.BeforeListenerTrampolines1stElement; beforeListenerOffset >= 0 {
binary.LittleEndian.PutUint64(opaque[beforeListenerOffset:], uint64(uintptr(unsafe.Pointer(&m.parent.listenerBeforeTrampolines[0]))))
}
@@ -160,6 +172,21 @@ func (m *moduleEngine) NewFunction(index wasm.Index) api.Function {
localIndex -= importedFnCount
}
if source := m.module.Source; source.IsHostModule {
// For host modules, we need to look up the GoFunction from the CodeSection.
def := source.FunctionDefinition(localIndex)
goF := source.CodeSection[localIndex].GoFunc
switch typed := goF.(type) {
case api.GoFunction:
// GoFunction doesn't need looked up module.
return &hostFunction{def: def, g: goFunctionAsGoModuleFunction(typed)}
case api.GoModuleFunction:
return &hostFunction{def: def, lookedUpModule: m.module, g: typed}
default:
panic(fmt.Sprintf("unexpected GoFunc type: %T", goF))
}
}
src := m.module.Source
typIndex := src.FunctionSection[localIndex]
typ := src.TypeSection[typIndex]
@@ -189,6 +216,10 @@ func (m *moduleEngine) NewFunction(index wasm.Index) api.Function {
ce.execCtx.memoryWait32TrampolineAddress = sharedFunctions.memoryWait32Address
ce.execCtx.memoryWait64TrampolineAddress = sharedFunctions.memoryWait64Address
ce.execCtx.memoryNotifyTrampolineAddress = sharedFunctions.memoryNotifyAddress
ce.execCtx.throwAllocTrampolineAddress = sharedFunctions.throwAllocTrampolineAddress
ce.execCtx.throwTrampolineAddress = sharedFunctions.throwTrampolineAddress
ce.execCtx.tryTableEnterTrampolineAddress = sharedFunctions.tryTableEnterAddress
ce.execCtx.tryTableLeaveTrampolineAddress = sharedFunctions.tryTableLeaveAddress
ce.execCtx.memmoveAddress = memmovPtr
ce.init()
return ce
@@ -242,15 +273,13 @@ func (m *moduleEngine) ResolveImportedFunction(index, descFunc, indexInImportedM
executableOffset, moduleCtxOffset, typeIDOffset := m.parent.offsets.ImportedFunctionOffset(index)
importedME := importedModuleEngine.(*moduleEngine)
if int(indexInImportedModule) >= len(importedME.importedFunctions) {
indexInImportedModule -= wasm.Index(len(importedME.importedFunctions))
} else {
if int(indexInImportedModule) < len(importedME.importedFunctions) {
imported := &importedME.importedFunctions[indexInImportedModule]
m.ResolveImportedFunction(index, descFunc, imported.indexInModule, imported.me)
return // Recursively resolve the imported function.
}
offset := importedME.parent.functionOffsets[indexInImportedModule]
offset := importedME.parent.functionOffsets[indexInImportedModule-wasm.Index(len(importedME.importedFunctions))]
typeID := m.module.TypeIDs[descFunc]
executable := &importedME.parent.executable[offset]
// Write functionInstance.
@@ -330,3 +359,45 @@ func (m *moduleEngine) LookupFunction(t *wasm.TableInstance, typeId wasm.Functio
func moduleInstanceFromOpaquePtr(ptr *byte) *wasm.ModuleInstance {
return *(**wasm.ModuleInstance)(unsafe.Pointer(ptr))
}
type hostFunction struct {
internalapi.WazeroOnly
def *wasm.FunctionDefinition
lookedUpModule *wasm.ModuleInstance
g api.GoModuleFunction
}
// goFunctionAsGoModuleFunction converts api.GoFunction to api.GoModuleFunction which ignores the api.Module argument.
func goFunctionAsGoModuleFunction(g api.GoFunction) api.GoModuleFunction {
return api.GoModuleFunc(func(ctx context.Context, _ api.Module, stack []uint64) {
g.Call(ctx, stack)
})
}
// Definition implements api.Function.
func (f *hostFunction) Definition() api.FunctionDefinition { return f.def }
// Call implements api.Function.
func (f *hostFunction) Call(ctx context.Context, params ...uint64) ([]uint64, error) {
typ := f.def.Functype
stackSize := typ.ParamNumInUint64
rn := typ.ResultNumInUint64
if rn > stackSize {
stackSize = rn
}
stack := make([]uint64, stackSize)
copy(stack, params)
return stack[:rn], f.CallWithStack(ctx, stack)
}
// CallWithStack implements api.Function.
func (f *hostFunction) CallWithStack(ctx context.Context, stack []uint64) (err error) {
defer func() {
if r := recover(); r != nil {
builder := wasmdebug.NewErrorBuilder()
err = builder.FromRecovered(r)
}
}()
f.g.Call(ctx, f.lookedUpModule, stack)
return nil
}
@@ -291,9 +291,9 @@ func (bb *basicBlock) addPred(blk BasicBlock, branch *Instruction) {
for i := range bb.preds {
existingPred := &bb.preds[i]
if existingPred.blk == pred && existingPred.branch != branch {
// If the target is already added, then this must come from the same BrTable,
// If the target is already added, then this must come from the same BrTable or TryTableDispatch,
// otherwise such redundant branch should be eliminated by the frontend. (which should be simpler).
panic(fmt.Sprintf("BUG: redundant non BrTable jumps in %s whose targes are the same", bb.Name()))
panic(fmt.Sprintf("BUG: redundant non BrTable/TryTableDispatch jumps in %s whose targets are the same", bb.Name()))
}
}
@@ -344,19 +344,21 @@ func (bb *basicBlock) validate(b *builder) {
}
}
var exp int
if bb.ReturnBlock() {
exp = len(b.currentSignature.Results)
} else {
exp = len(bb.params.View())
}
if pred.branch.opcode != OpcodeBrTable {
var exp int
if bb.ReturnBlock() {
exp = len(b.currentSignature.Results)
} else {
exp = len(bb.params.View())
}
if len(pred.branch.vs.View()) != exp {
panic(fmt.Sprintf(
"BUG: len(argument at %s) != len(params at %s): %d != %d: %s",
pred.blk.Name(), bb.Name(),
len(pred.branch.vs.View()), len(bb.params.View()), pred.branch.Format(b),
))
if len(pred.branch.vs.View()) != exp {
panic(fmt.Sprintf(
"BUG: len(argument at %s) != len(params at %s): %d != %d: %s",
pred.blk.Name(), bb.Name(),
len(pred.branch.vs.View()), len(bb.params.View()), pred.branch.Format(b),
))
}
}
}
@@ -30,6 +30,25 @@ const (
ExitCodeMemoryWait64
ExitCodeMemoryNotify
ExitCodeUnalignedAtomic
// ExitCodeThrowAlloc is the first phase of wasm throw: Go allocates the
// Exception heap object (with Params sized to the tag's param count) and
// writes its Params data pointer to execCtx.exceptionParamsPtr.
// Compiled code then stores params directly into the Exception.Params slice,
// followed by ExitCodeThrow to search for a matching handler.
ExitCodeThrowAlloc
// ExitCodeThrow is the shared throw/throw_ref exit code.
// The exnref is passed on the stack. The handler searches for a
// matching catch clause and restores the stack checkpoint.
ExitCodeThrow
// ExitCodeNullReference is an exit code for a null reference trap (throw_ref with null exnref).
ExitCodeNullReference
// ExitCodeTryTableEnter is an exit code for entering a try_table block.
// The catch clause info is encoded in the upper bits. The dispatch loop
// saves the current SP/FP/returnAddress as a try handler checkpoint.
ExitCodeTryTableEnter
// ExitCodeTryTableLeave is an exit code for leaving a try_table block.
// The dispatch loop pops the most recent try handler.
ExitCodeTryTableLeave
exitCodeMax
)
@@ -86,6 +105,16 @@ func (e ExitCode) String() string {
return "memory_wait64"
case ExitCodeMemoryNotify:
return "memory_notify"
case ExitCodeThrowAlloc:
return "throw_alloc"
case ExitCodeThrow:
return "throw"
case ExitCodeNullReference:
return "null_reference"
case ExitCodeTryTableEnter:
return "try_table_enter"
case ExitCodeTryTableLeave:
return "try_table_leave"
}
panic("TODO")
}
@@ -107,3 +136,15 @@ func ExitCodeCallGoFunctionWithIndex(index int, withListener bool) ExitCode {
func GoFunctionIndexFromExitCode(exitCode ExitCode) int {
return int(exitCode >> 8)
}
// TryTableIDFromExitCode extracts the try-table ID from an ExitCodeTryTableEnter
// exit code. Uses the same encoding as GoFunctionIndexFromExitCode (upper 24 bits).
func TryTableIDFromExitCode(exitCode ExitCode) int {
return GoFunctionIndexFromExitCode(exitCode)
}
// CatchClauseInstance is a runtime catch clause with resolved tag index.
type CatchClauseInstance struct {
Kind byte // wasm.CatchKindCatch, etc.
TagIndex uint32 // module-local tag index
}
@@ -53,6 +53,23 @@ const (
ExecutionContextOffsetMemoryWait32TrampolineAddress Offset = 1160
ExecutionContextOffsetMemoryWait64TrampolineAddress Offset = 1168
ExecutionContextOffsetMemoryNotifyTrampolineAddress Offset = 1176
// ExecutionContextOffsetThrowAllocTrampolineAddress is the address of the
// throw-alloc trampoline, which allocates the Exception heap object,
// sets exceptionParamsPtr, and returns the exnref.
ExecutionContextOffsetThrowAllocTrampolineAddress Offset = 1184
ExecutionContextOffsetThrowTrampolineAddress Offset = 1192
ExecutionContextOffsetTryTableEnterTrampolineAddress Offset = 1200
ExecutionContextOffsetTryTableLeaveTrampolineAddress Offset = 1208
// ExecutionContextOffsetExceptionPtr holds the pointer to the Exception struct,
// used on the throw side and by catch_ref/catch_all_ref handlers.
ExecutionContextOffsetExceptionPtr Offset = 1216
// ExecutionContextOffsetExceptionParamsPtr points into the Exception's
// Params slice backing array. Used by both throw (store params) and
// catch (load params) sides.
ExecutionContextOffsetExceptionParamsPtr Offset = 1224
// ExecutionContextOffsetCaughtExceptionClauseIdx is the matched catch clause index
// written by handleException and read by compiled handler dispatch code.
ExecutionContextOffsetCaughtExceptionClauseIdx Offset = 1232
)
// ModuleContextOffsetData allows the compilers to get the information about offsets to the fields of wazevo.moduleContextOpaque,
@@ -66,6 +83,7 @@ type ModuleContextOffsetData struct {
GlobalsBegin,
TypeIDs1stElement,
TablesBegin,
TagsBegin,
BeforeListenerTrampolines1stElement,
AfterListenerTrampolines1stElement,
DataInstances1stElement,
@@ -122,6 +140,11 @@ func (m *ModuleContextOffsetData) TableOffset(tableIndex int) Offset {
return m.TablesBegin + Offset(tableIndex)*8
}
// TagOffset returns an offset of the i-th tag instance pointer.
func (m *ModuleContextOffsetData) TagOffset(tagIndex int) Offset {
return m.TagsBegin + Offset(tagIndex)*8
}
// NewModuleContextOffsetData creates a ModuleContextOffsetData determining the structure of moduleContextOpaque for the given Module.
// The structure is described in the comment of wazevo.moduleContextOpaque.
func NewModuleContextOffsetData(m *wasm.Module, withListener bool) ModuleContextOffsetData {
@@ -185,6 +208,15 @@ func NewModuleContextOffsetData(m *wasm.Module, withListener bool) ModuleContext
ret.TablesBegin = -1
}
if tags := int(m.ImportTagCount) + len(m.TagSection); tags > 0 {
offset = align8(offset)
ret.TagsBegin = offset
// Pointers to *wasm.TagInstance.
offset += Offset(tags) * 8
} else {
ret.TagsBegin = -1
}
if withListener {
offset = align8(offset)
ret.BeforeListenerTrampolines1stElement = offset
-69
View File
@@ -1,69 +0,0 @@
package fsapi
import experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
// File includes methods not yet ready to document for end users, notably
// non-blocking functionality.
//
// Particularly, Poll is subject to debate. For example, whether a user should
// be able to choose how to implement timeout or not. Currently, this interface
// allows the user to choose to sleep or use native polling, and which choice
// they make impacts thread behavior as summarized here:
// https://github.com/tetratelabs/wazero/pull/1606#issuecomment-1665475516
type File interface {
experimentalsys.File
// IsNonblock returns true if the file was opened with O_NONBLOCK, or
// SetNonblock was successfully enabled on this file.
//
// # Notes
//
// - This might not match the underlying state of the file descriptor if
// the file was not opened via OpenFile.
IsNonblock() bool
// SetNonblock toggles the non-blocking mode (O_NONBLOCK) of this file.
//
// # Errors
//
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed.
//
// # Notes
//
// - This is like syscall.SetNonblock and `fcntl` with O_NONBLOCK in
// POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html
SetNonblock(enable bool) experimentalsys.Errno
// Poll returns if the file has data ready to be read or written.
//
// # Parameters
//
// The `flag` parameter determines which event to await, such as POLLIN,
// POLLOUT, or a combination like `POLLIN|POLLOUT`.
//
// The `timeoutMillis` parameter is how long to block for an event, or
// interrupted, in milliseconds. There are two special values:
// - zero returns immediately
// - any negative value blocks any amount of time
//
// # Results
//
// `ready` means there was data ready to read or written. False can mean no
// event was ready or `errno` is not zero.
//
// A zero `errno` is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - ENOTSUP: the implementation does not the flag combination.
// - EINTR: the call was interrupted prior to an event.
//
// # Notes
//
// - This is like `poll` in POSIX, for a single file.
// See https://pubs.opengroup.org/onlinepubs/9699919799/functions/poll.html
// - No-op files, such as those which read from /dev/null, should return
// immediately true, as data will never become available.
// - See /RATIONALE.md for detailed notes including impact of blocking.
Poll(flag Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno)
}
-20
View File
@@ -1,20 +0,0 @@
package fsapi
// Pflag are bit flags used for File.Poll. Values, including zero, should not
// be interpreted numerically. Instead, use by constants prefixed with 'POLL'.
//
// # Notes
//
// - This is like `pollfd.events` flags for `poll` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/poll.h.html
type Pflag uint32
// Only define bitflags we support and are needed by `poll_oneoff` in wasip1
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#eventrwflags
const (
// POLLIN is a read event.
POLLIN Pflag = 1 << iota
// POLLOUT is a write event.
POLLOUT
)
-27
View File
@@ -1,27 +0,0 @@
package fsapi
import experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
func Adapt(f experimentalsys.File) File {
if f, ok := f.(File); ok {
return f
}
return unimplementedFile{f}
}
type unimplementedFile struct{ experimentalsys.File }
// IsNonblock implements File.IsNonblock
func (unimplementedFile) IsNonblock() bool {
return false
}
// SetNonblock implements File.SetNonblock
func (unimplementedFile) SetNonblock(bool) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Poll implements File.Poll
func (unimplementedFile) Poll(Pflag, int32) (ready bool, errno experimentalsys.Errno) {
return false, experimentalsys.ENOSYS
}
+2 -17
View File
@@ -1,28 +1,13 @@
package platform
import (
"runtime"
"golang.org/x/sys/cpu"
)
import "golang.org/x/sys/cpu"
// CpuFeatures exposes the capabilities for this CPU, queried via the Has method.
var CpuFeatures = loadCpuFeatureFlags()
func loadCpuFeatureFlags() (flags CpuFeatureFlags) {
switch runtime.GOOS {
case "darwin", "windows":
// These OSes do not allow userland to read the instruction set attribute registers,
// but basically require atomic instructions:
// - "darwin" is the desktop version (mobile version is "ios"),
// and the M1 is a ARMv8.4.
// - "windows" requires them from Windows 11, see page 12
// https://download.microsoft.com/download/7/8/8/788bf5ab-0751-4928-a22c-dffdc23c27f2/Minimum%20Hardware%20Requirements%20for%20Windows%2011.pdf
if cpu.ARM64.HasATOMICS {
flags |= CpuFeatureArm64Atomic
default:
if cpu.ARM64.HasATOMICS {
flags |= CpuFeatureArm64Atomic
}
}
return
}
+23 -1
View File
@@ -14,6 +14,14 @@ func CompilerSupported() bool {
}
func CompilerSupports(features api.CoreFeatures) bool {
if !compilerPlatformSupports(features) {
return false
}
// Won't panic
return executableMmapSupported()
}
func compilerPlatformSupports(features api.CoreFeatures) bool {
switch runtime.GOOS {
case "linux", "darwin", "freebsd", "netbsd", "windows":
if runtime.GOARCH == "arm64" {
@@ -30,7 +38,7 @@ func CompilerSupports(features api.CoreFeatures) bool {
}
}
// MmapCodeSegment copies the code into the executable region and returns the byte slice of the region.
// MmapCodeSegment allocates and returns a byte slice to copy executable code into.
//
// See https://man7.org/linux/man-pages/man2/mmap.2.html for mmap API and flags.
func MmapCodeSegment(size int) ([]byte, error) {
@@ -47,3 +55,17 @@ func MunmapCodeSegment(code []byte) error {
}
return munmapCodeSegment(code)
}
func executableMmapSupported() bool {
seg, err := MmapCodeSegment(1)
if err != nil {
return false
}
defer func() {
_ = MunmapCodeSegment(seg)
}()
if err := MprotectCodeSegment(seg); err != nil {
return false
}
return true
}
+9 -8
View File
@@ -7,7 +7,6 @@ import (
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/descriptor"
"github.com/tetratelabs/wazero/internal/fsapi"
socketapi "github.com/tetratelabs/wazero/internal/sock"
"github.com/tetratelabs/wazero/internal/sysfs"
)
@@ -51,7 +50,7 @@ type FileEntry struct {
FS sys.FS
// File is always non-nil.
File fsapi.File
File sys.File
// direntCache is nil until DirentCache was called.
direntCache *DirentCache
@@ -272,7 +271,7 @@ func (c *FSContext) OpenFile(fs sys.FS, path string, flag sys.Oflag, perm fs.Fil
if f, errno := fs.OpenFile(path, flag, perm); errno != 0 {
return 0, errno
} else {
fe := &FileEntry{FS: fs, File: fsapi.Adapt(f)}
fe := &FileEntry{FS: fs, File: f}
if path == "/" || path == "." {
fe.Name = ""
} else {
@@ -329,12 +328,14 @@ func (c *FSContext) SockAccept(sockFD int32, nonblock bool) (int32, sys.Errno) {
return 0, errno
}
fe := &FileEntry{File: fsapi.Adapt(conn)}
fe := &FileEntry{File: conn}
if nonblock {
if errno = fe.File.SetNonblock(true); errno != 0 {
_ = conn.Close()
return 0, errno
if pf, ok := fe.File.(sys.PollableFile); ok {
if errno = pf.SetNonblock(true); errno != 0 {
_ = conn.Close()
return 0, errno
}
}
}
@@ -412,7 +413,7 @@ func (c *Context) InitFSContext(
}
for _, tl := range tcpListeners {
c.fsc.openedFiles.Insert(&FileEntry{IsPreopen: true, File: fsapi.Adapt(sysfs.NewTCPListenerFile(tl))})
c.fsc.openedFiles.Insert(&FileEntry{IsPreopen: true, File: sysfs.NewTCPListenerFile(tl)})
}
return nil
}
-16
View File
@@ -2,7 +2,6 @@ package sys
import (
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/sys"
)
@@ -134,18 +133,3 @@ func (d *lazyDir) Close() experimentalsys.Errno {
}
return f.Close()
}
// IsNonblock implements the same method as documented on fsapi.File
func (d *lazyDir) IsNonblock() bool {
return false
}
// SetNonblock implements the same method as documented on fsapi.File
func (d *lazyDir) SetNonblock(bool) experimentalsys.Errno {
return experimentalsys.EISDIR
}
// Poll implements the same method as documented on fsapi.File
func (d *lazyDir) Poll(fsapi.Pflag, int32) (ready bool, errno experimentalsys.Errno) {
return false, experimentalsys.ENOSYS
}
+16 -19
View File
@@ -5,7 +5,6 @@ import (
"os"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/internal/sysfs"
"github.com/tetratelabs/wazero/sys"
)
@@ -24,6 +23,19 @@ func (f *StdinFile) Read(buf []byte) (int, experimentalsys.Errno) {
return n, experimentalsys.UnwrapOSError(err)
}
// Poll implements the same method as documented on experimentalsys.Pollable
func (f *StdinFile) Poll(flag experimentalsys.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
if p, ok := f.Reader.(experimentalsys.Pollable); ok {
return p.Poll(flag, timeoutMillis)
}
if flag != experimentalsys.POLLIN {
return false, experimentalsys.ENOTSUP
}
return true, 0 // No poll support; assume always ready
}
type writerFile struct {
noopStdoutFile
@@ -48,9 +60,9 @@ func (noopStdinFile) Read([]byte) (int, experimentalsys.Errno) {
return 0, 0 // Always EOF
}
// Poll implements the same method as documented on fsapi.File
func (noopStdinFile) Poll(flag fsapi.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
if flag != fsapi.POLLIN {
// Poll implements the same method as documented on experimentalsys.Pollable
func (noopStdinFile) Poll(flag experimentalsys.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
if flag != experimentalsys.POLLIN {
return false, experimentalsys.ENOTSUP
}
return true, 0 // always ready to read nothing
@@ -84,21 +96,6 @@ func (noopStdioFile) IsDir() (bool, experimentalsys.Errno) {
// Close implements the same method as documented on sys.File
func (noopStdioFile) Close() (errno experimentalsys.Errno) { return }
// IsNonblock implements the same method as documented on fsapi.File
func (noopStdioFile) IsNonblock() bool {
return false
}
// SetNonblock implements the same method as documented on fsapi.File
func (noopStdioFile) SetNonblock(bool) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Poll implements the same method as documented on fsapi.File
func (noopStdioFile) Poll(fsapi.Pflag, int32) (ready bool, errno experimentalsys.Errno) {
return false, experimentalsys.ENOSYS
}
func stdinFileEntry(r io.Reader) (*FileEntry, error) {
if r == nil {
return &FileEntry{Name: "stdin", IsPreopen: true, File: &noopStdinFile{}}, nil
+62 -9
View File
@@ -7,11 +7,10 @@ import (
"time"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/sys"
)
func NewStdioFile(stdin bool, f fs.File) (fsapi.File, error) {
func NewStdioFile(stdin bool, f fs.File) (experimentalsys.File, error) {
// Return constant stat, which has fake times, but keep the underlying
// file mode. Fake times are needed to pass wasi-testsuite.
// https://github.com/WebAssembly/wasi-testsuite/blob/af57727/tests/rust/src/bin/fd_filestat_get.rs#L1-L19
@@ -27,7 +26,7 @@ func NewStdioFile(stdin bool, f fs.File) (fsapi.File, error) {
} else {
flag = experimentalsys.O_WRONLY
}
var file fsapi.File
var file experimentalsys.File
if of, ok := f.(*os.File); ok {
// This is ok because functions that need path aren't used by stdioFile
file = newOsFile("", flag, 0, of)
@@ -63,10 +62,37 @@ func OpenFSFile(fs fs.FS, path string, flag experimentalsys.Oflag, perm fs.FileM
}
type stdioFile struct {
fsapi.File
experimentalsys.File
st sys.Stat_t
}
// IsNonblock implements experimentalsys.PollableFile by forwarding to the
// underlying file if it supports it.
func (f *stdioFile) IsNonblock() bool {
if pf, ok := f.File.(experimentalsys.PollableFile); ok {
return pf.IsNonblock()
}
return false
}
// SetNonblock implements experimentalsys.PollableFile by forwarding to the
// underlying file if it supports it.
func (f *stdioFile) SetNonblock(enable bool) experimentalsys.Errno {
if pf, ok := f.File.(experimentalsys.PollableFile); ok {
return pf.SetNonblock(enable)
}
return experimentalsys.ENOSYS
}
// Poll implements experimentalsys.Pollable by forwarding to the underlying file
// if it supports polling.
func (f *stdioFile) Poll(flag experimentalsys.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
if p, ok := f.File.(experimentalsys.Pollable); ok {
return p.Poll(flag, timeoutMillis)
}
return false, experimentalsys.ENOSYS
}
// SetAppend implements File.SetAppend
func (f *stdioFile) SetAppend(bool) experimentalsys.Errno {
// Ignore for stdio.
@@ -341,18 +367,45 @@ func (f *fsFile) close() experimentalsys.Errno {
return experimentalsys.UnwrapOSError(f.file.Close())
}
// IsNonblock implements the same method as documented on fsapi.File
// nonblocker is a subset of PollableFile for checking non-blocking mode on
// an fs.File. fs.File cannot implement PollableFile due to conflicting Close
// signatures (error vs Errno), so we use this narrower interface.
type nonblocker interface {
IsNonblock() bool
SetNonblock(enable bool) experimentalsys.Errno
}
// IsNonblock implements experimentalsys.PollableFile by forwarding to the
// underlying fs.File if it supports it.
func (f *fsFile) IsNonblock() bool {
if nb, ok := f.file.(nonblocker); ok {
return nb.IsNonblock()
}
return false
}
// SetNonblock implements the same method as documented on fsapi.File
func (f *fsFile) SetNonblock(bool) experimentalsys.Errno {
// SetNonblock implements experimentalsys.PollableFile by forwarding to the
// underlying fs.File if it supports it.
func (f *fsFile) SetNonblock(enable bool) experimentalsys.Errno {
if nb, ok := f.file.(nonblocker); ok {
return nb.SetNonblock(enable)
}
if !enable {
return 0 // disabling nonblock on a file that doesn't support it is a no-op
}
return experimentalsys.ENOSYS
}
// Poll implements the same method as documented on fsapi.File
func (f *fsFile) Poll(fsapi.Pflag, int32) (ready bool, errno experimentalsys.Errno) {
// Poll implements experimentalsys.Pollable by forwarding to the underlying
// fs.File if it supports polling.
//
// Note: fsFile cannot implement PollableFile because fs.File and
// experimentalsys.File have conflicting Close signatures (error vs Errno),
// so no type can satisfy both. Pollable has no such conflict.
func (f *fsFile) Poll(flag experimentalsys.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
if p, ok := f.file.(experimentalsys.Pollable); ok {
return p.Poll(flag, timeoutMillis)
}
return false, experimentalsys.ENOSYS
}
+5 -6
View File
@@ -6,11 +6,10 @@ import (
"os"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/sys"
)
func newOsFile(path string, flag experimentalsys.Oflag, perm fs.FileMode, f *os.File) fsapi.File {
func newOsFile(path string, flag experimentalsys.Oflag, perm fs.FileMode, f *os.File) experimentalsys.File {
// On POSIX, if a file is removed from or added to the directory after the
// most recent call to opendir() or rewinddir(), whether a subsequent call
// to readdir() returns an entry for that file is unspecified.
@@ -153,12 +152,12 @@ func (f *osFile) checkSameFile(osf *os.File) experimentalsys.Errno {
return experimentalsys.ENOENT
}
// IsNonblock implements the same method as documented on fsapi.File
// IsNonblock implements the same method as documented on experimentalsys.PollableFile
func (f *osFile) IsNonblock() bool {
return isNonblock(f)
}
// SetNonblock implements the same method as documented on fsapi.File
// SetNonblock implements the same method as documented on experimentalsys.PollableFile
func (f *osFile) SetNonblock(enable bool) (errno experimentalsys.Errno) {
if enable {
f.flag |= experimentalsys.O_NONBLOCK
@@ -229,8 +228,8 @@ func (f *osFile) Seek(offset int64, whence int) (newOffset int64, errno experime
return
}
// Poll implements the same method as documented on fsapi.File
func (f *osFile) Poll(flag fsapi.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
// Poll implements the same method as documented on experimentalsys.Pollable
func (f *osFile) Poll(flag experimentalsys.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
return poll(f.fd, flag, timeoutMillis)
}
+2 -3
View File
@@ -4,12 +4,11 @@ package sysfs
import (
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
)
// poll implements `Poll` as documented on sys.File via a file descriptor.
func poll(fd uintptr, flag fsapi.Pflag, timeoutMillis int32) (ready bool, errno sys.Errno) {
if flag != fsapi.POLLIN {
func poll(fd uintptr, flag sys.Pflag, timeoutMillis int32) (ready bool, errno sys.Errno) {
if flag != sys.POLLIN {
return false, sys.ENOTSUP
}
fds := []pollFd{newPollFd(fd, _POLLIN, 0)}
+2 -3
View File
@@ -4,10 +4,9 @@ package sysfs
import (
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
)
// poll implements `Poll` as documented on fsapi.File via a file descriptor.
func poll(uintptr, fsapi.Pflag, int32) (bool, sys.Errno) {
// poll implements `Poll` as documented on sys.File via a file descriptor.
func poll(uintptr, sys.Pflag, int32) (bool, sys.Errno) {
return false, sys.ENOSYS
}
+7 -8
View File
@@ -5,7 +5,6 @@ import (
"os"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
socketapi "github.com/tetratelabs/wazero/internal/sock"
"github.com/tetratelabs/wazero/sys"
)
@@ -71,13 +70,13 @@ func (f *tcpListenerFile) Addr() *net.TCPAddr {
return f.tl.Addr().(*net.TCPAddr)
}
// IsNonblock implements the same method as documented on fsapi.File
// IsNonblock implements the same method as documented on experimentalsys.PollableFile
func (f *tcpListenerFile) IsNonblock() bool {
return f.nonblock
}
// Poll implements the same method as documented on fsapi.File
func (f *tcpListenerFile) Poll(flag fsapi.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
// Poll implements the same method as documented on experimentalsys.Pollable
func (f *tcpListenerFile) Poll(flag experimentalsys.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
return false, experimentalsys.ENOSYS
}
@@ -167,7 +166,7 @@ func (f *tcpConnFile) close() experimentalsys.Errno {
return f.Shutdown(socketapi.SHUT_RDWR)
}
// SetNonblock implements the same method as documented on fsapi.File
// SetNonblock implements the same method as documented on experimentalsys.PollableFile
func (f *tcpConnFile) SetNonblock(enabled bool) (errno experimentalsys.Errno) {
f.nonblock = enabled
_, errno = syscallConnControl(f.tc, func(fd uintptr) (int, experimentalsys.Errno) {
@@ -176,12 +175,12 @@ func (f *tcpConnFile) SetNonblock(enabled bool) (errno experimentalsys.Errno) {
return
}
// IsNonblock implements the same method as documented on fsapi.File
// IsNonblock implements the same method as documented on experimentalsys.PollableFile
func (f *tcpConnFile) IsNonblock() bool {
return f.nonblock
}
// Poll implements the same method as documented on fsapi.File
func (f *tcpConnFile) Poll(flag fsapi.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
// Poll implements the same method as documented on experimentalsys.Pollable
func (f *tcpConnFile) Poll(flag experimentalsys.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
return false, experimentalsys.ENOSYS
}
+2 -3
View File
@@ -7,7 +7,6 @@ import (
"syscall"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
socketapi "github.com/tetratelabs/wazero/internal/sock"
)
@@ -15,7 +14,7 @@ import (
func (f *tcpListenerFile) Accept() (socketapi.TCPConn, experimentalsys.Errno) {
// Ensure we have an incoming connection, otherwise return immediately.
if f.nonblock {
if ready, errno := _pollSock(f.tl, fsapi.POLLIN, 0); !ready || errno != 0 {
if ready, errno := _pollSock(f.tl, experimentalsys.POLLIN, 0); !ready || errno != 0 {
return nil, experimentalsys.EAGAIN
}
}
@@ -30,7 +29,7 @@ func (f *tcpListenerFile) Accept() (socketapi.TCPConn, experimentalsys.Errno) {
}
}
// SetNonblock implements the same method as documented on fsapi.File
// SetNonblock implements the same method as documented on experimentalsys.PollableFile
func (f *tcpListenerFile) SetNonblock(enabled bool) (errno experimentalsys.Errno) {
f.nonblock = enabled
_, errno = syscallConnControl(f.tl, func(fd uintptr) (int, experimentalsys.Errno) {
+2 -3
View File
@@ -7,7 +7,6 @@ import (
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
socketapi "github.com/tetratelabs/wazero/internal/sock"
)
@@ -18,9 +17,9 @@ func newTCPListenerFile(tl *net.TCPListener) socketapi.TCPSock {
return newDefaultTCPListenerFile(tl)
}
func _pollSock(conn syscall.Conn, flag fsapi.Pflag, timeoutMillis int32) (bool, sys.Errno) {
func _pollSock(conn syscall.Conn, flag sys.Pflag, timeoutMillis int32) (bool, sys.Errno) {
n, errno := syscallConnControl(conn, func(fd uintptr) (int, sys.Errno) {
if ready, errno := poll(fd, fsapi.POLLIN, 0); !ready || errno != 0 {
if ready, errno := poll(fd, sys.POLLIN, 0); !ready || errno != 0 {
return -1, errno
} else {
return 0, errno
+14 -16
View File
@@ -6,9 +6,7 @@ import (
"net"
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
socketapi "github.com/tetratelabs/wazero/internal/sock"
)
@@ -24,28 +22,28 @@ type unsupportedSockFile struct {
}
// Accept implements the same method as documented on socketapi.TCPSock
func (f *unsupportedSockFile) Accept() (socketapi.TCPConn, sys.Errno) {
return nil, sys.ENOSYS
func (f *unsupportedSockFile) Accept() (socketapi.TCPConn, experimentalsys.Errno) {
return nil, experimentalsys.ENOSYS
}
func _pollSock(conn syscall.Conn, flag fsapi.Pflag, timeoutMillis int32) (bool, sys.Errno) {
return false, sys.ENOTSUP
func _pollSock(conn syscall.Conn, flag experimentalsys.Pflag, timeoutMillis int32) (bool, experimentalsys.Errno) {
return false, experimentalsys.ENOTSUP
}
func setNonblockSocket(fd uintptr, enabled bool) sys.Errno {
return sys.ENOTSUP
func setNonblockSocket(fd uintptr, enabled bool) experimentalsys.Errno {
return experimentalsys.ENOTSUP
}
func readSocket(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOTSUP
func readSocket(fd uintptr, buf []byte) (int, experimentalsys.Errno) {
return -1, experimentalsys.ENOTSUP
}
func writeSocket(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOTSUP
func writeSocket(fd uintptr, buf []byte) (int, experimentalsys.Errno) {
return -1, experimentalsys.ENOTSUP
}
func recvfrom(fd uintptr, buf []byte, flags int32) (n int, errno sys.Errno) {
return -1, sys.ENOTSUP
func recvfrom(fd uintptr, buf []byte, flags int32) (n int, errno experimentalsys.Errno) {
return -1, experimentalsys.ENOTSUP
}
// syscallConnControl extracts a syscall.RawConn from the given syscall.Conn and applies
@@ -54,8 +52,8 @@ func recvfrom(fd uintptr, buf []byte, flags int32) (n int, errno sys.Errno) {
// syscallConnControl streamlines the pattern of extracting the syscall.Rawconn,
// invoking its syscall.RawConn.Control method, then handling properly the errors that may occur
// within fn or returned by syscall.RawConn.Control itself.
func syscallConnControl(conn syscall.Conn, fn func(fd uintptr) (int, experimentalsys.Errno)) (n int, errno sys.Errno) {
return -1, sys.ENOTSUP
func syscallConnControl(conn syscall.Conn, fn func(fd uintptr) (int, experimentalsys.Errno)) (n int, errno experimentalsys.Errno) {
return -1, experimentalsys.ENOTSUP
}
// Accept implements the same method as documented on socketapi.TCPSock
+2 -3
View File
@@ -10,7 +10,6 @@ import (
"golang.org/x/sys/windows"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
socketapi "github.com/tetratelabs/wazero/internal/sock"
)
@@ -70,8 +69,8 @@ func setNonblockSocket(fd uintptr, enabled bool) sys.Errno {
return sys.UnwrapOSError(errno)
}
func _pollSock(conn syscall.Conn, flag fsapi.Pflag, timeoutMillis int32) (bool, sys.Errno) {
if flag != fsapi.POLLIN {
func _pollSock(conn syscall.Conn, flag sys.Pflag, timeoutMillis int32) (bool, sys.Errno) {
if flag != sys.POLLIN {
return false, sys.ENOTSUP
}
n, errno := syscallConnControl(conn, func(fd uintptr) (int, sys.Errno) {
+28 -4
View File
@@ -45,11 +45,22 @@ func decodeCode(r *bytes.Reader, codeSectionStart uint64, ret *wasm.Code) (err e
}
bytesRead += n + 1
switch vt := b; vt {
switch vt := b; wasm.ValueType(vt) {
case wasm.ValueTypeI32, wasm.ValueTypeF32, wasm.ValueTypeI64, wasm.ValueTypeF64,
wasm.ValueTypeFuncref, wasm.ValueTypeExternref, wasm.ValueTypeV128:
wasm.ValueTypeFuncref, wasm.ValueTypeExternref, wasm.ValueTypeV128,
wasm.ValueTypeExnref:
default:
return fmt.Errorf("invalid local type: 0x%x", vt)
switch vt {
case wasm.RefPrefixNullable, wasm.RefPrefixNonNullable:
// Read and skip the heap type.
_, htNum, err := leb128.DecodeInt33AsInt64(r)
if err != nil {
return fmt.Errorf("read local ref heap type: %v", err)
}
bytesRead += htNum
default:
return fmt.Errorf("invalid local type: 0x%x", vt)
}
}
}
@@ -78,8 +89,21 @@ func decodeCode(r *bytes.Reader, codeSectionStart uint64, ret *wasm.Code) (err e
return fmt.Errorf("read type of local: %v", err)
}
var vt wasm.ValueType
switch b {
case wasm.RefPrefixNullable, wasm.RefPrefixNonNullable:
before := r.Len()
vt, err = decodeRefType(r, b == wasm.RefPrefixNullable)
if err != nil {
return err
}
remaining -= int64(before - r.Len())
default:
vt = wasm.ValueType(b)
}
for j := uint32(0); j < num; j++ {
localTypes = append(localTypes, b)
localTypes = append(localTypes, vt)
}
}
+95 -84
View File
@@ -6,100 +6,111 @@ import (
"io"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/ieee754"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/wasm"
)
func decodeConstantExpression(r *bytes.Reader, enabledFeatures api.CoreFeatures, ret *wasm.ConstantExpression) error {
b, err := r.ReadByte()
if err != nil {
return fmt.Errorf("read opcode: %v", err)
}
remainingBeforeData := int64(r.Len())
offsetAtData := r.Size() - remainingBeforeData
opcode := b
switch opcode {
case wasm.OpcodeI32Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
_, _, err = leb128.DecodeInt32(r)
case wasm.OpcodeI64Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
_, _, err = leb128.DecodeInt64(r)
case wasm.OpcodeF32Const:
buf := make([]byte, 4)
if _, err := io.ReadFull(r, buf); err != nil {
return fmt.Errorf("read f32 constant: %v", err)
}
_, err = ieee754.DecodeFloat32(buf)
case wasm.OpcodeF64Const:
buf := make([]byte, 8)
if _, err := io.ReadFull(r, buf); err != nil {
return fmt.Errorf("read f64 constant: %v", err)
}
_, err = ieee754.DecodeFloat64(buf)
case wasm.OpcodeGlobalGet:
_, _, err = leb128.DecodeUint32(r)
case wasm.OpcodeRefNull:
if err := enabledFeatures.RequireEnabled(api.CoreFeatureBulkMemoryOperations); err != nil {
return fmt.Errorf("ref.null is not supported as %w", err)
}
reftype, err := r.ReadByte()
lenAtStart := r.Len()
startPos := r.Size() - int64(lenAtStart)
for {
opcode, err := r.ReadByte()
if err != nil {
return fmt.Errorf("read reference type for ref.null: %w", err)
} else if reftype != wasm.RefTypeFuncref && reftype != wasm.RefTypeExternref {
return fmt.Errorf("invalid type for ref.null: 0x%x", reftype)
return fmt.Errorf("read const expression opcode: %v", err)
}
case wasm.OpcodeRefFunc:
if err := enabledFeatures.RequireEnabled(api.CoreFeatureBulkMemoryOperations); err != nil {
return fmt.Errorf("ref.func is not supported as %w", err)
switch opcode {
case wasm.OpcodeI32Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
_, _, err = leb128.DecodeInt32(r)
case wasm.OpcodeI32Add, wasm.OpcodeI32Sub, wasm.OpcodeI32Mul:
// No immediate to read.
if !enabledFeatures.IsEnabled(experimental.CoreFeaturesExtendedConst) {
return fmt.Errorf("%v is not supported in a constant expression as feature \"extended-const\" is disabled", wasm.InstructionName(opcode))
}
case wasm.OpcodeI64Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
_, _, err = leb128.DecodeInt64(r)
case wasm.OpcodeI64Add, wasm.OpcodeI64Sub, wasm.OpcodeI64Mul:
// No immediate to read.
if !enabledFeatures.IsEnabled(experimental.CoreFeaturesExtendedConst) {
return fmt.Errorf("%v is not supported in a constant expression as feature \"extended-const\" is disabled", wasm.InstructionName(opcode))
}
case wasm.OpcodeF32Const:
buf := make([]byte, 4)
if _, err := io.ReadFull(r, buf); err != nil {
return fmt.Errorf("read f32 constant: %v", err)
}
_, err = ieee754.DecodeFloat32(buf)
case wasm.OpcodeF64Const:
buf := make([]byte, 8)
if _, err := io.ReadFull(r, buf); err != nil {
return fmt.Errorf("read f64 constant: %v", err)
}
_, err = ieee754.DecodeFloat64(buf)
case wasm.OpcodeGlobalGet:
_, _, err = leb128.DecodeUint32(r)
case wasm.OpcodeRefNull:
if err := enabledFeatures.RequireEnabled(api.CoreFeatureBulkMemoryOperations); err != nil {
return fmt.Errorf("ref.null is not supported as %w", err)
}
b, err := r.ReadByte()
reftype := wasm.ValueType(b)
if err != nil {
return fmt.Errorf("read reference type for ref.null: %w", err)
}
switch reftype {
case wasm.RefTypeFuncref, wasm.RefTypeExternref, wasm.ValueTypeExnref:
// Valid abstract heap type.
default:
// Could be a concrete type index; unread the byte and try reading as LEB128.
if err := r.UnreadByte(); err != nil {
return fmt.Errorf("unread byte for ref.null: %w", err)
}
_, _, err = leb128.DecodeUint32(r)
if err != nil {
return fmt.Errorf("invalid type for ref.null: 0x%x", reftype)
}
}
case wasm.OpcodeRefFunc:
if err := enabledFeatures.RequireEnabled(api.CoreFeatureBulkMemoryOperations); err != nil {
return fmt.Errorf("ref.func is not supported as %w", err)
}
// Parsing index.
_, _, err = leb128.DecodeUint32(r)
case wasm.OpcodeVecPrefix:
if err := enabledFeatures.RequireEnabled(api.CoreFeatureSIMD); err != nil {
return fmt.Errorf("vector instructions are not supported as %w", err)
}
opcode, err = r.ReadByte()
if err != nil {
return fmt.Errorf("read vector instruction opcode suffix: %w", err)
}
if opcode != wasm.OpcodeVecV128Const {
return fmt.Errorf("invalid vector opcode for const expression: %#x", opcode)
}
n, err := r.Read(make([]byte, 16))
if err != nil {
return fmt.Errorf("read vector const instruction immediates: %w", err)
} else if n != 16 {
return fmt.Errorf("read vector const instruction immediates: needs 16 bytes but was %d bytes", n)
}
case wasm.OpcodeEnd:
data := make([]byte, lenAtStart-(r.Len()))
if _, err := r.ReadAt(data, startPos); err != nil {
return fmt.Errorf("error re-buffering ConstantExpression.Data: %w", err)
}
ret.Data = data
return nil
default:
return fmt.Errorf("%v for const expression op code: %#x", ErrInvalidByte, opcode)
}
// Parsing index.
_, _, err = leb128.DecodeUint32(r)
case wasm.OpcodeVecPrefix:
if err := enabledFeatures.RequireEnabled(api.CoreFeatureSIMD); err != nil {
return fmt.Errorf("vector instructions are not supported as %w", err)
}
opcode, err = r.ReadByte()
if err != nil {
return fmt.Errorf("read vector instruction opcode suffix: %w", err)
return fmt.Errorf("read value: %v", err)
}
if opcode != wasm.OpcodeVecV128Const {
return fmt.Errorf("invalid vector opcode for const expression: %#x", opcode)
}
remainingBeforeData = int64(r.Len())
offsetAtData = r.Size() - remainingBeforeData
n, err := r.Read(make([]byte, 16))
if err != nil {
return fmt.Errorf("read vector const instruction immediates: %w", err)
} else if n != 16 {
return fmt.Errorf("read vector const instruction immediates: needs 16 bytes but was %d bytes", n)
}
default:
return fmt.Errorf("%v for const expression opt code: %#x", ErrInvalidByte, b)
}
if err != nil {
return fmt.Errorf("read value: %v", err)
}
if b, err = r.ReadByte(); err != nil {
return fmt.Errorf("look for end opcode: %v", err)
}
if b != wasm.OpcodeEnd {
return fmt.Errorf("constant expression has been not terminated")
}
ret.Data = make([]byte, remainingBeforeData-int64(r.Len())-1)
if _, err = r.ReadAt(ret.Data, offsetAtData); err != nil {
return fmt.Errorf("error re-buffering ConstantExpression.Data")
}
ret.Opcode = opcode
return nil
}
+16 -6
View File
@@ -8,6 +8,7 @@ import (
"io"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wasmdebug"
@@ -113,7 +114,7 @@ func DecodeModule(
case wasm.SectionIDType:
m.TypeSection, err = decodeTypeSection(enabledFeatures, r)
case wasm.SectionIDImport:
m.ImportSection, m.ImportPerModule, m.ImportFunctionCount, m.ImportGlobalCount, m.ImportMemoryCount, m.ImportTableCount, err = decodeImportSection(r, memSizer, memoryLimitPages, enabledFeatures)
m.ImportSection, m.ImportPerModule, m.ImportFunctionCount, m.ImportGlobalCount, m.ImportMemoryCount, m.ImportTableCount, m.ImportTagCount, err = decodeImportSection(r, memSizer, memoryLimitPages, enabledFeatures)
if err != nil {
return nil, err // avoid re-wrapping the error.
}
@@ -123,6 +124,11 @@ func DecodeModule(
m.TableSection, err = decodeTableSection(r, enabledFeatures)
case wasm.SectionIDMemory:
m.MemorySection, err = decodeMemorySection(r, enabledFeatures, memSizer, memoryLimitPages)
case wasm.SectionIDTag:
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil {
return nil, fmt.Errorf("tag section not supported as %v", err)
}
m.TagSection, err = decodeTagSection(r)
case wasm.SectionIDGlobal:
if m.GlobalSection, err = decodeGlobalSection(r, enabledFeatures); err != nil {
return nil, err // avoid re-wrapping the error.
@@ -176,8 +182,15 @@ func checkSectionOrder(current, previous wasm.SectionID) (byte, bool) {
return previous, true
}
// DataCount was introduced in Wasm 2.0,
// and it's the maximum we support so far.
// Tag section (ID 13) must come after Memory (5) and before Global (6).
if current == wasm.SectionIDTag {
return current, previous <= wasm.SectionIDMemory
}
if previous == wasm.SectionIDTag {
return current, current >= wasm.SectionIDGlobal
}
// DataCount was introduced in Wasm 2.0.
// It must come after Element and before Code.
if current > wasm.SectionIDDataCount {
return current, false
@@ -189,9 +202,6 @@ func checkSectionOrder(current, previous wasm.SectionID) (byte, bool) {
return current, current >= wasm.SectionIDCode
}
// Tag will be introduced in Wasm 3.0.
// It must come after Memory and before Global.
// Otherwise, strictly increasing order.
return current, current > previous
}
+24 -43
View File
@@ -2,7 +2,6 @@ package binary
import (
"bytes"
"errors"
"fmt"
"github.com/tetratelabs/wazero/api"
@@ -21,13 +20,13 @@ func ensureElementKindFuncRef(r *bytes.Reader) error {
return nil
}
func decodeElementInitValueVector(r *bytes.Reader) ([]wasm.Index, error) {
func decodeElementInitValueVector(r *bytes.Reader) ([]wasm.ConstantExpression, error) {
vs, _, err := leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("get size of vector: %w", err)
}
vec := make([]wasm.Index, vs)
vec := make([]wasm.ConstantExpression, vs)
for i := range vec {
u32, _, err := leb128.DecodeUint32(r)
if err != nil {
@@ -37,61 +36,43 @@ func decodeElementInitValueVector(r *bytes.Reader) ([]wasm.Index, error) {
if u32 >= wasm.MaximumFunctionIndex {
return nil, fmt.Errorf("too large function index in Element init: %d", u32)
}
vec[i] = u32
vec[i] = wasm.NewConstantExpressionFromOpcode(wasm.OpcodeRefFunc, leb128.EncodeUint32(u32))
}
return vec, nil
}
func decodeElementConstExprVector(r *bytes.Reader, elemType wasm.RefType, enabledFeatures api.CoreFeatures) ([]wasm.Index, error) {
func decodeElementConstExprVector(r *bytes.Reader, elemType wasm.RefType, enabledFeatures api.CoreFeatures) ([]wasm.ConstantExpression, error) {
vs, _, err := leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("failed to get the size of constexpr vector: %w", err)
}
vec := make([]wasm.Index, vs)
vec := make([]wasm.ConstantExpression, vs)
for i := range vec {
var expr wasm.ConstantExpression
err := decodeConstantExpression(r, enabledFeatures, &expr)
err := decodeConstantExpression(r, enabledFeatures, &vec[i])
if err != nil {
return nil, err
}
switch expr.Opcode {
case wasm.OpcodeRefFunc:
if elemType != wasm.RefTypeFuncref {
return nil, fmt.Errorf("element type mismatch: want %s, but constexpr has funcref", wasm.RefTypeName(elemType))
}
v, _, _ := leb128.LoadUint32(expr.Data)
if v >= wasm.MaximumFunctionIndex {
return nil, fmt.Errorf("too large function index in Element init: %d", v)
}
vec[i] = v
case wasm.OpcodeRefNull:
if elemType != expr.Data[0] {
return nil, fmt.Errorf("element type mismatch: want %s, but constexpr has %s",
wasm.RefTypeName(elemType), wasm.RefTypeName(expr.Data[0]))
}
vec[i] = wasm.ElementInitNullReference
case wasm.OpcodeGlobalGet:
i32, _, _ := leb128.LoadInt32(expr.Data)
// Resolving the reference type from globals is done at instantiation phase. See the comment on
// wasm.elementInitImportedGlobalReferenceType.
vec[i] = wasm.WrapGlobalIndexAsElementInit(wasm.Index(i32))
default:
return nil, fmt.Errorf("const expr must be either ref.null or ref.func but was %s", wasm.InstructionName(expr.Opcode))
}
// Expression will be validated later since we don't yet have globals to resolve the types yet.
}
return vec, nil
}
func decodeElementRefType(r *bytes.Reader) (ret wasm.RefType, err error) {
ret, err = r.ReadByte()
func decodeElementRefType(r *bytes.Reader) (wasm.RefType, error) {
b, err := r.ReadByte()
if err != nil {
err = fmt.Errorf("read element ref type: %w", err)
return
return 0, fmt.Errorf("read element ref type: %w", err)
}
if ret != wasm.RefTypeFuncref && ret != wasm.RefTypeExternref {
return 0, errors.New("ref type must be funcref or externref for element as of WebAssembly 2.0")
switch b {
case wasm.RefPrefixNullable, wasm.RefPrefixNonNullable:
return decodeRefType(r, b == wasm.RefPrefixNullable)
default:
ret := wasm.ValueType(b)
if ret != wasm.RefTypeFuncref && ret != wasm.RefTypeExternref {
return 0, fmt.Errorf("invalid ref type for element: 0x%x", b)
}
return ret, nil
}
return
}
const (
@@ -142,7 +123,7 @@ func decodeElementSegment(r *bytes.Reader, enabledFeatures api.CoreFeatures, ret
}
ret.Mode = wasm.ElementModeActive
ret.Type = wasm.RefTypeFuncref
ret.Type = wasm.RefTypeFuncref.AsNonNullable()
return nil
case elementSegmentPrefixPassiveFuncrefValueVector:
// Prefix 1 requires funcref.
@@ -155,7 +136,7 @@ func decodeElementSegment(r *bytes.Reader, enabledFeatures api.CoreFeatures, ret
return err
}
ret.Mode = wasm.ElementModePassive
ret.Type = wasm.RefTypeFuncref
ret.Type = wasm.RefTypeFuncref.AsNonNullable()
return nil
case elementSegmentPrefixActiveFuncrefValueVectorWithTableIndex:
ret.TableIndex, _, err = leb128.DecodeUint32(r)
@@ -185,7 +166,7 @@ func decodeElementSegment(r *bytes.Reader, enabledFeatures api.CoreFeatures, ret
}
ret.Mode = wasm.ElementModeActive
ret.Type = wasm.RefTypeFuncref
ret.Type = wasm.RefTypeFuncref.AsNonNullable()
return nil
case elementSegmentPrefixDeclarativeFuncrefValueVector:
// Prefix 3 requires funcref.
@@ -196,7 +177,7 @@ func decodeElementSegment(r *bytes.Reader, enabledFeatures api.CoreFeatures, ret
if err != nil {
return err
}
ret.Type = wasm.RefTypeFuncref
ret.Type = wasm.RefTypeFuncref.AsNonNullable()
ret.Mode = wasm.ElementModeDeclarative
return nil
case elementSegmentPrefixActiveFuncrefConstExprVector:
+1 -1
View File
@@ -21,7 +21,7 @@ func decodeExport(r *bytes.Reader, ret *wasm.Export) (err error) {
ret.Type = b
switch ret.Type {
case wasm.ExternTypeFunc, wasm.ExternTypeTable, wasm.ExternTypeMemory, wasm.ExternTypeGlobal:
case wasm.ExternTypeFunc, wasm.ExternTypeTable, wasm.ExternTypeMemory, wasm.ExternTypeGlobal, wasm.ExternTypeTag:
if ret.Index, _, err = leb128.DecodeUint32(r); err != nil {
err = fmt.Errorf("error decoding export index: %w", err)
}
+16
View File
@@ -42,6 +42,22 @@ func decodeImport(
ret.DescMem, err = decodeMemory(r, enabledFeatures, memorySizer, memoryLimitPages)
case wasm.ExternTypeGlobal:
ret.DescGlobal, err = decodeGlobalType(r)
case wasm.ExternTypeTag:
if err = enabledFeatures.RequireEnabled(api.CoreFeatureSIMD << 4); err != nil { // CoreFeaturesExceptionHandling
err = fmt.Errorf("tag imports require exception handling feature: %w", err)
break
}
// Tag import: read attribute byte (must be 0x00) then type index.
var attr byte
attr, err = r.ReadByte()
if err != nil {
break
}
if attr != 0x00 {
err = fmt.Errorf("invalid tag attribute: %#x", attr)
break
}
ret.DescTag, _, err = leb128.DecodeUint32(r)
default:
err = fmt.Errorf("%w: invalid byte for importdesc: %#x", ErrInvalidByte, b)
}
+86 -3
View File
@@ -16,15 +16,70 @@ func decodeTypeSection(enabledFeatures api.CoreFeatures, r *bytes.Reader) ([]was
return nil, fmt.Errorf("get size of vector: %w", err)
}
result := make([]wasm.FunctionType, vs)
var result []wasm.FunctionType
for i := uint32(0); i < vs; i++ {
if err = decodeFunctionType(enabledFeatures, r, &result[i]); err != nil {
// Peek at the leading byte to check for rec group (0x4e, GC proposal).
b, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("read %d-th type: %v", i, err)
}
if b == 0x4e {
// Rec group: contains multiple types.
recCount, _, err := leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("read rec group count: %v", err)
}
startIdx := uint32(len(result))
for j := uint32(0); j < recCount; j++ {
var ft wasm.FunctionType
if err = decodeFunctionType(enabledFeatures, r, &ft); err != nil {
return nil, fmt.Errorf("read %d-th type in rec group: %v", j, err)
}
ft.RecGroupSize = int(recCount)
ft.RecGroupPosition = int(j)
result = append(result, ft)
}
for j := uint32(0); j < recCount; j++ {
if err := validateTypeForwardRefs(&result[startIdx+j], startIdx+recCount); err != nil {
return nil, err
}
}
} else {
// Put back the byte and decode as a regular function type.
if err := r.UnreadByte(); err != nil {
return nil, err
}
var ft wasm.FunctionType
if err = decodeFunctionType(enabledFeatures, r, &ft); err != nil {
return nil, fmt.Errorf("read %d-th type: %v", i, err)
}
if err := validateTypeForwardRefs(&ft, uint32(len(result))); err != nil {
return nil, err
}
result = append(result, ft)
}
}
return result, nil
}
// validateTypeForwardRefs rejects concrete reference types (ref $t) whose type
// index is not yet defined. For standalone types, maxTypeIndex is the count of
// types decoded so far; for rec groups, it is the index after the last member,
// allowing mutual references within the group.
func validateTypeForwardRefs(ft *wasm.FunctionType, maxTypeIndex uint32) error {
for i, vt := range ft.Params {
if vt.IsConcreteRef() && vt.TypeIndex() >= maxTypeIndex {
return fmt.Errorf("unknown type index %d in param[%d]", vt.TypeIndex(), i)
}
}
for i, vt := range ft.Results {
if vt.IsConcreteRef() && vt.TypeIndex() >= maxTypeIndex {
return fmt.Errorf("unknown type index %d in result[%d]", vt.TypeIndex(), i)
}
}
return nil
}
// decodeImportSection decodes the decoded import segments plus the count per wasm.ExternType.
func decodeImportSection(
r *bytes.Reader,
@@ -33,7 +88,7 @@ func decodeImportSection(
enabledFeatures api.CoreFeatures,
) (result []wasm.Import,
perModule map[string][]*wasm.Import,
funcCount, globalCount, memoryCount, tableCount wasm.Index, err error,
funcCount, globalCount, memoryCount, tableCount, tagCount wasm.Index, err error,
) {
vs, _, err := leb128.DecodeUint32(r)
if err != nil {
@@ -61,6 +116,9 @@ func decodeImportSection(
case wasm.ExternTypeTable:
imp.IndexPerType = tableCount
tableCount++
case wasm.ExternTypeTag:
imp.IndexPerType = tagCount
tagCount++
}
perModule[imp.Module] = append(perModule[imp.Module], imp)
}
@@ -216,6 +274,31 @@ func decodeDataSection(r *bytes.Reader, enabledFeatures api.CoreFeatures) ([]was
return result, nil
}
func decodeTagSection(r *bytes.Reader) ([]wasm.Tag, error) {
vs, _, err := leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("get size of vector: %w", err)
}
result := make([]wasm.Tag, vs)
for i := uint32(0); i < vs; i++ {
// Read attribute byte (must be 0x00 per spec).
attr, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("read tag[%d] attribute: %w", i, err)
}
if attr != 0x00 {
return nil, fmt.Errorf("tag[%d] has invalid attribute: %#x", i, attr)
}
// Read type index.
result[i].Type, _, err = leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("read tag[%d] type index: %w", i, err)
}
}
return result, nil
}
func decodeDataCountSection(r *bytes.Reader) (count *uint32, err error) {
v, _, err := leb128.DecodeUint32(r)
if err != nil && err != io.EOF {
+37 -1
View File
@@ -12,11 +12,39 @@ import (
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-table
func decodeTable(r *bytes.Reader, enabledFeatures api.CoreFeatures, ret *wasm.Table) (err error) {
ret.Type, err = r.ReadByte()
b, err := r.ReadByte()
if err != nil {
return fmt.Errorf("read leading byte: %v", err)
}
hasInitExpr := false
if b == 0x40 {
// Table with initializer expression: 0x40 0x00 tabletype expr
reserved, err := r.ReadByte()
if err != nil {
return fmt.Errorf("read reserved byte after 0x40: %v", err)
}
if reserved != 0x00 {
return fmt.Errorf("expected 0x00 after 0x40 table prefix, got 0x%02x", reserved)
}
hasInitExpr = true
b, err = r.ReadByte()
if err != nil {
return fmt.Errorf("read table ref type: %v", err)
}
}
switch b {
case wasm.RefPrefixNullable, wasm.RefPrefixNonNullable:
vt, err := decodeRefType(r, b == wasm.RefPrefixNullable)
if err != nil {
return err
}
ret.Type = vt
default:
ret.Type = wasm.ValueType(b)
}
if ret.Type != wasm.RefTypeFuncref {
if err = enabledFeatures.RequireEnabled(api.CoreFeatureReferenceTypes); err != nil {
return fmt.Errorf("table type funcref is invalid: %w", err)
@@ -39,5 +67,13 @@ func decodeTable(r *bytes.Reader, enabledFeatures api.CoreFeatures, ret *wasm.Ta
if shared {
return fmt.Errorf("tables cannot be marked as shared")
}
if hasInitExpr {
var initExpr wasm.ConstantExpression
if err := decodeConstantExpression(r, enabledFeatures, &initExpr); err != nil {
return fmt.Errorf("read table init expr: %v", err)
}
ret.InitExpr = &initExpr
}
return
}
+49 -11
View File
@@ -16,23 +16,61 @@ func decodeValueTypes(r *bytes.Reader, num uint32) ([]wasm.ValueType, error) {
return nil, nil
}
ret := make([]wasm.ValueType, num)
_, err := io.ReadFull(r, ret)
if err != nil {
return nil, err
}
for _, v := range ret {
switch v {
case wasm.ValueTypeI32, wasm.ValueTypeF32, wasm.ValueTypeI64, wasm.ValueTypeF64,
wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeV128:
ret := make([]wasm.ValueType, 0, num)
for i := uint32(0); i < num; i++ {
b, err := r.ReadByte()
if err != nil {
return nil, err
}
switch b {
case wasm.ValueTypeI32.Kind(), wasm.ValueTypeF32.Kind(), wasm.ValueTypeI64.Kind(), wasm.ValueTypeF64.Kind(),
wasm.ValueTypeExternref.Kind(), wasm.ValueTypeFuncref.Kind(), wasm.ValueTypeV128.Kind(),
wasm.ValueTypeExnref.Kind():
ret = append(ret, wasm.ValueType(b))
case wasm.RefPrefixNullable, wasm.RefPrefixNonNullable:
vt, err := decodeRefType(r, b == wasm.RefPrefixNullable)
if err != nil {
return nil, err
}
ret = append(ret, vt)
default:
return nil, fmt.Errorf("invalid value type: %d", v)
return nil, fmt.Errorf("invalid value type: %d", b)
}
}
return ret, nil
}
// decodeRefType decodes a heap type from r and returns the corresponding
// ValueType with the given nullability. Abstract nullable refs are desugared
// to their short forms:
// - (ref null func) -> funcref
// - (ref null extern) -> externref
// - (ref null exn) -> exnref
func decodeRefType(r *bytes.Reader, nullable bool) (wasm.ValueType, error) {
ht, _, err := leb128.DecodeInt33AsInt64(r)
if err != nil {
return 0, fmt.Errorf("read ref heap type: %w", err)
}
var vt wasm.ValueType
switch ht {
case wasm.HeapTypeFunc:
vt = wasm.ValueTypeFuncref
case wasm.HeapTypeExtern:
vt = wasm.ValueTypeExternref
case wasm.HeapTypeExn:
vt = wasm.ValueTypeExnref
default:
if ht < 0 {
return 0, fmt.Errorf("unknown abstract heap type: %d", ht)
}
vt = wasm.ValueTypeConcreteRef(uint32(ht), nullable)
}
if !nullable {
vt = vt.AsNonNullable()
}
return vt, nil
}
// decodeUTF8 decodes a size prefixed string from the reader, returning it and the count of bytes read.
// contextFormat and contextArgs apply an error format when present
func decodeUTF8(r *bytes.Reader, contextFormat string, contextArgs ...interface{}) (string, uint32, error) {
+259
View File
@@ -0,0 +1,259 @@
package wasm
import (
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/tetratelabs/wazero/internal/leb128"
)
type ConstantExpression struct {
Data []byte
}
func evaluateConstExpr(e *ConstantExpression, globalResolver func(globalIndex Index) (ValueType, uint64, uint64, error), funcRefResolver func(funcIndex Index) (Reference, error)) ([]uint64, ValueType, error) {
var stack []uint64
var typeStack []ValueType
var pc uint64
data := e.Data
for {
if pc >= uint64(len(data)) {
return nil, 0, io.ErrUnexpectedEOF
}
opCode := data[pc]
pc++
switch opCode {
case OpcodeI32Const:
v, n, err := leb128.LoadInt32(data[pc:])
if err != nil {
return nil, 0, fmt.Errorf("read i32: %w", err)
}
pc += n
stack = append(stack, uint64(uint32(v)))
typeStack = append(typeStack, ValueTypeI32)
case OpcodeI64Const:
v, n, err := leb128.LoadInt64(data[pc:])
if err != nil {
return nil, 0, fmt.Errorf("read i64: %w", err)
}
pc += n
stack = append(stack, uint64(v))
typeStack = append(typeStack, ValueTypeI64)
case OpcodeF32Const:
if len(data[pc:]) < 4 {
return nil, 0, io.ErrUnexpectedEOF
}
v := binary.LittleEndian.Uint32(data[pc:])
pc += 4
stack = append(stack, uint64(v))
typeStack = append(typeStack, ValueTypeF32)
case OpcodeF64Const:
if len(data[pc:]) < 8 {
return nil, 0, io.ErrUnexpectedEOF
}
v := binary.LittleEndian.Uint64(data[pc:])
pc += 8
stack = append(stack, uint64(v))
typeStack = append(typeStack, ValueTypeF64)
case OpcodeGlobalGet:
v, n, err := leb128.LoadUint32(data[pc:])
if err != nil {
return nil, 0, fmt.Errorf("read index of global: %w", err)
}
pc += n
typ, lo, hi, err := globalResolver(Index(v))
if err != nil {
return nil, 0, err
}
switch typ {
case ValueTypeV128:
stack = append(stack, lo, hi)
default:
stack = append(stack, lo)
}
typeStack = append(typeStack, typ)
case OpcodeRefNull:
// Reference types are opaque 64bit pointer at runtime.
if pc >= uint64(len(data)) {
return nil, 0, fmt.Errorf("read reference type for ref.null: %w", io.ErrShortBuffer)
}
b := data[pc]
var valType ValueType
switch b {
case RefTypeFuncref.Kind():
valType = RefTypeFuncref
pc++
case RefTypeExternref.Kind():
valType = RefTypeExternref
pc++
case ValueTypeExnref.Kind():
valType = ValueTypeExnref
pc++
default:
// Concrete type index encoded as LEB128.
typeIdx, n, err := leb128.LoadUint32(data[pc:])
if err != nil {
return nil, 0, fmt.Errorf("invalid type for ref.null: 0x%x", b)
}
pc += n
valType = ValueTypeConcreteRef(typeIdx, true)
}
stack = append(stack, 0)
typeStack = append(typeStack, valType)
case OpcodeRefFunc:
v, n, err := leb128.LoadUint32(data[pc:])
if err != nil {
return nil, 0, fmt.Errorf("read i32: %w", err)
}
pc += n
ref, err := funcRefResolver(Index(v))
if err != nil {
return nil, 0, err
}
stack = append(stack, uint64(ref))
typeStack = append(typeStack, ValueTypeFuncref)
case OpcodeVecPrefix:
if data[pc] != OpcodeVecV128Const {
return nil, 0, fmt.Errorf("invalid vector opcode for const expression: %#x", data[pc-1])
}
pc++
if len(data[pc:]) < 16 {
return nil, 0, fmt.Errorf("%s needs 16 bytes but was %d bytes", OpcodeVecV128ConstName, len(data[pc:]))
}
lo := binary.LittleEndian.Uint64(data[pc:])
pc += 8
hi := binary.LittleEndian.Uint64(data[pc:])
pc += 8
stack = append(stack, lo, hi)
typeStack = append(typeStack, ValueTypeV128)
case OpcodeI32Add:
if len(typeStack) < 2 {
return nil, 0, errors.New("stack underflow on i32.add")
}
v1 := typeStack[len(typeStack)-1]
v2 := typeStack[len(typeStack)-2]
if v1 != ValueTypeI32 || v2 != ValueTypeI32 {
return nil, 0, fmt.Errorf("type mismatch on i32.add: %s, %s", ValueTypeName(v2), ValueTypeName(v1))
}
b, a := stack[len(stack)-1], stack[len(stack)-2]
stack = stack[:len(stack)-2]
stack = append(stack, uint64(uint32(a)+uint32(b)))
typeStack = typeStack[:len(typeStack)-2]
typeStack = append(typeStack, ValueTypeI32)
case OpcodeI32Sub:
if len(typeStack) < 2 {
return nil, 0, errors.New("stack underflow on i32.sub")
}
v1 := typeStack[len(typeStack)-1]
v2 := typeStack[len(typeStack)-2]
if v1 != ValueTypeI32 || v2 != ValueTypeI32 {
return nil, 0, fmt.Errorf("type mismatch on i32.sub: %s, %s", ValueTypeName(v2), ValueTypeName(v1))
}
b, a := stack[len(stack)-1], stack[len(stack)-2]
stack = stack[:len(stack)-2]
stack = append(stack, uint64(uint32(a)-uint32(b)))
typeStack = typeStack[:len(typeStack)-2]
typeStack = append(typeStack, ValueTypeI32)
case OpcodeI32Mul:
if len(typeStack) < 2 {
return nil, 0, errors.New("stack underflow on i32.mul")
}
v1 := typeStack[len(typeStack)-1]
v2 := typeStack[len(typeStack)-2]
if v1 != ValueTypeI32 || v2 != ValueTypeI32 {
return nil, 0, fmt.Errorf("type mismatch on i32.mul: %s, %s", ValueTypeName(v2), ValueTypeName(v1))
}
b, a := stack[len(stack)-1], stack[len(stack)-2]
stack = stack[:len(stack)-2]
stack = append(stack, uint64(uint32(a)*uint32(b)))
typeStack = typeStack[:len(typeStack)-2]
typeStack = append(typeStack, ValueTypeI32)
case OpcodeI64Add:
if len(typeStack) < 2 {
return nil, 0, errors.New("stack underflow on i64.add")
}
v1 := typeStack[len(typeStack)-1]
v2 := typeStack[len(typeStack)-2]
if v1 != ValueTypeI64 || v2 != ValueTypeI64 {
return nil, 0, fmt.Errorf("type mismatch on i64.add: %s, %s", ValueTypeName(v2), ValueTypeName(v1))
}
b, a := stack[len(stack)-1], stack[len(stack)-2]
stack = stack[:len(stack)-2]
stack = append(stack, a+b)
typeStack = typeStack[:len(typeStack)-2]
typeStack = append(typeStack, ValueTypeI64)
case OpcodeI64Sub:
if len(typeStack) < 2 {
return nil, 0, errors.New("stack underflow on i64.sub")
}
v1 := typeStack[len(typeStack)-1]
v2 := typeStack[len(typeStack)-2]
if v1 != ValueTypeI64 || v2 != ValueTypeI64 {
return nil, 0, fmt.Errorf("type mismatch on i64.sub: %s, %s", ValueTypeName(v2), ValueTypeName(v1))
}
b, a := stack[len(stack)-1], stack[len(stack)-2]
stack = stack[:len(stack)-2]
stack = append(stack, a-b)
typeStack = typeStack[:len(typeStack)-2]
typeStack = append(typeStack, ValueTypeI64)
case OpcodeI64Mul:
if len(typeStack) < 2 {
return nil, 0, errors.New("stack underflow on i64.mul")
}
v1 := typeStack[len(typeStack)-1]
v2 := typeStack[len(typeStack)-2]
if v1 != ValueTypeI64 || v2 != ValueTypeI64 {
return nil, 0, fmt.Errorf("type mismatch on i64.mul: %s, %s", ValueTypeName(v2), ValueTypeName(v1))
}
b, a := stack[len(stack)-1], stack[len(stack)-2]
stack = stack[:len(stack)-2]
stack = append(stack, a*b)
typeStack = typeStack[:len(typeStack)-2]
typeStack = append(typeStack, ValueTypeI64)
case OpcodeEnd:
if len(typeStack) != 1 {
return nil, 0, errors.New("stack has more than one value at end of constant expression")
}
return stack, typeStack[0], nil
default:
return nil, 0, fmt.Errorf("invalid opcode for const expression: 0x%x", opCode)
}
}
}
func evaluateConstExprInModuleInstance(e *ConstantExpression, m *ModuleInstance) []uint64 {
v, _, _ := evaluateConstExpr(
e,
func(globalIndex Index) (ValueType, uint64, uint64, error) {
g := m.Globals[globalIndex]
return g.Type.ValType, g.Val, g.ValHi, nil
},
func(funcIndex Index) (Reference, error) {
return m.Engine.FunctionInstanceReference(funcIndex), nil
},
)
return v
}
func NewConstantExpressionFromOpcode(
opcode byte, opData []byte,
) ConstantExpression {
data := make([]byte, 0, 3+len(opData)) // 2 for opcode and optional vec prefix, 1 for end
if opcode == OpcodeVecV128Const {
data = append(data, OpcodeVecPrefix)
}
data = append(data, opcode)
data = append(data, opData...)
data = append(data, OpcodeEnd)
return ConstantExpression{Data: data}
}
func NewConstantExpressionFromI32(val int32) ConstantExpression {
return NewConstantExpressionFromOpcode(OpcodeI32Const, leb128.EncodeInt32(val))
}
func NewConstantExpressionFromI64(val int64) ConstantExpression {
return NewConstantExpressionFromOpcode(OpcodeI64Const, leb128.EncodeInt64(val))
}
+2
View File
@@ -45,6 +45,8 @@ func (m *Module) SectionElementCount(sectionID SectionID) uint32 { // element as
return uint32(len(m.CodeSection))
case SectionIDData:
return uint32(len(m.DataSection))
case SectionIDTag:
return uint32(len(m.TagSection))
default:
panic(fmt.Errorf("BUG: unknown section: %d", sectionID))
}
+9
View File
@@ -0,0 +1,9 @@
package wasm
// Exception represents a thrown WebAssembly exception.
type Exception struct {
// Tag is the tag instance that was thrown.
Tag *TagInstance
// Params holds the argument values matching the tag's function type params.
Params []uint64
}
+472 -46
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
"maps"
"slices"
"strconv"
"strings"
@@ -29,9 +30,9 @@ const maximumValuesOnStack = 1 << 27
// Returns an error if the instruction sequence is not valid,
// or potentially it can exceed the maximum number of values on the stack.
func (m *Module) validateFunction(sts *stacks, enabledFeatures api.CoreFeatures, idx Index, functions []Index,
globals []GlobalType, memory *Memory, tables []Table, declaredFunctionIndexes map[Index]struct{}, br *bytes.Reader,
globals []GlobalType, memory *Memory, tables []Table, tags []Index, declaredFunctionIndexes map[Index]struct{}, br *bytes.Reader,
) error {
return m.validateFunctionWithMaxStackValues(sts, enabledFeatures, idx, functions, globals, memory, tables, maximumValuesOnStack, declaredFunctionIndexes, br)
return m.validateFunctionWithMaxStackValues(sts, enabledFeatures, idx, functions, globals, memory, tables, tags, maximumValuesOnStack, declaredFunctionIndexes, br)
}
func readMemArg(pc uint64, body []byte) (align, offset uint32, read uint64, err error) {
@@ -69,6 +70,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
globals []GlobalType,
memory *Memory,
tables []Table,
tags []Index,
maxStackValues int,
declaredFunctionIndexes map[Index]struct{},
br *bytes.Reader,
@@ -98,7 +100,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
} else {
instName = InstructionName(op)
}
fmt.Printf("handling %s, stack=%s, blocks: %v\n", instName, valueTypeStack.stack, controlBlockStack)
fmt.Printf("handling %s, stack=%v, blocks: %v\n", instName, valueTypeStack.stack, controlBlockStack)
}
if len(controlBlockStack.stack) == 0 {
@@ -347,6 +349,9 @@ func (m *Module) validateFunctionWithMaxStackValues(
return fmt.Errorf("invalid local index for %s %d >= %d(=len(locals)+len(parameters))",
OpcodeLocalGetName, index, l)
}
if err := sts.requireLocalInit(index, inputLen, localTypes); err != nil {
return err
}
if index < inputLen {
valueTypeStack.push(functionType.Params[index])
} else {
@@ -367,6 +372,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
if err := valueTypeStack.popAndVerifyType(expType); err != nil {
return err
}
sts.markLocalInit(index, inputLen, localTypes)
case OpcodeLocalTee:
inputLen := uint32(len(functionType.Params))
if l := uint32(len(localTypes)) + inputLen; index >= l {
@@ -383,6 +389,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
return err
}
valueTypeStack.push(expType)
sts.markLocalInit(index, inputLen, localTypes)
case OpcodeGlobalGet:
if index >= uint32(len(globals)) {
return fmt.Errorf("invalid index for %s", OpcodeGlobalGetName)
@@ -503,9 +510,8 @@ func (m *Module) validateFunctionWithMaxStackValues(
return err
}
if actual == valueTypeUnknown {
// Re-assign the expected type to unknown.
defaultLabelType[index] = valueTypeUnknown
} else if actual != exp {
} else if !isRefSubtypeOf(actual, exp) {
return typeMismatchError(true, OpcodeBrTableName, actual, exp, i)
}
}
@@ -530,14 +536,151 @@ func (m *Module) validateFunctionWithMaxStackValues(
return fmt.Errorf("inconsistent block type length for %s at %d; %v (ln=%d) != %v (l=%d)", OpcodeBrTableName, l, defaultLabelType, ln, tableLabelType, l)
}
for i := range defaultLabelType {
if defaultLabelType[i] != valueTypeUnknown && defaultLabelType[i] != tableLabelType[i] {
return fmt.Errorf("incosistent block type for %s at %d", OpcodeBrTableName, l)
if defaultLabelType[i] != valueTypeUnknown && !areRefTypesCompatible(defaultLabelType[i], tableLabelType[i]) {
return fmt.Errorf("inconsistent block type for %s at %d", OpcodeBrTableName, l)
}
}
}
// br_table instruction is stack-polymorphic.
valueTypeStack.unreachable()
} else if op == OpcodeThrow {
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil {
return fmt.Errorf("%s invalid as %v", OpcodeThrowName, err)
}
pc++
tagIndex, num, err := leb128.LoadUint32(body[pc:])
if err != nil {
return fmt.Errorf("read immediate: %v", err)
}
pc += num - 1
if tagIndex >= uint32(len(tags)) {
return fmt.Errorf("invalid tag index for %s: %d", OpcodeThrowName, tagIndex)
}
tagType := &m.TypeSection[tags[tagIndex]]
// Pop values matching the tag's params in reverse order.
for i := len(tagType.Params) - 1; i >= 0; i-- {
if err := valueTypeStack.popAndVerifyType(tagType.Params[i]); err != nil {
return fmt.Errorf("type mismatch on %s operation: %v", OpcodeThrowName, err)
}
}
// throw is stack-polymorphic (never returns).
valueTypeStack.unreachable()
} else if op == OpcodeThrowRef {
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil {
return fmt.Errorf("%s invalid as %v", OpcodeThrowRefName, err)
}
if err := valueTypeStack.popAndVerifyType(ValueTypeExnref); err != nil {
return fmt.Errorf("type mismatch on %s: %v", OpcodeThrowRefName, err)
}
// throw_ref is stack-polymorphic (never returns).
valueTypeStack.unreachable()
} else if op == OpcodeTryTable {
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil {
return fmt.Errorf("%s invalid as %v", OpcodeTryTableName, err)
}
br.Reset(body[pc+1:])
bt, num, err := DecodeBlockType(m.TypeSection, br, enabledFeatures)
if err != nil {
return fmt.Errorf("read block: %w", err)
}
pc += num
// Read catch clause count.
catchCount, catchNum, err := leb128.LoadUint32(body[pc+1:])
if err != nil {
return fmt.Errorf("read catch count: %v", err)
}
pc += catchNum
// Validate each catch clause.
for i := uint32(0); i < catchCount; i++ {
pc++
catchKind := body[pc]
switch catchKind {
case CatchKindCatch, CatchKindCatchRef:
pc++
tagIdx, tagNum, err := leb128.LoadUint32(body[pc:])
if err != nil {
return fmt.Errorf("read catch tag index: %v", err)
}
pc += tagNum - 1
if tagIdx >= uint32(len(tags)) {
return fmt.Errorf("invalid tag index in catch clause: %d", tagIdx)
}
tagType := &m.TypeSection[tags[tagIdx]]
pc++
labelIdx, labelNum, err := leb128.LoadUint32(body[pc:])
if err != nil {
return fmt.Errorf("read catch label index: %v", err)
}
pc += labelNum - 1
if int(labelIdx) >= len(controlBlockStack.stack) {
return fmt.Errorf("invalid label index in catch clause: %d", labelIdx)
}
// Validate that the target label can accept the catch values.
target := &controlBlockStack.stack[len(controlBlockStack.stack)-int(labelIdx)-1]
var expectedTypes []ValueType
if target.op == OpcodeLoop {
expectedTypes = target.blockType.Params
} else {
expectedTypes = target.blockType.Results
}
var catchTypes []ValueType
catchTypes = append(catchTypes, tagType.Params...)
if catchKind == CatchKindCatchRef {
catchTypes = append(catchTypes, ValueTypeExnref.AsNonNullable())
}
if len(catchTypes) != len(expectedTypes) {
return fmt.Errorf("catch clause type mismatch: catch delivers %d values but label expects %d", len(catchTypes), len(expectedTypes))
}
for j := range catchTypes {
if !isRefSubtypeOf(catchTypes[j], expectedTypes[j]) {
return fmt.Errorf("catch clause type mismatch at index %d: %v is not a subtype of %v", j, catchTypes[j], expectedTypes[j])
}
}
case CatchKindCatchAll, CatchKindCatchAllRef:
pc++
labelIdx, labelNum, err := leb128.LoadUint32(body[pc:])
if err != nil {
return fmt.Errorf("read catch_all label index: %v", err)
}
pc += labelNum - 1
if int(labelIdx) >= len(controlBlockStack.stack) {
return fmt.Errorf("invalid label index in catch_all clause: %d", labelIdx)
}
target := &controlBlockStack.stack[len(controlBlockStack.stack)-int(labelIdx)-1]
var expectedTypes []ValueType
if target.op == OpcodeLoop {
expectedTypes = target.blockType.Params
} else {
expectedTypes = target.blockType.Results
}
var catchTypes []ValueType
if catchKind == CatchKindCatchAllRef {
catchTypes = append(catchTypes, ValueTypeExnref.AsNonNullable())
}
if len(catchTypes) != len(expectedTypes) {
return fmt.Errorf("catch_all clause type mismatch: catch delivers %d values but label expects %d", len(catchTypes), len(expectedTypes))
}
for j := range catchTypes {
if !isRefSubtypeOf(catchTypes[j], expectedTypes[j]) {
return fmt.Errorf("catch_all clause type mismatch at index %d", j)
}
}
default:
return fmt.Errorf("invalid catch kind: %#x", catchKind)
}
}
controlBlockStack.push(pc, 0, 0, bt, 0, op)
if err = valueTypeStack.popParams(op, bt.Params, false); err != nil {
return err
}
for _, p := range bt.Params {
valueTypeStack.push(p)
}
valueTypeStack.pushStackLimit(len(bt.Params))
} else if op == OpcodeCall || op == OpcodeTailCallReturnCall {
pc++
index, num, err := leb128.LoadUint32(body[pc:])
@@ -557,20 +700,11 @@ func (m *Module) validateFunctionWithMaxStackValues(
}
funcType := &m.TypeSection[functions[index]]
for i := 0; i < len(funcType.Params); i++ {
if err := valueTypeStack.popAndVerifyType(funcType.Params[len(funcType.Params)-1-i]); err != nil {
return fmt.Errorf("type mismatch on %s operation param type: %v", opcodeName, err)
}
}
for _, exp := range funcType.Results {
valueTypeStack.push(exp)
if err := sts.validateCallSignature(opcodeName, funcType); err != nil {
return err
}
if op == OpcodeTailCallReturnCall {
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesTailCall); err != nil {
return fmt.Errorf("%s invalid as %v", OpcodeTailCallReturnCallName, err)
}
// Same formatting as OpcodeEnd on the outer-most block
if err := valueTypeStack.requireStackValues(false, "", functionType.Results, false); err != nil {
if err := sts.validateReturnCall(OpcodeTailCallReturnCallName, enabledFeatures, funcType, functionType); err != nil {
return err
}
// behaves as a jump.
@@ -619,21 +753,39 @@ func (m *Module) validateFunctionWithMaxStackValues(
return fmt.Errorf("cannot pop the offset in table for %s", opcodeName)
}
funcType := &m.TypeSection[typeIndex]
for i := 0; i < len(funcType.Params); i++ {
if err = valueTypeStack.popAndVerifyType(funcType.Params[len(funcType.Params)-1-i]); err != nil {
return fmt.Errorf("type mismatch on %s operation input type", opcodeName)
}
if err := sts.validateCallSignature(opcodeName, funcType); err != nil {
return err
}
for _, exp := range funcType.Results {
valueTypeStack.push(exp)
}
if op == OpcodeTailCallReturnCallIndirect {
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesTailCall); err != nil {
return fmt.Errorf("%s invalid as %v", OpcodeTailCallReturnCallIndirectName, err)
if err := sts.validateReturnCall(OpcodeTailCallReturnCallIndirectName, enabledFeatures, funcType, functionType); err != nil {
return err
}
// Same formatting as OpcodeEnd on the outer-most block
if err := valueTypeStack.requireStackValues(false, "", functionType.Results, false); err != nil {
// behaves as a jump.
valueTypeStack.unreachable()
}
} else if op == OpcodeCallRef || op == OpcodeReturnCallRef {
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesTypedFunctionReferences); err != nil {
return fmt.Errorf("%s invalid as %v", instructionNames[op], err)
}
pc++
typeIndex, num, err := leb128.LoadUint32(body[pc:])
if err != nil {
return fmt.Errorf("read immediate: %v", err)
}
pc += num - 1
if int(typeIndex) >= len(m.TypeSection) {
return fmt.Errorf("invalid type index at %s: %d", instructionNames[op], typeIndex)
}
// Pop the funcref operand (nullable concrete ref to the type).
if err := valueTypeStack.popAndVerifyType(ValueTypeConcreteRef(typeIndex, true)); err != nil {
return fmt.Errorf("type mismatch on %s operation: %v", instructionNames[op], err)
}
funcType := &m.TypeSection[typeIndex]
if err := sts.validateCallSignature(instructionNames[op], funcType); err != nil {
return err
}
if op == OpcodeReturnCallRef {
if err := sts.validateReturnCall(OpcodeReturnCallRefName, enabledFeatures, funcType, functionType); err != nil {
return err
}
// behaves as a jump.
@@ -859,13 +1011,28 @@ func (m *Module) validateFunctionWithMaxStackValues(
switch op {
case OpcodeRefNull:
pc++
switch reftype := body[pc]; reftype {
switch reftype := body[pc]; ValueType(reftype) {
case ValueTypeExternref:
valueTypeStack.push(ValueTypeExternref)
case ValueTypeFuncref:
valueTypeStack.push(ValueTypeFuncref)
case ValueTypeExnref:
valueTypeStack.push(ValueTypeExnref)
default:
return fmt.Errorf("unknown type for ref.null: 0x%x", reftype)
// Concrete type index encoded as LEB128 u32.
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesTypedFunctionReferences); err != nil {
return fmt.Errorf("ref.null with concrete type invalid as %v", err)
}
br.Reset(body[pc:])
typeIdx, num, err := leb128.DecodeUint32(br)
if err != nil {
return fmt.Errorf("unknown type for ref.null: 0x%x", reftype)
}
if int(typeIdx) >= len(m.TypeSection) {
return fmt.Errorf("unknown type for ref.null: type index %d out of range", typeIdx)
}
pc += uint64(num) - 1
valueTypeStack.push(ValueTypeConcreteRef(typeIdx, true))
}
case OpcodeRefIsNull:
tp, err := valueTypeStack.pop()
@@ -885,8 +1052,119 @@ func (m *Module) validateFunctionWithMaxStackValues(
return fmt.Errorf("undeclared function index %d for ref.func", index)
}
pc += num - 1
valueTypeStack.push(ValueTypeFuncref)
if enabledFeatures.IsEnabled(experimental.CoreFeaturesTypedFunctionReferences) {
valueTypeStack.push(ValueTypeConcreteRef(functions[index], false))
} else {
valueTypeStack.push(ValueTypeFuncref)
}
}
} else if op == OpcodeRefAsNonNull {
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesTypedFunctionReferences); err != nil {
return fmt.Errorf("%s invalid as %v", OpcodeRefAsNonNullName, err)
}
tp, err := valueTypeStack.pop()
if err != nil {
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeRefAsNonNullName, err)
} else if !isReferenceValueType(tp) && tp != valueTypeUnknown {
return fmt.Errorf("type mismatch: expected reference type but was %s for %s", ValueTypeName(tp), OpcodeRefAsNonNullName)
}
if tp == valueTypeUnknown {
valueTypeStack.push(tp)
} else {
valueTypeStack.push(tp.AsNonNullable())
}
} else if op == OpcodeBrOnNull {
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesTypedFunctionReferences); err != nil {
return fmt.Errorf("%s invalid as %v", OpcodeBrOnNullName, err)
}
pc++
index, num, err := leb128.LoadUint32(body[pc:])
if err != nil {
return fmt.Errorf("read immediate: %v", err)
} else if int(index) >= len(controlBlockStack.stack) {
return fmt.Errorf("invalid %s operation: index out of range", OpcodeBrOnNullName)
}
pc += num - 1
// Pop the reference operand.
tp, err := valueTypeStack.pop()
if err != nil {
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeBrOnNullName, err)
} else if !isReferenceValueType(tp) && tp != valueTypeUnknown {
return fmt.Errorf("type mismatch: expected reference type but was %s for %s", ValueTypeName(tp), OpcodeBrOnNullName)
}
// If null, branch with the label's expected values (without the ref).
target := &controlBlockStack.stack[len(controlBlockStack.stack)-int(index)-1]
var targetResultType []ValueType
if target.op == OpcodeLoop {
targetResultType = target.blockType.Params
} else {
targetResultType = target.blockType.Results
}
if err = valueTypeStack.popResults(op, targetResultType, false); err != nil {
return err
}
// Push back the label results (they stay on the stack for the fall-through path).
for _, t := range targetResultType {
valueTypeStack.push(t)
}
// On the fall-through path, the non-null ref is on the stack.
if tp != valueTypeUnknown {
valueTypeStack.push(tp.AsNonNullable())
} else {
valueTypeStack.push(tp)
}
} else if op == OpcodeBrOnNonNull {
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesTypedFunctionReferences); err != nil {
return fmt.Errorf("%s invalid as %v", OpcodeBrOnNonNullName, err)
}
pc++
index, num, err := leb128.LoadUint32(body[pc:])
if err != nil {
return fmt.Errorf("read immediate: %v", err)
} else if int(index) >= len(controlBlockStack.stack) {
return fmt.Errorf("invalid %s operation: index out of range", OpcodeBrOnNonNullName)
}
pc += num - 1
// Pop the reference operand.
tp, err := valueTypeStack.pop()
if err != nil {
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeBrOnNonNullName, err)
} else if !isReferenceValueType(tp) && tp != valueTypeUnknown {
return fmt.Errorf("type mismatch: expected reference type but was %s for %s", ValueTypeName(tp), OpcodeBrOnNonNullName)
}
// If non-null, branch to label with the label's expected values.
// The non-null ref is the LAST value delivered to the label.
target := &controlBlockStack.stack[len(controlBlockStack.stack)-int(index)-1]
var targetResultType []ValueType
if target.op == OpcodeLoop {
targetResultType = target.blockType.Params
} else {
targetResultType = target.blockType.Results
}
// The branch delivers: [label types..., non-null ref].
// So the last type in the target must match the non-null ref.
if len(targetResultType) == 0 {
return fmt.Errorf("type mismatch on %s: label has no results but needs a ref type", OpcodeBrOnNonNullName)
}
nonNullTp := tp
if nonNullTp != valueTypeUnknown {
nonNullTp = tp.AsNonNullable()
}
lastTarget := targetResultType[len(targetResultType)-1]
if nonNullTp != valueTypeUnknown && !isRefSubtypeOf(nonNullTp, lastTarget) {
return fmt.Errorf("type mismatch on %s: ref type %s is not a subtype of label's last result %s",
OpcodeBrOnNonNullName, ValueTypeName(nonNullTp), ValueTypeName(lastTarget))
}
// Pop and verify the remaining label types (all except the last which is the ref).
remaining := targetResultType[:len(targetResultType)-1]
if err = valueTypeStack.popResults(op, remaining, false); err != nil {
return err
}
// Push back the remaining label results for the fall-through path.
for _, t := range remaining {
valueTypeStack.push(t)
}
// On the fall-through (null) path, nothing extra is pushed.
} else if op == OpcodeTableGet || op == OpcodeTableSet {
if err := enabledFeatures.RequireEnabled(api.CoreFeatureReferenceTypes); err != nil {
return fmt.Errorf("%s is invalid as %v", InstructionName(op), err)
@@ -902,7 +1180,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
refType := tables[tableIndex].Type
if op == OpcodeTableGet {
if err := valueTypeStack.popAndVerifyType(api.ValueTypeI32); err != nil {
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the operand for table.get: %v", err)
}
valueTypeStack.push(refType)
@@ -910,7 +1188,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
if err := valueTypeStack.popAndVerifyType(refType); err != nil {
return fmt.Errorf("cannot pop the operand for table.set: %v", err)
}
if err := valueTypeStack.popAndVerifyType(api.ValueTypeI32); err != nil {
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the operand for table.set: %v", err)
}
}
@@ -1039,7 +1317,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
return fmt.Errorf("table of index %d not found", tableIndex)
}
if m.ElementSection[elementIndex].Type != tables[tableIndex].Type {
if !isRefSubtypeOf(m.ElementSection[elementIndex].Type, tables[tableIndex].Type) {
return fmt.Errorf("type mismatch for table.init: element type %s does not match table type %s",
RefTypeName(m.ElementSection[elementIndex].Type),
RefTypeName(tables[tableIndex].Type),
@@ -1086,7 +1364,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
return fmt.Errorf("table of index %d not found", srcTableIndex)
}
if tables[srcTableIndex].Type != tables[dstTableIndex].Type {
if !isRefSubtypeOf(tables[srcTableIndex].Type, tables[dstTableIndex].Type) {
return fmt.Errorf("table type mismatch for table.copy: %s (src) != %s (dst)",
RefTypeName(tables[srcTableIndex].Type), RefTypeName(tables[dstTableIndex].Type))
}
@@ -1446,6 +1724,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
return fmt.Errorf("read block: %w", err)
}
controlBlockStack.push(pc, 0, 0, bt, num, 0)
controlBlockStack.stack[len(controlBlockStack.stack)-1].savedInitLocals = maps.Clone(sts.initLocals)
if err = valueTypeStack.popParams(op, bt.Params, false); err != nil {
return err
}
@@ -1834,6 +2113,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
return fmt.Errorf("read block: %w", err)
}
controlBlockStack.push(pc, 0, 0, bt, num, op)
controlBlockStack.stack[len(controlBlockStack.stack)-1].savedInitLocals = maps.Clone(sts.initLocals)
if err = valueTypeStack.popParams(op, bt.Params, false); err != nil {
return err
}
@@ -1850,6 +2130,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
return fmt.Errorf("read block: %w", err)
}
controlBlockStack.push(pc, 0, 0, bt, num, op)
controlBlockStack.stack[len(controlBlockStack.stack)-1].savedInitLocals = maps.Clone(sts.initLocals)
if err = valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the operand for 'if': %v", err)
}
@@ -1873,6 +2154,8 @@ func (m *Module) validateFunctionWithMaxStackValues(
if err := valueTypeStack.popResults(OpcodeIf, bl.blockType.Results, true); err != nil {
return err
}
// Restore init locals to the state at if-entry for the else branch.
sts.initLocals = maps.Clone(bl.savedInitLocals)
// Before entering instructions inside else, we pop all the values pushed by then block.
valueTypeStack.resetAtStackLimit()
// Plus we have to push any block params again.
@@ -1882,6 +2165,10 @@ func (m *Module) validateFunctionWithMaxStackValues(
} else if op == OpcodeEnd {
bl := controlBlockStack.pop()
bl.endAt = pc
// Restore init locals to the state at block entry (skip for outermost function block).
if len(controlBlockStack.stack) > 0 {
sts.initLocals = bl.savedInitLocals
}
// OpcodeEnd can end a block or the function itself. Check to see what it is:
@@ -1889,7 +2176,7 @@ func (m *Module) validateFunctionWithMaxStackValues(
if ifMissingElse {
// If this is the end of block without else, the number of block's results and params must be same.
// Otherwise, the value stack would result in the inconsistent state at runtime.
if !bytes.Equal(bl.blockType.Results, bl.blockType.Params) {
if !slices.Equal(bl.blockType.Results, bl.blockType.Params) {
return typeCountError(false, OpcodeElseName, bl.blockType.Params, bl.blockType.Results)
}
// -1 skips else, to handle if block without else properly.
@@ -1952,20 +2239,50 @@ func (m *Module) validateFunctionWithMaxStackValues(
return fmt.Errorf("too many type immediates for %s", InstructionName(op))
}
pc++
tp := body[pc]
if tp != ValueTypeI32 && tp != ValueTypeI64 && tp != ValueTypeF32 && tp != ValueTypeF64 &&
tp != api.ValueTypeExternref && tp != ValueTypeFuncref && tp != ValueTypeV128 {
return fmt.Errorf("invalid type %s for %s", ValueTypeName(tp), OpcodeTypedSelectName)
b := body[pc]
switch b {
case RefPrefixNullable, RefPrefixNonNullable:
br.Reset(body[pc+1:])
ht, num, err := leb128.DecodeInt33AsInt64(br)
if err != nil {
return fmt.Errorf("read heap type for %s: %v", OpcodeTypedSelectName, err)
}
pc += uint64(num)
switch ht {
case HeapTypeFunc:
// ok
case HeapTypeExtern:
// ok
case HeapTypeExn:
// ok
default:
if ht < 0 {
return fmt.Errorf("invalid heap type for %s: %d", OpcodeTypedSelectName, ht)
}
if int(ht) >= len(m.TypeSection) {
return fmt.Errorf("unknown type for %s: type index %d out of range", OpcodeTypedSelectName, ht)
}
}
default:
tp := ValueType(b)
if tp != ValueTypeI32 && tp != ValueTypeI64 && tp != ValueTypeF32 && tp != ValueTypeF64 &&
tp != ValueTypeExternref && tp != ValueTypeFuncref && tp != ValueTypeV128 {
return fmt.Errorf("invalid type %s for %s", ValueTypeName(tp), OpcodeTypedSelectName)
}
}
} else if isReferenceValueType(v1) || isReferenceValueType(v2) {
return fmt.Errorf("reference types cannot be used for non typed select instruction")
}
if v1 != v2 && v1 != valueTypeUnknown && v2 != valueTypeUnknown {
if v1 != valueTypeUnknown && v2 != valueTypeUnknown && !areRefTypesCompatible(v1, v2) {
return fmt.Errorf("type mismatch on 1st and 2nd select operands")
}
if v1 == valueTypeUnknown {
valueTypeStack.push(v2)
} else if v2 == valueTypeUnknown {
valueTypeStack.push(v1)
} else if isRefSubtypeOf(v1, v2) {
valueTypeStack.push(v2)
} else {
valueTypeStack.push(v1)
}
@@ -1973,6 +2290,10 @@ func (m *Module) validateFunctionWithMaxStackValues(
// unreachable instruction is stack-polymorphic.
valueTypeStack.unreachable()
} else if op == OpcodeNop {
} else if enabledFeatures.IsEnabled(experimental.CoreFeaturesExceptionHandling) &&
(op == OpcodeLegacyTry || op == OpcodeLegacyCatch || op == OpcodeLegacyRethrow ||
op == OpcodeLegacyDelegate || op == OpcodeLegacyCatchAll) {
return fmt.Errorf("legacy exception handling instruction 0x%x not supported; recompile with wasm-opt --translate-to-exnref", op)
} else {
return fmt.Errorf("invalid instruction 0x%x", op)
}
@@ -2047,6 +2368,8 @@ type stacks struct {
cs controlBlockStack
// ls is the label slice that is reused for each br_table instruction.
ls []uint32
// initLocals tracks which non-nullable ref locals have been initialized.
initLocals map[uint32]struct{}
}
func (sts *stacks) reset(functionType *FunctionType) {
@@ -2057,12 +2380,64 @@ func (sts *stacks) reset(functionType *FunctionType) {
sts.cs.stack = sts.cs.stack[:0]
sts.cs.stack = append(sts.cs.stack, controlBlock{blockType: functionType})
sts.ls = sts.ls[:0]
clear(sts.initLocals)
if sts.initLocals == nil {
sts.initLocals = make(map[uint32]struct{})
}
}
func (sts *stacks) validateCallSignature(opName string, funcType *FunctionType) error {
for i := len(funcType.Params) - 1; i >= 0; i-- {
if err := sts.vs.popAndVerifyType(funcType.Params[i]); err != nil {
return fmt.Errorf("type mismatch on %s operation param type: %v", opName, err)
}
}
for _, exp := range funcType.Results {
sts.vs.push(exp)
}
return nil
}
func (sts *stacks) validateReturnCall(
opName string,
enabledFeatures api.CoreFeatures,
calleeFuncType, callerFuncType *FunctionType,
) error {
if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesTailCall); err != nil {
return fmt.Errorf("%s invalid as %v", opName, err)
}
if len(calleeFuncType.Results) != len(callerFuncType.Results) {
return fmt.Errorf("type mismatch on %s: caller returns %d values but callee returns %d",
opName, len(callerFuncType.Results), len(calleeFuncType.Results))
}
return sts.vs.requireStackValues(false, "", callerFuncType.Results, false)
}
type controlBlockStack struct {
stack []controlBlock
}
func (sts *stacks) requireLocalInit(index, inputLen uint32, localTypes []ValueType) error {
if index >= inputLen {
lt := localTypes[index-inputLen]
if lt.IsRef() && !lt.IsNullable() {
if _, ok := sts.initLocals[index]; !ok {
return fmt.Errorf("uninitialized local %d", index)
}
}
}
return nil
}
func (sts *stacks) markLocalInit(index, inputLen uint32, localTypes []ValueType) {
if index >= inputLen {
lt := localTypes[index-inputLen]
if lt.IsRef() && !lt.IsNullable() {
sts.initLocals[index] = struct{}{}
}
}
}
func (s *controlBlockStack) pop() *controlBlock {
tail := len(s.stack) - 1
ret := &s.stack[tail]
@@ -2125,7 +2500,7 @@ func (s *valueTypeStack) popAndVerifyType(expected ValueType) error {
if !ok {
return fmt.Errorf("%s missing", ValueTypeName(expected))
}
if have != expected && have != valueTypeUnknown && expected != valueTypeUnknown {
if have != valueTypeUnknown && expected != valueTypeUnknown && !isRefSubtypeOf(have, expected) {
return fmt.Errorf("type mismatch: expected %s, but was %s", ValueTypeName(expected), ValueTypeName(have))
}
return nil
@@ -2207,7 +2582,7 @@ func (s *valueTypeStack) requireStackValues(
// Finally, check the types of the values:
for i, v := range s.requireStackValuesTmp {
nextWant := want[countWanted-i-1] // have is in reverse order (stack)
if v != nextWant && v != valueTypeUnknown && nextWant != valueTypeUnknown {
if v != valueTypeUnknown && nextWant != valueTypeUnknown && !isRefSubtypeOf(v, nextWant) {
return typeMismatchError(isParam, context, v, nextWant, i)
}
}
@@ -2304,6 +2679,8 @@ type controlBlock struct {
blockTypeBytes uint64
// op is zero when the outermost block
op Opcode
// savedInitLocals is the set of initialized locals at the time this block was entered.
savedInitLocals map[uint32]struct{}
}
// DecodeBlockType decodes the type index from a positive 33-bit signed integer. Negative numbers indicate up to one
@@ -2336,6 +2713,54 @@ func DecodeBlockType(types []FunctionType, r *bytes.Reader, enabledFeatures api.
ret = blockType_v_funcref
case -17: // 0x6f in original byte = externref
ret = blockType_v_externref
case -23: // 0x69 in original byte = exnref
ret = blockType_v_exnref
case -29: // 0x63 = ref null (nullable)
ht, htNum, err := leb128.DecodeInt33AsInt64(r)
if err != nil {
return nil, 0, fmt.Errorf("read ref heap type in block: %w", err)
}
num += htNum
switch ht {
case HeapTypeExn:
ret = blockType_v_exnref
case HeapTypeFunc:
ret = blockType_v_funcref
case HeapTypeExtern:
ret = blockType_v_externref
default:
if ht < 0 {
return nil, 0, fmt.Errorf("unknown abstract heap type in block: %d", ht)
}
if int64(len(types)) <= ht {
return nil, 0, fmt.Errorf("unknown type")
}
vt := ValueTypeConcreteRef(uint32(ht), true)
ret = &FunctionType{Results: []ValueType{vt}, ResultNumInUint64: 1}
}
case -28: // 0x64 = ref (non-nullable)
ht, htNum, err := leb128.DecodeInt33AsInt64(r)
if err != nil {
return nil, 0, fmt.Errorf("read ref heap type in block: %w", err)
}
num += htNum
switch ht {
case HeapTypeExn:
ret = &FunctionType{Results: []ValueType{ValueTypeExnref.AsNonNullable()}, ResultNumInUint64: 1}
case HeapTypeFunc:
ret = &FunctionType{Results: []ValueType{ValueTypeFuncref.AsNonNullable()}, ResultNumInUint64: 1}
case HeapTypeExtern:
ret = &FunctionType{Results: []ValueType{ValueTypeExternref.AsNonNullable()}, ResultNumInUint64: 1}
default:
if ht < 0 {
return nil, 0, fmt.Errorf("unknown abstract heap type in block: %d", ht)
}
if int64(len(types)) <= ht {
return nil, 0, fmt.Errorf("unknown type")
}
vt := ValueTypeConcreteRef(uint32(ht), false)
ret = &FunctionType{Results: []ValueType{vt}, ResultNumInUint64: 1}
}
default:
if err = enabledFeatures.RequireEnabled(api.CoreFeatureMultiValue); err != nil {
return nil, num, fmt.Errorf("block with function type return invalid as %v", err)
@@ -2358,6 +2783,7 @@ var (
blockType_v_v128 = &FunctionType{Results: []ValueType{ValueTypeV128}, ResultNumInUint64: 2}
blockType_v_funcref = &FunctionType{Results: []ValueType{ValueTypeFuncref}, ResultNumInUint64: 1}
blockType_v_externref = &FunctionType{Results: []ValueType{ValueTypeExternref}, ResultNumInUint64: 1}
blockType_v_exnref = &FunctionType{Results: []ValueType{ValueTypeExnref}, ResultNumInUint64: 1}
)
// SplitCallStack returns the input stack resliced to the count of params and
+26 -4
View File
@@ -168,8 +168,8 @@ func (f *FunctionDefinition) GoFunction() interface{} {
}
// ParamTypes implements api.FunctionDefinition ParamTypes.
func (f *FunctionDefinition) ParamTypes() []ValueType {
return f.Functype.Params
func (f *FunctionDefinition) ParamTypes() []api.ValueType {
return ToApiValueType(f.Functype.Params)
}
// ParamNames implements the same method as documented on api.FunctionDefinition.
@@ -178,11 +178,33 @@ func (f *FunctionDefinition) ParamNames() []string {
}
// ResultTypes implements api.FunctionDefinition ResultTypes.
func (f *FunctionDefinition) ResultTypes() []ValueType {
return f.Functype.Results
func (f *FunctionDefinition) ResultTypes() []api.ValueType {
return ToApiValueType(f.Functype.Results)
}
// ResultNames implements the same method as documented on api.FunctionDefinition.
func (f *FunctionDefinition) ResultNames() []string {
return f.resultNames
}
func ToApiValueType(values []ValueType) []api.ValueType {
if values == nil {
return nil
}
apiValues := make([]api.ValueType, len(values))
for i, v := range values {
apiValues[i] = api.ValueType(v)
}
return apiValues
}
func FromApiValueType(apiValues []api.ValueType) []ValueType {
if apiValues == nil {
return nil
}
values := make([]ValueType, len(apiValues))
for i, v := range apiValues {
values[i] = ValueType(v)
}
return values
}
+2 -2
View File
@@ -13,7 +13,7 @@ type constantGlobal struct {
// Type implements api.Global.
func (g constantGlobal) Type() api.ValueType {
return g.g.Type.ValType
return g.g.Type.ValType.Kind()
}
// Get implements api.Global.
@@ -35,7 +35,7 @@ type mutableGlobal struct {
// Type implements api.Global.
func (g mutableGlobal) Type() api.ValueType {
return g.g.Type.ValType
return g.g.Type.ValType.Kind()
}
// Get implements api.Global.
+3 -3
View File
@@ -1,12 +1,12 @@
package wasm
import (
"bytes"
"context"
"errors"
"fmt"
"math"
"reflect"
"slices"
"github.com/tetratelabs/wazero/api"
)
@@ -47,7 +47,7 @@ func (f *reflectGoModuleFunction) EqualTo(that interface{}) bool {
return false
} else {
// TODO compare reflect pointers
return bytes.Equal(f.params, f2.params) && bytes.Equal(f.results, f2.results)
return slices.Equal(f.params, f2.params) && slices.Equal(f.results, f2.results)
}
}
@@ -67,7 +67,7 @@ func (f *reflectGoFunction) EqualTo(that interface{}) bool {
} else {
// TODO compare reflect pointers
return f.pk == f2.pk &&
bytes.Equal(f.params, f2.params) && bytes.Equal(f.results, f2.results)
slices.Equal(f.params, f2.params) && slices.Equal(f.results, f2.results)
}
}
+77 -6
View File
@@ -20,6 +20,20 @@ const (
// OpcodeElse brackets a sequence of instructions enclosed by an OpcodeIf. A branch instruction on a then label
// breaks out to after the OpcodeEnd on the enclosing OpcodeIf.
OpcodeElse Opcode = 0x05
// Exception handling instructions (toggled with CoreFeaturesExceptionHandling)
// OpcodeThrow throws an exception with the given tag.
OpcodeThrow Opcode = 0x08
// OpcodeThrowRef re-throws the exception referenced by an exnref value.
OpcodeThrowRef Opcode = 0x0a
// Legacy exception handling opcodes (not supported; use wasm-opt --translate-to-exnref)
OpcodeLegacyTry Opcode = 0x06
OpcodeLegacyCatch Opcode = 0x07
OpcodeLegacyRethrow Opcode = 0x09
// OpcodeEnd terminates a control instruction OpcodeBlock, OpcodeLoop or OpcodeIf.
OpcodeEnd Opcode = 0x0b
@@ -48,6 +62,16 @@ const (
OpcodeSelect Opcode = 0x1b
OpcodeTypedSelect Opcode = 0x1c
// Legacy exception handling opcodes (not supported; use wasm-opt --translate-to-exnref)
OpcodeLegacyDelegate Opcode = 0x18
OpcodeLegacyCatchAll Opcode = 0x19
// Exception handling instructions (toggled with CoreFeaturesExceptionHandling)
// OpcodeTryTable brackets a sequence of instructions with catch clauses for exception handling.
OpcodeTryTable Opcode = 0x1f
// variable instructions
OpcodeLocalGet Opcode = 0x20
@@ -250,6 +274,18 @@ const (
// Currently, this is only supported in the constant expression in element segments.
OpcodeRefFunc = 0xd2
// Typed function references instructions (toggled with CoreFeaturesTypedFunctionReferences)
// OpcodeRefAsNonNull pops a nullable reference and traps if null, otherwise pushes
// the non-nullable version.
OpcodeRefAsNonNull Opcode = 0xd4
// OpcodeBrOnNull pops a reference and branches if null, otherwise pushes
// the non-nullable reference.
OpcodeBrOnNull Opcode = 0xd5
// OpcodeBrOnNonNull pops a reference and branches if non-null (carrying the ref),
// otherwise falls through.
OpcodeBrOnNonNull Opcode = 0xd6
// Below are toggled with CoreFeatureSignExtensionOps
// OpcodeI32Extend8S extends a signed 8-bit integer to a 32-bit integer.
@@ -787,6 +823,14 @@ const (
OpcodeTailCallReturnCallIndirect OpcodeTailCall = 0x13
)
// OpcodeCallRef and OpcodeReturnCallRef are typed function references instructions.
//
// These opcodes are toggled with CoreFeaturesTypedFunctionReferences.
const (
OpcodeCallRef Opcode = 0x14
OpcodeReturnCallRef Opcode = 0x15
)
const (
OpcodeUnreachableName = "unreachable"
OpcodeNopName = "nop"
@@ -962,9 +1006,14 @@ const (
OpcodeF32ReinterpretI32Name = "f32.reinterpret_i32"
OpcodeF64ReinterpretI64Name = "f64.reinterpret_i64"
OpcodeRefNullName = "ref.null"
OpcodeRefIsNullName = "ref.is_null"
OpcodeRefFuncName = "ref.func"
OpcodeRefNullName = "ref.null"
OpcodeRefIsNullName = "ref.is_null"
OpcodeRefFuncName = "ref.func"
OpcodeRefAsNonNullName = "ref.as_non_null"
OpcodeBrOnNullName = "br_on_null"
OpcodeBrOnNonNullName = "br_on_non_null"
OpcodeCallRefName = "call_ref"
OpcodeReturnCallRefName = "return_call_ref"
OpcodeTableGetName = "table.get"
OpcodeTableSetName = "table.set"
@@ -989,6 +1038,8 @@ var instructionNames = [256]string{
OpcodeLoop: OpcodeLoopName,
OpcodeIf: OpcodeIfName,
OpcodeElse: OpcodeElseName,
OpcodeThrow: OpcodeThrowName,
OpcodeThrowRef: OpcodeThrowRefName,
OpcodeEnd: OpcodeEndName,
OpcodeBr: OpcodeBrName,
OpcodeBrIf: OpcodeBrIfName,
@@ -996,6 +1047,7 @@ var instructionNames = [256]string{
OpcodeReturn: OpcodeReturnName,
OpcodeCall: OpcodeCallName,
OpcodeCallIndirect: OpcodeCallIndirectName,
OpcodeTryTable: OpcodeTryTableName,
OpcodeDrop: OpcodeDropName,
OpcodeSelect: OpcodeSelectName,
OpcodeTypedSelect: OpcodeTypedSelectName,
@@ -1157,9 +1209,14 @@ var instructionNames = [256]string{
OpcodeF32ReinterpretI32: OpcodeF32ReinterpretI32Name,
OpcodeF64ReinterpretI64: OpcodeF64ReinterpretI64Name,
OpcodeRefNull: OpcodeRefNullName,
OpcodeRefIsNull: OpcodeRefIsNullName,
OpcodeRefFunc: OpcodeRefFuncName,
OpcodeRefNull: OpcodeRefNullName,
OpcodeRefIsNull: OpcodeRefIsNullName,
OpcodeRefFunc: OpcodeRefFuncName,
OpcodeRefAsNonNull: OpcodeRefAsNonNullName,
OpcodeBrOnNull: OpcodeBrOnNullName,
OpcodeBrOnNonNull: OpcodeBrOnNonNullName,
OpcodeCallRef: OpcodeCallRefName,
OpcodeReturnCallRef: OpcodeReturnCallRefName,
OpcodeTableGet: OpcodeTableGetName,
OpcodeTableSet: OpcodeTableSetName,
@@ -1889,3 +1946,17 @@ var tailCallInstructionName = map[OpcodeTailCall]string{
func TailCallInstructionName(oc OpcodeTailCall) (ret string) {
return tailCallInstructionName[oc]
}
// Catch clause kinds used within try_table encoding.
const (
CatchKindCatch byte = 0x00
CatchKindCatchRef byte = 0x01
CatchKindCatchAll byte = 0x02
CatchKindCatchAllRef byte = 0x03
)
const (
OpcodeThrowName = "throw"
OpcodeThrowRefName = "throw_ref"
OpcodeTryTableName = "try_table"
)
+405 -124
View File
@@ -6,15 +6,13 @@ import (
"encoding/binary"
"errors"
"fmt"
"io"
"slices"
"sort"
"strings"
"sync"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/ieee754"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/wasmdebug"
)
@@ -97,6 +95,19 @@ type Module struct {
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0
MemorySection *Memory
// TagSection contains each tag defined in this module for exception handling.
//
// Tag indexes are offset by any imported tags because the tag index begins with imports, followed by
// ones defined in this module.
//
// Note: In the Binary Format, this is SectionIDTag.
//
// See https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md
TagSection []Tag
// ImportTagCount is the cached count of imported tags set during decoding.
ImportTagCount Index
// GlobalSection contains each global defined in this module.
//
// Global indexes are offset by any imported globals because the global index begins with imports, followed by
@@ -227,6 +238,16 @@ func boolToByte(b bool) (ret byte) {
// typeOfFunction returns the wasm.FunctionType for the given function space index or nil.
func (m *Module) typeOfFunction(funcIdx Index) *FunctionType {
typeIdx, ok := m.typeIndexOfFunction(funcIdx)
if !ok {
return nil
}
return &m.TypeSection[typeIdx]
}
// typeIndexOfFunction returns the type section index for the given function
// space index, or false if the index is out of range.
func (m *Module) typeIndexOfFunction(funcIdx Index) (Index, bool) {
typeSectionLength, importedFunctionCount := uint32(len(m.TypeSection)), m.ImportFunctionCount
if funcIdx < importedFunctionCount {
// Imports are not exclusively functions. This is the current function index in the loop.
@@ -238,9 +259,9 @@ func (m *Module) typeOfFunction(funcIdx Index) *FunctionType {
}
if funcIdx == cur {
if imp.DescFunc >= typeSectionLength {
return nil
return 0, false
}
return &m.TypeSection[imp.DescFunc]
return imp.DescFunc, true
}
cur++
}
@@ -248,13 +269,13 @@ func (m *Module) typeOfFunction(funcIdx Index) *FunctionType {
funcSectionIdx := funcIdx - m.ImportFunctionCount
if funcSectionIdx >= uint32(len(m.FunctionSection)) {
return nil
return 0, false
}
typeIdx := m.FunctionSection[funcSectionIdx]
if typeIdx >= typeSectionLength {
return nil
return 0, false
}
return &m.TypeSection[typeIdx]
return typeIdx, true
}
func (m *Module) Validate(enabledFeatures api.CoreFeatures) error {
@@ -263,15 +284,23 @@ func (m *Module) Validate(enabledFeatures api.CoreFeatures) error {
tp.CacheNumInUint64()
}
if err := m.validateConcreteRefTypes(); err != nil {
return err
}
if err := m.validateStartSection(); err != nil {
return err
}
functions, globals, memory, tables, err := m.AllDeclarations()
functions, globals, memory, tables, tags, err := m.AllDeclarations()
if err != nil {
return err
}
if err = m.validateTableInitExprs(globals, uint32(len(functions))); err != nil {
return err
}
if err = m.validateImports(enabledFeatures); err != nil {
return err
}
@@ -284,12 +313,12 @@ func (m *Module) Validate(enabledFeatures api.CoreFeatures) error {
return err
}
if err = m.validateExports(enabledFeatures, functions, globals, memory, tables); err != nil {
if err = m.validateExports(enabledFeatures, functions, globals, memory, tables, tags); err != nil {
return err
}
if m.CodeSection != nil {
if err = m.validateFunctions(enabledFeatures, functions, globals, memory, tables, MaximumFunctionIndex); err != nil {
if err = m.validateFunctions(enabledFeatures, functions, globals, memory, tables, tags, MaximumFunctionIndex); err != nil {
return err
}
} // No need to validate host functions as NewHostModule validates
@@ -301,6 +330,65 @@ func (m *Module) Validate(enabledFeatures api.CoreFeatures) error {
if err = m.validateDataCountSection(); err != nil {
return err
}
if err = m.validateTagSection(); err != nil {
return err
}
return nil
}
func (m *Module) validateConcreteRefTypes() error {
numTypes := uint32(len(m.TypeSection))
for i, g := range m.GlobalSection {
if vt := g.Type.ValType; vt.IsConcreteRef() && vt.TypeIndex() >= numTypes {
return fmt.Errorf("unknown type %d in global[%d]", vt.TypeIndex(), i)
}
}
for i, t := range m.TableSection {
if vt := t.Type; vt.IsConcreteRef() && vt.TypeIndex() >= numTypes {
return fmt.Errorf("unknown type %d in table[%d]", vt.TypeIndex(), i)
}
}
for i, c := range m.CodeSection {
for j, lt := range c.LocalTypes {
if lt.IsConcreteRef() && lt.TypeIndex() >= numTypes {
return fmt.Errorf("unknown type %d in func[%d].local[%d]", lt.TypeIndex(), i, j)
}
}
}
for i, e := range m.ElementSection {
if vt := e.Type; vt.IsConcreteRef() && vt.TypeIndex() >= numTypes {
return fmt.Errorf("unknown type %d in element[%d]", vt.TypeIndex(), i)
}
}
return nil
}
func (m *Module) validateTableInitExprs(globals []GlobalType, numFuncs uint32) error {
importedGlobals := globals[:m.ImportGlobalCount]
for i, t := range m.TableSection {
if !t.Type.IsNullable() && t.InitExpr == nil {
return fmt.Errorf("type mismatch: non-nullable table[%d] requires an init expression", i)
}
if t.InitExpr != nil {
if err := m.validateConstExpression(importedGlobals, numFuncs, t.InitExpr, t.Type); err != nil {
return fmt.Errorf("table[%d] init: %w", i, err)
}
}
}
return nil
}
func (m *Module) validateTagSection() error {
for i, tag := range m.TagSection {
if tag.Type >= uint32(len(m.TypeSection)) {
return fmt.Errorf("tag[%d] type index out of range", i)
}
ft := &m.TypeSection[tag.Type]
if len(ft.Results) > 0 {
return fmt.Errorf("tag[%d] type must have empty results, got %v", i, ft.Results)
}
}
return nil
}
@@ -330,14 +418,14 @@ func (m *Module) validateGlobals(globals []GlobalType, numFuncts, maxGlobals uin
importedGlobals := globals[:m.ImportGlobalCount]
for i := range m.GlobalSection {
g := &m.GlobalSection[i]
if err := validateConstExpression(importedGlobals, numFuncts, &g.Init, g.Type.ValType); err != nil {
if err := m.validateConstExpression(importedGlobals, numFuncts, &g.Init, g.Type.ValType); err != nil {
return err
}
}
return nil
}
func (m *Module) validateFunctions(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table, maximumFunctionIndex uint32) error {
func (m *Module) validateFunctions(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table, tags []Index, maximumFunctionIndex uint32) error {
if uint32(len(functions)) > maximumFunctionIndex {
return fmt.Errorf("too many functions (%d) in a module", len(functions))
}
@@ -353,7 +441,7 @@ func (m *Module) validateFunctions(enabledFeatures api.CoreFeatures, functions [
return fmt.Errorf("code count (%d) != function count (%d)", codeCount, functionCount)
}
declaredFuncIndexes, err := m.declaredFunctionIndexes()
declaredFuncIndexes, err := m.declaredFunctionIndexes(enabledFeatures)
if err != nil {
return err
}
@@ -371,7 +459,7 @@ func (m *Module) validateFunctions(enabledFeatures api.CoreFeatures, functions [
if c.GoFunc != nil {
continue
}
if err = m.validateFunction(vs, enabledFeatures, Index(idx), functions, globals, memory, tables, declaredFuncIndexes, br); err != nil {
if err = m.validateFunction(vs, enabledFeatures, Index(idx), functions, globals, memory, tables, tags, declaredFuncIndexes, br); err != nil {
return fmt.Errorf("invalid %s: %w", m.funcDesc(SectionIDFunction, Index(idx)), err)
}
}
@@ -397,7 +485,7 @@ func (m *Module) validateFunctions(enabledFeatures api.CoreFeatures, functions [
//
// See https://github.com/WebAssembly/reference-types/issues/31
// See https://github.com/WebAssembly/reference-types/issues/76
func (m *Module) declaredFunctionIndexes() (ret map[Index]struct{}, err error) {
func (m *Module) declaredFunctionIndexes(enabledFeatures api.CoreFeatures) (ret map[Index]struct{}, err error) {
ret = map[uint32]struct{}{}
for i := range m.ExportSection {
@@ -409,23 +497,39 @@ func (m *Module) declaredFunctionIndexes() (ret map[Index]struct{}, err error) {
for i := range m.GlobalSection {
g := &m.GlobalSection[i]
if g.Init.Opcode == OpcodeRefFunc {
var index uint32
index, _, err = leb128.LoadUint32(g.Init.Data)
if err != nil {
err = fmt.Errorf("%s[%d] failed to initialize: %w", SectionIDName(SectionIDGlobal), i, err)
return
}
ret[index] = struct{}{}
_, _, initErr := evaluateConstExpr(
&g.Init,
func(globalIndex Index) (ValueType, uint64, uint64, error) {
vt, err := m.resolveConstExprGlobalType(enabledFeatures, SectionIDGlobal, Index(i), globalIndex)
return vt, 0, 0, err
},
func(funcIndex Index) (Reference, error) {
ret[funcIndex] = struct{}{}
return 0, nil
},
)
if initErr != nil {
err = fmt.Errorf("%s[%d] failed to initialize: %w", SectionIDName(SectionIDGlobal), i, initErr)
return
}
}
for i := range m.ElementSection {
elem := &m.ElementSection[i]
for _, index := range elem.Init {
if index != ElementInitNullReference {
ret[index] = struct{}{}
}
for _, initExpr := range elem.Init {
_, _, _ = evaluateConstExpr(
&initExpr,
func(globalIndex Index) (ValueType, uint64, uint64, error) {
vt, err := m.resolveConstExprGlobalType(enabledFeatures, SectionIDElement, Index(i), globalIndex)
return vt, 0, 0, err
},
func(funcIndex Index) (Reference, error) {
ret[funcIndex] = struct{}{}
return 0, nil
},
)
}
}
return
@@ -467,7 +571,7 @@ func (m *Module) validateMemory(memory *Memory, globals []GlobalType, _ api.Core
for i := range m.DataSection {
d := &m.DataSection[i]
if !d.IsPassive() {
if err := validateConstExpression(importedGlobals, 0, &d.OffsetExpression, ValueTypeI32); err != nil {
if err := m.validateConstExpression(importedGlobals, 0, &d.OffsetExpression, ValueTypeI32); err != nil {
return fmt.Errorf("calculate offset: %w", err)
}
}
@@ -493,12 +597,19 @@ func (m *Module) validateImports(enabledFeatures api.CoreFeatures) error {
if err := enabledFeatures.RequireEnabled(api.CoreFeatureMutableGlobal); err != nil {
return fmt.Errorf("invalid import[%q.%q] global: %w", imp.Module, imp.Name, err)
}
case ExternTypeTag:
if int(imp.DescTag) >= len(m.TypeSection) {
return fmt.Errorf("invalid import[%q.%q] tag: type index out of range", imp.Module, imp.Name)
}
if len(m.TypeSection[imp.DescTag].Results) > 0 {
return fmt.Errorf("invalid import[%q.%q] tag: tag types must have no results", imp.Module, imp.Name)
}
}
}
return nil
}
func (m *Module) validateExports(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table) error {
func (m *Module) validateExports(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table, tags []Index) error {
for i := range m.ExportSection {
exp := &m.ExportSection[i]
index := exp.Index
@@ -525,78 +636,43 @@ func (m *Module) validateExports(enabledFeatures api.CoreFeatures, functions []I
if index >= uint32(len(tables)) {
return fmt.Errorf("table for export[%q] out of range", exp.Name)
}
case ExternTypeTag:
if index >= uint32(len(tags)) {
return fmt.Errorf("tag for export[%q] out of range", exp.Name)
}
}
}
return nil
}
func validateConstExpression(globals []GlobalType, numFuncs uint32, expr *ConstantExpression, expectedType ValueType) (err error) {
var actualType ValueType
switch expr.Opcode {
case OpcodeI32Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
_, _, err = leb128.LoadInt32(expr.Data)
if err != nil {
return fmt.Errorf("read i32: %w", err)
}
actualType = ValueTypeI32
case OpcodeI64Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
_, _, err = leb128.LoadInt64(expr.Data)
if err != nil {
return fmt.Errorf("read i64: %w", err)
}
actualType = ValueTypeI64
case OpcodeF32Const:
_, err = ieee754.DecodeFloat32(expr.Data)
if err != nil {
return fmt.Errorf("read f32: %w", err)
}
actualType = ValueTypeF32
case OpcodeF64Const:
_, err = ieee754.DecodeFloat64(expr.Data)
if err != nil {
return fmt.Errorf("read f64: %w", err)
}
actualType = ValueTypeF64
case OpcodeGlobalGet:
id, _, err := leb128.LoadUint32(expr.Data)
if err != nil {
return fmt.Errorf("read index of global: %w", err)
}
if uint32(len(globals)) <= id {
return fmt.Errorf("global index out of range")
}
actualType = globals[id].ValType
case OpcodeRefNull:
if len(expr.Data) == 0 {
return fmt.Errorf("read reference type for ref.null: %w", io.ErrShortBuffer)
}
reftype := expr.Data[0]
if reftype != RefTypeFuncref && reftype != RefTypeExternref {
return fmt.Errorf("invalid type for ref.null: 0x%x", reftype)
}
actualType = reftype
case OpcodeRefFunc:
index, _, err := leb128.LoadUint32(expr.Data)
if err != nil {
return fmt.Errorf("read i32: %w", err)
} else if index >= numFuncs {
return fmt.Errorf("ref.func index out of range [%d] with length %d", index, numFuncs-1)
}
actualType = ValueTypeFuncref
case OpcodeVecV128Const:
if len(expr.Data) != 16 {
return fmt.Errorf("%s needs 16 bytes but was %d bytes", OpcodeVecV128ConstName, len(expr.Data))
}
actualType = ValueTypeV128
default:
return fmt.Errorf("invalid opcode for const expression: 0x%x", expr.Opcode)
func (m *Module) validateConstExpression(globals []GlobalType, numFuncs uint32, expr *ConstantExpression, expectedType ValueType) (err error) {
var lastRefFuncIdx Index
_, typ, err := evaluateConstExpr(
expr,
func(globalIndex Index) (ValueType, uint64, uint64, error) {
if uint32(len(globals)) <= globalIndex {
return 0, 0, 0, fmt.Errorf("global index out of range")
}
return globals[globalIndex].ValType, 0, 0, nil
},
func(funcIndex Index) (Reference, error) {
if funcIndex >= numFuncs {
return 0, fmt.Errorf("ref.func index out of range [%d] with length %d", funcIndex, numFuncs-1)
}
lastRefFuncIdx = funcIndex
return 0, nil
},
)
if err != nil {
return err
}
if actualType != expectedType {
return fmt.Errorf("const expression type mismatch expected %s but got %s",
ValueTypeName(expectedType), ValueTypeName(actualType))
if typ == ValueTypeFuncref {
if typeIndex, ok := m.typeIndexOfFunction(lastRefFuncIdx); ok {
typ = ValueTypeConcreteRef(typeIndex, false)
}
}
if !isRefSubtypeOf(typ, expectedType) {
return fmt.Errorf("const expression type mismatch expected %s but got %s", ValueTypeName(expectedType), ValueTypeName(typ))
}
return nil
}
@@ -609,6 +685,16 @@ func (m *Module) validateDataCountSection() (err error) {
return
}
func (m *ModuleInstance) buildTags(module *Module) {
for i := range module.TagSection {
tag := &module.TagSection[i]
t := &TagInstance{
Type: &module.TypeSection[tag.Type],
}
m.Tags[i+int(module.ImportTagCount)] = t
}
}
func (m *ModuleInstance) buildGlobals(module *Module, funcRefResolver func(funcIndex Index) Reference) {
importedGlobals := m.Globals[:module.ImportGlobalCount]
@@ -627,6 +713,48 @@ func (m *ModuleInstance) buildGlobals(module *Module, funcRefResolver func(funcI
}
}
func (m *Module) resolveConstExprGlobalType(enabledFeatures api.CoreFeatures, sectionID SectionID, sectionIdx Index, idx Index) (ValueType, error) {
if idx < m.ImportGlobalCount {
// Imports are not exclusively globals. This is the current global index in the loop.
cur := uint32(0)
for i := range m.ImportSection {
imp := &m.ImportSection[i]
if imp.Type != ExternTypeGlobal {
continue
}
if idx == cur {
return imp.DescGlobal.ValType, nil
}
cur++
}
// should not happen as idx < ImportGlobalCount
return 0, fmt.Errorf("index %d not found in imported globals", idx)
}
// NOTE: in the <= 2.0 spec, global.get in a constant expression can only refer to imported globals.
// In version 3.0, this restriction is removed, and all globals prior to the current one are allowed.
// To avoid implementing too many flags, this relaxation is gated behind the CoreFeaturesExtendedConst flag,
// which includes other related extensions in constant expressions.
if !enabledFeatures.IsEnabled(experimental.CoreFeaturesExtendedConst) {
return 0, fmt.Errorf("%s[%d] (global.get %d): out of range of imported globals", SectionIDName(sectionID), sectionIdx, idx)
}
idx -= uint32(m.ImportGlobalCount)
// Check that the given global has been initialized.
if sectionIdx == Index(SectionIDGlobal) && idx >= sectionIdx {
return 0, fmt.Errorf("%s[%d] global %d out of range of initialized globals", SectionIDName(sectionID), sectionIdx, idx)
}
// Bounds check:
if idx >= uint32(len(m.GlobalSection)) {
return 0, fmt.Errorf("%s[%d] (global.get %d): out of range of initialized globals", SectionIDName(sectionID), sectionIdx, idx)
}
return m.GlobalSection[idx].Type.ValType, nil
}
func paramNames(localNames IndirectNameMap, funcIdx uint32, paramLen int) []string {
for i := range localNames {
nm := &localNames[i]
@@ -685,6 +813,13 @@ type FunctionType struct {
// ResultsNumInUint64 is the number of uint64 values requires to represent the Wasm result type.
ResultNumInUint64 int
// RecGroupSize is the size of the rec group this type belongs to.
// Standalone types (not in an explicit rec group) have RecGroupSize 1.
RecGroupSize int
// RecGroupPosition is the 0-based position of this type within its rec group.
RecGroupPosition int
}
func (f *FunctionType) CacheNumInUint64() {
@@ -709,7 +844,16 @@ func (f *FunctionType) CacheNumInUint64() {
// EqualsSignature returns true if the function type has the same parameters and results.
func (f *FunctionType) EqualsSignature(params []ValueType, results []ValueType) bool {
return bytes.Equal(f.Params, params) && bytes.Equal(f.Results, results)
return slices.Equal(f.Params, params) && slices.Equal(f.Results, results)
}
// EqualsType returns true if the function types are structurally equal AND
// belong to the same rec group position/size (GC proposal type identity).
func (f *FunctionType) EqualsType(other *FunctionType) bool {
if !f.EqualsSignature(other.Params, other.Results) {
return false
}
return f.RecGroupSize == other.RecGroupSize && f.RecGroupPosition == other.RecGroupPosition
}
// key gets or generates the key for Store.typeIDs. e.g. "i32_v" for one i32 parameter and no (void) result.
@@ -732,6 +876,9 @@ func (f *FunctionType) key() string {
if len(f.Results) == 0 {
ret += "v"
}
if f.RecGroupSize > 1 {
ret += fmt.Sprintf("|rec%d/%d", f.RecGroupPosition, f.RecGroupSize)
}
f.string = ret
return ret
}
@@ -757,6 +904,8 @@ type Import struct {
DescMem *Memory
// DescGlobal is the inlined GlobalType when Type equals ExternTypeGlobal
DescGlobal GlobalType
// DescTag is the type index when Type equals ExternTypeTag
DescTag Index
// IndexPerType has the index of this import per ExternType.
IndexPerType Index
}
@@ -793,6 +942,13 @@ func (m *Memory) Validate(memoryLimitPages uint32) error {
return nil
}
// Tag represents an exception tag defined in the tag section.
// The Type field is an index into the TypeSection; the referenced function type
// must have empty results (tags carry parameters but produce no results).
type Tag struct {
Type Index
}
type GlobalType struct {
ValType ValueType
Mutable bool
@@ -803,11 +959,6 @@ type Global struct {
Init ConstantExpression
}
type ConstantExpression struct {
Opcode Opcode
Data []byte
}
// Export is the binary representation of an export indicated by Type
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-export
type Export struct {
@@ -931,8 +1082,8 @@ type NameMapAssoc struct {
NameMap NameMap
}
// AllDeclarations returns all declarations for functions, globals, memories and tables in a module including imported ones.
func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, memory *Memory, tables []Table, err error) {
// AllDeclarations returns all declarations for functions, globals, memories, tables and tags in a module including imported ones.
func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, memory *Memory, tables []Table, tags []Index, err error) {
for i := range m.ImportSection {
imp := &m.ImportSection[i]
switch imp.Type {
@@ -944,6 +1095,8 @@ func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, mem
memory = imp.DescMem
case ExternTypeTable:
tables = append(tables, imp.DescTable)
case ExternTypeTag:
tags = append(tags, imp.DescTag)
}
}
@@ -952,6 +1105,10 @@ func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, mem
g := &m.GlobalSection[i]
globals = append(globals, g.Type)
}
for i := range m.TagSection {
t := &m.TagSection[i]
tags = append(tags, t.Type)
}
if m.MemorySection != nil {
if memory != nil { // shouldn't be possible due to Validate
err = errors.New("at most one table allowed in module")
@@ -993,6 +1150,11 @@ const (
// See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#data-count-section
// See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/appendix/changes.html#bulk-memory-and-table-instructions
SectionIDDataCount
// SectionIDTag is for exception handling tags.
//
// See https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md
SectionIDTag SectionID = 13
)
// SectionIDName returns the canonical name of a module section.
@@ -1025,37 +1187,151 @@ func SectionIDName(sectionID SectionID) string {
return "data"
case SectionIDDataCount:
return "data_count"
case SectionIDTag:
return "tag"
}
return "unknown"
}
// ValueType is an alias of api.ValueType defined to simplify imports.
type ValueType = api.ValueType
// ValueType represents a WebAssembly value type as a uint64.
//
// Layout:
//
// bits 0-7: kind byte (backward-compatible with api.ValueType)
// bits 8-15: flags (nullability, concrete ref)
// bits 32-63: type index (for concrete refs like (ref $3))
type ValueType uint64
const (
ValueTypeI32 = api.ValueTypeI32
ValueTypeI64 = api.ValueTypeI64
ValueTypeF32 = api.ValueTypeF32
ValueTypeF64 = api.ValueTypeF64
// TODO: ValueTypeV128 is not exposed in the api pkg yet.
ValueTypeV128 ValueType = 0x7b
// TODO: ValueTypeFuncref is not exposed in the api pkg yet.
ValueTypeFuncref ValueType = 0x70
ValueTypeExternref = api.ValueTypeExternref
flagNonNullable ValueType = 1 << 8
flagConcreteRef ValueType = 1 << 9
)
// ValueTypeName is an alias of api.ValueTypeName defined to simplify imports.
func ValueTypeName(t ValueType) string {
if t == ValueTypeFuncref {
return "funcref"
} else if t == ValueTypeV128 {
return "v128"
const (
ValueTypeI32 ValueType = 0x7f
ValueTypeI64 ValueType = 0x7e
ValueTypeF32 ValueType = 0x7d
ValueTypeF64 ValueType = 0x7c
ValueTypeV128 ValueType = 0x7b
ValueTypeFuncref ValueType = 0x70
ValueTypeExternref ValueType = 0x6f
ValueTypeExnref ValueType = 0x69
)
// Kind returns the base type byte (bits 0-7).
func (v ValueType) Kind() byte { return byte(v) }
// IsRef returns true if this is a reference type (including non-nullable variants).
func (v ValueType) IsRef() bool {
k := v.Kind()
return k == ValueTypeFuncref.Kind() || k == ValueTypeExternref.Kind() || k == ValueTypeExnref.Kind() ||
v&flagConcreteRef != 0
}
// IsNullable returns true if this reference type is nullable. Must only be called on ref types.
func (v ValueType) IsNullable() bool { return v.IsRef() && v&flagNonNullable == 0 }
// IsConcreteRef returns true if this is a concrete reference type with a type index.
func (v ValueType) IsConcreteRef() bool { return v&flagConcreteRef != 0 }
// TypeIndex returns the concrete type index (bits 32-63).
func (v ValueType) TypeIndex() uint32 { return uint32(v >> 32) }
// AsNonNullable returns a copy with the non-nullable flag set.
func (v ValueType) AsNonNullable() ValueType { return v | flagNonNullable }
// AsNullable returns a copy with the non-nullable flag cleared.
func (v ValueType) AsNullable() ValueType { return v &^ flagNonNullable }
// ValueTypeConcreteRef creates a concrete reference type with the given type index and nullability.
func ValueTypeConcreteRef(typeIndex uint32, nullable bool) ValueType {
v := ValueTypeFuncref | flagConcreteRef | ValueType(typeIndex)<<32
if !nullable {
v |= flagNonNullable
}
return api.ValueTypeName(t)
return v
}
const (
// RefPrefixNullable is the binary encoding prefix for nullable reference types (ref null <heaptype>).
RefPrefixNullable byte = 0x63
// RefPrefixNonNullable is the binary encoding prefix for non-nullable reference types (ref <heaptype>).
RefPrefixNonNullable byte = 0x64
)
const (
// HeapTypeFunc is the abstract heap type for function references.
HeapTypeFunc int64 = -16
// HeapTypeExtern is the abstract heap type for external references.
HeapTypeExtern int64 = -17
// HeapTypeExn is the abstract heap type for exception references.
HeapTypeExn int64 = -23
)
// ValueTypeName returns the name of a ValueType.
func ValueTypeName(t ValueType) string {
if t.IsConcreteRef() {
if t.IsNullable() {
return fmt.Sprintf("(ref null %d)", t.TypeIndex())
}
return fmt.Sprintf("(ref %d)", t.TypeIndex())
}
switch t.AsNullable() {
case ValueTypeI32:
return "i32"
case ValueTypeI64:
return "i64"
case ValueTypeF32:
return "f32"
case ValueTypeF64:
return "f64"
case ValueTypeV128:
return "v128"
case ValueTypeFuncref:
if !t.IsNullable() {
return "(ref func)"
}
return "funcref"
case ValueTypeExternref:
if !t.IsNullable() {
return "(ref extern)"
}
return "externref"
case ValueTypeExnref:
if !t.IsNullable() {
return "(ref exn)"
}
return "exnref"
}
return "unknown"
}
func isReferenceValueType(vt ValueType) bool {
return vt == ValueTypeExternref || vt == ValueTypeFuncref
return vt.IsRef()
}
// isRefSubtypeOf returns true if actual is a subtype of (or equal to) expected.
// Non-nullable is a subtype of nullable. Concrete function refs are subtypes of funcref.
func isRefSubtypeOf(actual, expected ValueType) bool {
if actual == expected {
return true
}
// Non-nullable is subtype of nullable (same kind/index).
if actual.AsNullable() == expected.AsNullable() && expected.IsNullable() {
return true
}
// Concrete function ref is subtype of (abstract) funcref (nullable or non-nullable).
if actual.IsConcreteRef() && expected.Kind() == ValueTypeFuncref.Kind() {
if !actual.IsNullable() || expected.IsNullable() {
return true
}
}
return false
}
// areRefTypesCompatible returns true if either type is a subtype of the other.
func areRefTypesCompatible(a, b ValueType) bool {
return isRefSubtypeOf(a, b) || isRefSubtypeOf(b, a)
}
// ExternType is an alias of api.ExternType defined to simplify imports.
@@ -1070,9 +1346,14 @@ const (
ExternTypeMemoryName = api.ExternTypeMemoryName
ExternTypeGlobal = api.ExternTypeGlobal
ExternTypeGlobalName = api.ExternTypeGlobalName
ExternTypeTag = ExternType(0x04)
ExternTypeTagName = "tag"
)
// ExternTypeName is an alias of api.ExternTypeName defined to simplify imports.
func ExternTypeName(t ValueType) string {
func ExternTypeName(t ExternType) string {
if t == ExternTypeTag {
return ExternTypeTagName
}
return api.ExternTypeName(t)
}
+144 -97
View File
@@ -2,7 +2,6 @@ package wasm
import (
"context"
"encoding/binary"
"errors"
"fmt"
"sync"
@@ -12,7 +11,6 @@ import (
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/expctxkeys"
"github.com/tetratelabs/wazero/internal/internalapi"
"github.com/tetratelabs/wazero/internal/leb128"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/sys"
)
@@ -79,6 +77,7 @@ type (
Globals []*GlobalInstance
MemoryInstance *MemoryInstance
Tables []*TableInstance
Tags []*TagInstance
// Engine implements function calls for this module.
Engine ModuleEngine
@@ -151,6 +150,13 @@ type (
Index Index
}
// TagInstance represents an instantiated exception handling tag.
// Tags are compared by identity (pointer equality), not structural type equality.
TagInstance struct {
// Type is the function type of this tag (params only; results must be empty).
Type *FunctionType
}
// FunctionTypeID is a uniquely assigned integer for a function type.
// This is wazero specific runtime object and specific to a store,
// and used at runtime to do type-checks on indirect function calls.
@@ -174,21 +180,15 @@ func (m *ModuleInstance) GetFunctionTypeID(t *FunctionType) FunctionTypeID {
func (m *ModuleInstance) buildElementInstances(elements []ElementSegment) {
m.ElementInstances = make([][]Reference, len(elements))
for i, elm := range elements {
if elm.Type == RefTypeFuncref && elm.Mode == ElementModePassive {
if elm.Type.Kind() == RefTypeFuncref.Kind() && elm.Mode == ElementModePassive {
// Only passive elements can be access as element instances.
// See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/syntax/modules.html#element-segments
inits := elm.Init
inst := make([]Reference, len(inits))
m.ElementInstances[i] = inst
for j, idx := range inits {
if index, ok := unwrapElementInitGlobalReference(idx); ok {
global := m.Globals[index]
inst[j] = Reference(global.Val)
} else {
if idx != ElementInitNullReference {
inst[j] = m.Engine.FunctionInstanceReference(idx)
}
}
initExprResults := evaluateConstExprInModuleInstance(&idx, m)
inst[j] = Reference(initExprResults[0])
}
}
}
@@ -202,17 +202,8 @@ func (m *ModuleInstance) applyElements(elems []ElementSegment) {
len(elem.Init) == 0 {
continue
}
var offset uint32
if elem.OffsetExpr.Opcode == OpcodeGlobalGet {
// Ignore error as it's already validated.
globalIdx, _, _ := leb128.LoadUint32(elem.OffsetExpr.Data)
global := m.Globals[globalIdx]
offset = uint32(global.Val)
} else {
// Ignore error as it's already validated.
o, _, _ := leb128.LoadInt32(elem.OffsetExpr.Data)
offset = uint32(o)
}
offsetExprResults := evaluateConstExprInModuleInstance(&elem.OffsetExpr, m)
offset := uint32(offsetExprResults[0])
table := m.Tables[elem.TableIndex]
references := table.References
@@ -233,18 +224,8 @@ func (m *ModuleInstance) applyElements(elems []ElementSegment) {
}
} else {
for i, init := range elem.Init {
if init == ElementInitNullReference {
continue
}
var ref Reference
if index, ok := unwrapElementInitGlobalReference(init); ok {
global := m.Globals[index]
ref = Reference(global.Val)
} else {
ref = m.Engine.FunctionInstanceReference(index)
}
references[offset+uint32(i)] = ref
initExprResults := evaluateConstExprInModuleInstance(&init, m)
references[offset+uint32(i)] = Reference(initExprResults[0])
}
}
}
@@ -256,7 +237,26 @@ func (m *ModuleInstance) validateData(data []DataSegment) (err error) {
for i := range data {
d := &data[i]
if !d.IsPassive() {
offset := int(executeConstExpressionI32(m.Globals, &d.OffsetExpression))
results, typ, err := evaluateConstExpr(
&d.OffsetExpression,
func(globalIndex Index) (ValueType, uint64, uint64, error) {
if globalIndex >= Index(len(m.Globals)) {
return 0, 0, 0, errors.New("global index out of range")
}
g := m.Globals[globalIndex]
return g.Type.ValType, g.Val, g.ValHi, nil
},
func(funcIndex Index) (Reference, error) {
return m.Engine.FunctionInstanceReference(funcIndex), nil
},
)
if err != nil {
return fmt.Errorf("%s[%d] failed to evaluate offset expression: %w", SectionIDName(SectionIDData), i, err)
}
if typ != ValueTypeI32 {
return fmt.Errorf("%s[%d] offset expression must return i32 but was %s", SectionIDName(SectionIDData), i, ValueTypeName(typ))
}
offset := int(results[0])
ceil := offset + len(d.Init)
if offset < 0 || ceil > len(m.MemoryInstance.Buffer) {
return fmt.Errorf("%s[%d]: out of bounds memory access", SectionIDName(SectionIDData), i)
@@ -275,8 +275,9 @@ func (m *ModuleInstance) applyData(data []DataSegment) error {
d := &data[i]
m.DataInstances[i] = d.Init
if !d.IsPassive() {
offset := executeConstExpressionI32(m.Globals, &d.OffsetExpression)
if offset < 0 || int(offset)+len(d.Init) > len(m.MemoryInstance.Buffer) {
offsetExprResults := evaluateConstExprInModuleInstance(&d.OffsetExpression, m)
offset := int(offsetExprResults[0])
if offset < 0 || offset+len(d.Init) > len(m.MemoryInstance.Buffer) {
return fmt.Errorf("%s[%d]: out of bounds memory access", SectionIDName(SectionIDData), i)
}
copy(m.MemoryInstance.Buffer[offset:], d.Init)
@@ -348,6 +349,7 @@ func (s *Store) instantiate(
m.Tables = make([]*TableInstance, int(module.ImportTableCount)+len(module.TableSection))
m.Globals = make([]*GlobalInstance, int(module.ImportGlobalCount)+len(module.GlobalSection))
m.Tags = make([]*TagInstance, int(module.ImportTagCount)+len(module.TagSection))
m.Engine, err = s.Engine.NewModuleEngine(module, m)
if err != nil {
return nil, err
@@ -367,6 +369,7 @@ func (s *Store) instantiate(
allocator, _ := ctx.Value(expctxkeys.MemoryAllocatorKey{}).(experimental.MemoryAllocator)
m.buildGlobals(module, m.Engine.FunctionInstanceReference)
m.buildTags(module)
m.buildMemory(module, allocator)
m.Exports = module.Exports
for _, exp := range m.Exports {
@@ -441,7 +444,15 @@ func (m *ModuleInstance) resolveImports(ctx context.Context, module *Module) (er
expectedType := &module.TypeSection[i.DescFunc]
src := importedModule.Source
actual := src.typeOfFunction(imported.Index)
if !actual.EqualsSignature(expectedType.Params, expectedType.Results) {
matched := false
if m.TypeIDs != nil && importedModule.TypeIDs != nil {
// Use structural type IDs for comparison (handles concrete ref types across modules).
actualTypeIdx, ok := src.typeIndexOfFunction(imported.Index)
matched = ok && importedModule.TypeIDs[actualTypeIdx] == m.TypeIDs[i.DescFunc]
} else {
matched = actual.EqualsSignature(expectedType.Params, expectedType.Results)
}
if !matched {
err = errorInvalidImport(i, fmt.Errorf("signature mismatch: %s != %s", expectedType, actual))
return
}
@@ -503,12 +514,22 @@ func (m *ModuleInstance) resolveImports(ctx context.Context, module *Module) (er
return
}
if expected.ValType != importedGlobal.Type.ValType {
if expected.Mutable && expected.ValType != importedGlobal.Type.ValType ||
!expected.Mutable && !isRefSubtypeOf(importedGlobal.Type.ValType, expected.ValType) {
err = errorInvalidImport(i, fmt.Errorf("value type mismatch: %s != %s",
ValueTypeName(expected.ValType), ValueTypeName(importedGlobal.Type.ValType)))
return
}
m.Globals[i.IndexPerType] = importedGlobal
case ExternTypeTag:
expected := &module.TypeSection[i.DescTag]
importedTag := importedModule.Tags[imported.Index]
if !importedTag.Type.EqualsType(expected) {
err = errorInvalidImport(i, fmt.Errorf("tag type mismatch: %s != %s",
expected, importedTag.Type))
return
}
m.Tags[i.IndexPerType] = importedTag
}
}
}
@@ -531,67 +552,27 @@ func errorInvalidImport(i *Import, err error) error {
return fmt.Errorf("import %s[%s.%s]: %w", ExternTypeName(i.Type), i.Module, i.Name, err)
}
// executeConstExpressionI32 executes the ConstantExpression which returns ValueTypeI32.
// The validity of the expression is ensured when calling this function as this is only called
// during instantiation phrase, and the validation happens in compilation (validateConstExpression).
func executeConstExpressionI32(importedGlobals []*GlobalInstance, expr *ConstantExpression) (ret int32) {
switch expr.Opcode {
case OpcodeI32Const:
ret, _, _ = leb128.LoadInt32(expr.Data)
case OpcodeGlobalGet:
id, _, _ := leb128.LoadUint32(expr.Data)
g := importedGlobals[id]
ret = int32(g.Val)
}
return
}
// initialize initializes the value of this global instance given the const expr and imported globals.
// funcRefResolver is called to get the actual funcref (engine specific) from the OpcodeRefFunc const expr.
//
// Global initialization constant expression can only reference the imported globals.
// See the note on https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#constant-expressions%E2%91%A0
func (g *GlobalInstance) initialize(importedGlobals []*GlobalInstance, expr *ConstantExpression, funcRefResolver func(funcIndex Index) Reference) {
switch expr.Opcode {
case OpcodeI32Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
v, _, _ := leb128.LoadInt32(expr.Data)
g.Val = uint64(uint32(v))
case OpcodeI64Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
v, _, _ := leb128.LoadInt64(expr.Data)
g.Val = uint64(v)
case OpcodeF32Const:
g.Val = uint64(binary.LittleEndian.Uint32(expr.Data))
case OpcodeF64Const:
g.Val = binary.LittleEndian.Uint64(expr.Data)
case OpcodeGlobalGet:
id, _, _ := leb128.LoadUint32(expr.Data)
importedG := importedGlobals[id]
switch importedG.Type.ValType {
case ValueTypeI32:
g.Val = uint64(uint32(importedG.Val))
case ValueTypeI64:
g.Val = importedG.Val
case ValueTypeF32:
g.Val = importedG.Val
case ValueTypeF64:
g.Val = importedG.Val
case ValueTypeV128:
g.Val, g.ValHi = importedG.Val, importedG.ValHi
case ValueTypeFuncref, ValueTypeExternref:
g.Val = importedG.Val
}
case OpcodeRefNull:
switch expr.Data[0] {
case ValueTypeExternref, ValueTypeFuncref:
g.Val = 0 // Reference types are opaque 64bit pointer at runtime.
}
case OpcodeRefFunc:
v, _, _ := leb128.LoadUint32(expr.Data)
g.Val = uint64(funcRefResolver(v))
case OpcodeVecV128Const:
g.Val, g.ValHi = binary.LittleEndian.Uint64(expr.Data[0:8]), binary.LittleEndian.Uint64(expr.Data[8:16])
result, _, _ := evaluateConstExpr(
expr,
func(globalIndex Index) (ValueType, uint64, uint64, error) {
g := importedGlobals[globalIndex]
return g.Type.ValType, g.Val, g.ValHi, nil
},
func(funcIndex Index) (Reference, error) {
return funcRefResolver(funcIndex), nil
},
)
switch len(result) {
case 1:
g.Val = result[0]
case 2:
g.Val, g.ValHi = result[0], result[1]
}
}
@@ -628,18 +609,84 @@ func (s *Store) GetFunctionTypeIDs(ts []FunctionType) ([]FunctionTypeID, error)
ret := make([]FunctionTypeID, len(ts))
for i := range ts {
t := &ts[i]
inst, err := s.GetFunctionTypeID(t)
key := structuralTypeKey(t, ret)
id, err := s.getFunctionTypeIDByKey(key)
if err != nil {
return nil, err
}
ret[i] = inst
ret[i] = id
}
return ret, nil
}
// structuralValueTypeName returns a string representation of a ValueType where
// concrete ref type indices are replaced with their FunctionTypeID. This makes
// the name independent of module-local type index numbering.
func structuralValueTypeName(vt ValueType, typeIDs []FunctionTypeID) string {
if vt.IsConcreteRef() {
idx := vt.TypeIndex()
if int(idx) < len(typeIDs) {
if vt.IsNullable() {
return fmt.Sprintf("(ref null tid=%d)", typeIDs[idx])
}
return fmt.Sprintf("(ref tid=%d)", typeIDs[idx])
}
}
return ValueTypeName(vt)
}
// structuralTypeKey returns a string key for a FunctionType that is stable
// across modules. For signatures without concrete ref types it falls back to
// FunctionType.key(). When concrete refs are present, local type indices are
// replaced with their already-assigned FunctionTypeID so that two modules
// defining structurally identical types at different indices produce the same
// key and share a single FunctionTypeID.
func structuralTypeKey(ft *FunctionType, typeIDs []FunctionTypeID) string {
hasConcreteRef := false
for _, p := range ft.Params {
if p.IsConcreteRef() {
hasConcreteRef = true
break
}
}
if !hasConcreteRef {
for _, r := range ft.Results {
if r.IsConcreteRef() {
hasConcreteRef = true
break
}
}
}
if !hasConcreteRef {
return ft.key()
}
var ret string
for _, b := range ft.Params {
ret += structuralValueTypeName(b, typeIDs)
}
if len(ft.Params) == 0 {
ret += "v_"
} else {
ret += "_"
}
for _, b := range ft.Results {
ret += structuralValueTypeName(b, typeIDs)
}
if len(ft.Results) == 0 {
ret += "v"
}
if ft.RecGroupSize > 1 {
ret += fmt.Sprintf("|rec%d/%d", ft.RecGroupPosition, ft.RecGroupSize)
}
return ret
}
func (s *Store) GetFunctionTypeID(t *FunctionType) (FunctionTypeID, error) {
return s.getFunctionTypeIDByKey(t.key())
}
func (s *Store) getFunctionTypeIDByKey(key string) (FunctionTypeID, error) {
s.mux.RLock()
key := t.key()
id, ok := s.typeIDs[key]
s.mux.RUnlock()
if !ok {
+86 -108
View File
@@ -6,18 +6,18 @@ import (
"sync"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/leb128"
)
// Table describes the limits of elements and its type in a table.
type Table struct {
Min uint32
Max *uint32
Type RefType
Min uint32
Max *uint32
Type RefType
InitExpr *ConstantExpression
}
// RefType is either RefTypeFuncref or RefTypeExternref as of WebAssembly core 2.0.
type RefType = byte
// RefType is a reference type used for table elements.
type RefType = ValueType
const (
// RefTypeFuncref represents a reference to a function.
@@ -66,8 +66,8 @@ type ElementSegment struct {
// Followings are set/used regardless of the Mode.
// Init indices are (nullable) table elements where each index is the function index by which the module initialize the table.
Init []Index
// Init expressions are table elements where each expression evaluates to the function index by which the module initialize the table.
Init []ConstantExpression
// Type holds the type of this element segment, which is the RefType in WebAssembly 2.0.
Type RefType
@@ -76,39 +76,6 @@ type ElementSegment struct {
Mode ElementMode
}
const (
// ElementInitNullReference represents the null reference in ElementSegment's Init.
// In Wasm spec, an init item represents either Function's Index or null reference,
// and in wazero, we limit the maximum number of functions available in a module to
// MaximumFunctionIndex. Therefore, it is safe to use 1 << 31 to represent the null
// reference in Element segments.
ElementInitNullReference Index = 1 << 31
// elementInitImportedGlobalReferenceType represents an init item which is resolved via an imported global constexpr.
// The actual function reference stored at Global is only known at instantiation-time, so we set this flag
// to items of ElementSegment.Init at binary decoding, and unwrap this flag at instantiation to resolve the value.
//
// This might collide the init element resolved via ref.func instruction which is resolved with the func index at decoding,
// but in practice, that is not allowed in wazero thanks to our limit MaximumFunctionIndex. Thus, it is safe to set this flag
// in init element to indicate as such.
elementInitImportedGlobalReferenceType Index = 1 << 30
)
// unwrapElementInitGlobalReference takes an item of the init vector of an ElementSegment,
// and returns the Global index if it is supposed to get generated from a global.
// ok is true if the given init item is as such.
func unwrapElementInitGlobalReference(init Index) (_ Index, ok bool) {
if init&elementInitImportedGlobalReferenceType == elementInitImportedGlobalReferenceType {
return init &^ elementInitImportedGlobalReferenceType, true
}
return init, false
}
// WrapGlobalIndexAsElementInit wraps the given index as an init item which is resolved via an imported global value.
// See the comments on elementInitImportedGlobalReferenceType for more details.
func WrapGlobalIndexAsElementInit(init Index) Index {
return init | elementInitImportedGlobalReferenceType
}
// IsActive returns true if the element segment is "active" mode which requires the runtime to initialize table
// with the contents in .Init field.
func (e *ElementSegment) IsActive() bool {
@@ -174,20 +141,39 @@ func (m *Module) validateTable(enabledFeatures api.CoreFeatures, tables []Table,
// Any offset applied is to the element, not the function index: validate here if the funcidx is sound.
for ei, init := range elem.Init {
if init == ElementInitNullReference {
continue
_, initType, err := evaluateConstExpr(
&init,
func(globalIndex Index) (ValueType, uint64, uint64, error) {
if globalIndex >= Index(globalsCount) {
return 0, 0, 0, fmt.Errorf("%s[%d].init[%d] global index %d out of range", SectionIDName(SectionIDElement), idx, ei, globalIndex)
}
vt, err := m.resolveConstExprGlobalType(enabledFeatures, SectionIDElement, idx, globalIndex)
return vt, 0, 0, err
},
func(funcIndex Index) (Reference, error) {
if funcIndex >= Index(funcCount) {
return 0, fmt.Errorf("%s[%d].init[%d] func index %d out of range", SectionIDName(SectionIDElement), idx, ei, funcIndex)
}
return 0, nil
},
)
if err != nil {
return err
}
index, ok := unwrapElementInitGlobalReference(init)
if ok {
if index >= globalsCount {
return fmt.Errorf("%s[%d].init[%d] global index %d out of range", SectionIDName(SectionIDElement), idx, ei, index)
switch elem.Type {
case RefTypeFuncref:
if initType != ValueTypeFuncref {
return fmt.Errorf("%s[%d].init[%d] must be funcref but was %s", SectionIDName(SectionIDElement), idx, ei, ValueTypeName(initType))
}
} else {
if elem.Type == RefTypeExternref {
return fmt.Errorf("%s[%d].init[%d] must be ref.null but was %d", SectionIDName(SectionIDElement), idx, ei, init)
case RefTypeExternref:
if initType != ValueTypeExternref {
return fmt.Errorf("%s[%d].init[%d] must be externref but was %s", SectionIDName(SectionIDElement), idx, ei, ValueTypeName(initType))
}
if index >= funcCount {
return fmt.Errorf("%s[%d].init[%d] func index %d out of range", SectionIDName(SectionIDElement), idx, ei, index)
default:
if !isRefSubtypeOf(initType, elem.Type) && initType != ValueTypeFuncref {
return fmt.Errorf("%s[%d].init[%d] must be %s but was %s",
SectionIDName(SectionIDElement), idx, ei, ValueTypeName(elem.Type), ValueTypeName(initType))
}
}
}
@@ -198,38 +184,49 @@ func (m *Module) validateTable(enabledFeatures api.CoreFeatures, tables []Table,
}
t := tables[elem.TableIndex]
if t.Type != elem.Type {
if !isRefSubtypeOf(elem.Type, t.Type) {
return fmt.Errorf("element type mismatch: table has %s but element has %s",
RefTypeName(t.Type), RefTypeName(elem.Type),
)
}
// global.get needs to be discovered during initialization
oc := elem.OffsetExpr.Opcode
if oc == OpcodeGlobalGet {
globalIdx, _, err := leb128.LoadUint32(elem.OffsetExpr.Data)
if err != nil {
return fmt.Errorf("%s[%d] couldn't read global.get parameter: %w", SectionIDName(SectionIDElement), idx, err)
} else if err = m.verifyImportGlobalI32(SectionIDElement, idx, globalIdx); err != nil {
hasGlobalRef := false
offsetExprResults, offsetExprType, err := evaluateConstExpr(
&elem.OffsetExpr,
func(globalIndex Index) (ValueType, uint64, uint64, error) {
hasGlobalRef = true
if globalIndex >= Index(globalsCount) {
return 0, 0, 0, fmt.Errorf("%s[%d] global index %d out of range", SectionIDName(SectionIDElement), idx, globalIndex)
}
vt, err := m.resolveConstExprGlobalType(enabledFeatures, SectionIDElement, idx, globalIndex)
if err != nil {
return 0, 0, 0, err
}
if vt != ValueTypeI32 {
return 0, 0, 0, fmt.Errorf("%s[%d] (global.get %d): import[%d].global.ValType != i32", SectionIDName(SectionIDElement), idx, globalIndex, i)
}
return ValueTypeI32, 0, 0, nil
},
func(funcIndex Index) (Reference, error) {
return 0, nil
},
)
if err != nil {
return fmt.Errorf("%s[%d] couldn't evaluate offset expression: %w", SectionIDName(SectionIDElement), idx, err)
}
if offsetExprType != ValueTypeI32 {
return fmt.Errorf("%s[%d] offset expression must return i32 but was %s", SectionIDName(SectionIDElement), idx, ValueTypeName(offsetExprType))
}
if !enabledFeatures.IsEnabled(api.CoreFeatureReferenceTypes) && !hasGlobalRef && elem.TableIndex >= importedTableCount {
offset := uint32(offsetExprResults[0])
if err = checkSegmentBounds(t.Min, uint64(initCount)+uint64(offset), idx); err != nil {
return err
}
} else if oc == OpcodeI32Const {
// Per https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L117 we must pass if imported
// table has set its min=0. Per https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L142, we
// have to do fail if module-defined min=0.
if !enabledFeatures.IsEnabled(api.CoreFeatureReferenceTypes) && elem.TableIndex >= importedTableCount {
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
o, _, err := leb128.LoadInt32(elem.OffsetExpr.Data)
if err != nil {
return fmt.Errorf("%s[%d] couldn't read i32.const parameter: %w", SectionIDName(SectionIDElement), idx, err)
}
offset := Index(o)
if err = checkSegmentBounds(t.Min, uint64(initCount)+uint64(offset), idx); err != nil {
return err
}
}
} else {
return fmt.Errorf("%s[%d] has an invalid const expression: %s", SectionIDName(SectionIDElement), idx, InstructionName(oc))
}
}
}
@@ -247,11 +244,20 @@ func (m *ModuleInstance) buildTables(module *Module, skipBoundCheck bool) (err e
idx := module.ImportTableCount
for i := range module.TableSection {
tsec := &module.TableSection[i]
// The module defining the table is the one that sets its Min/Max etc.
m.Tables[idx] = &TableInstance{
t := &TableInstance{
References: make([]Reference, tsec.Min), Min: tsec.Min, Max: tsec.Max,
Type: tsec.Type,
}
if tsec.InitExpr != nil {
initVals := evaluateConstExprInModuleInstance(tsec.InitExpr, m)
if len(initVals) > 0 && initVals[0] != 0 {
initRef := Reference(initVals[0])
for j := range t.References {
t.References[j] = initRef
}
}
}
m.Tables[idx] = t
idx++
}
@@ -259,18 +265,7 @@ func (m *ModuleInstance) buildTables(module *Module, skipBoundCheck bool) (err e
for elemI := range module.ElementSection { // Do not loop over the value since elementSegments is a slice of value.
elem := &module.ElementSection[elemI]
table := m.Tables[elem.TableIndex]
var offset uint32
if elem.OffsetExpr.Opcode == OpcodeGlobalGet {
// Ignore error as it's already validated.
globalIdx, _, _ := leb128.LoadUint32(elem.OffsetExpr.Data)
global := m.Globals[globalIdx]
offset = uint32(global.Val)
} else { // i32.const
// Ignore error as it's already validated.
o, _, _ := leb128.LoadInt32(elem.OffsetExpr.Data)
offset = uint32(o)
}
offset := uint32(evaluateConstExprInModuleInstance(&elem.OffsetExpr, m)[0])
// Check to see if we are out-of-bounds
initCount := uint64(len(elem.Init))
if err = checkSegmentBounds(table.Min, uint64(offset)+initCount, Index(elemI)); err != nil {
@@ -295,23 +290,6 @@ func checkSegmentBounds(min uint32, requireMin uint64, idx Index) error { // uin
return nil
}
func (m *Module) verifyImportGlobalI32(sectionID SectionID, sectionIdx Index, idx uint32) error {
ig := uint32(math.MaxUint32) // +1 == 0
for i := range m.ImportSection {
imp := &m.ImportSection[i]
if imp.Type == ExternTypeGlobal {
ig++
if ig == idx {
if imp.DescGlobal.ValType != ValueTypeI32 {
return fmt.Errorf("%s[%d] (global.get %d): import[%d].global.ValType != i32", SectionIDName(sectionID), sectionIdx, idx, i)
}
return nil
}
}
}
return fmt.Errorf("%s[%d] (global.get %d): out of range of imported globals", SectionIDName(sectionID), sectionIdx, idx)
}
// Grow appends the `initialRef` by `delta` times into the References slice.
// Returns -1 if the operation is not valid, otherwise the old length of the table.
//
+4
View File
@@ -33,6 +33,10 @@ var (
ErrRuntimeExpectedSharedMemory = New("expected shared memory")
// ErrRuntimeTooManyWaiters indicates that atomic.wait was called with too many waiters.
ErrRuntimeTooManyWaiters = New("too many waiters")
// ErrRuntimeUncaughtException indicates that a thrown exception was not caught by any handler.
ErrRuntimeUncaughtException = New("uncaught exception")
// ErrRuntimeNullReference indicates a null reference was used where a non-null reference was expected.
ErrRuntimeNullReference = New("null reference")
)
// Error is returned by a wasm.Engine during the execution of Wasm functions, and they indicate that the Wasm runtime