gojs: refactors GOOS and GOARCH specific code into their own packages (#959)

This refactors GOOS and GOARCH specific code into their own packages.
This allows logging interceptors to be built without cyclic package
dependencies.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-12-27 08:45:43 +08:00
committed by GitHub
parent a1706eb05c
commit 1ad900d179
15 changed files with 739 additions and 499 deletions

View File

@@ -1,9 +1,11 @@
package gojs
import (
"encoding/binary"
"errors"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/util"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -14,6 +16,8 @@ const (
wasmMinDataAddr = endOfPageZero + maxArgsAndEnviron // ld.wasmMinDataAddr
)
var le = binary.LittleEndian
// WriteArgsAndEnviron writes arguments and environment variables to memory, so
// they can be read by main, Go compiles as the function export "run".
func WriteArgsAndEnviron(mod api.Module) (argc, argv uint32, err error) {
@@ -28,14 +32,16 @@ func WriteArgsAndEnviron(mod api.Module) (argc, argv uint32, err error) {
strPtr := func(val []byte, field string, i int) (ptr uint32) {
// TODO: return err and format "%s[%d], field, i"
ptr = offset
mustWrite(mem, field, offset, append(val, 0))
util.MustWrite(mem, field, offset, append(val, 0))
offset += uint32(len(val) + 1)
if pad := offset % 8; pad != 0 {
offset += 8 - pad
}
return
}
argvPtrs := make([]uint32, 0, len(args)+1+len(environ)+1)
argvPtrLen := len(args) + 1 + len(environ) + 1
argvPtrs := make([]uint32, 0, argvPtrLen)
for i, arg := range args {
argvPtrs = append(argvPtrs, strPtr(arg, "args", i))
}
@@ -47,14 +53,18 @@ func WriteArgsAndEnviron(mod api.Module) (argc, argv uint32, err error) {
argvPtrs = append(argvPtrs, 0)
argv = offset
for _, ptr := range argvPtrs {
// TODO: return err and format "argvPtrs[%d], i"
mustWriteUint64Le(mem, "argvPtrs[i]", offset, uint64(ptr))
offset += 8
}
if offset >= wasmMinDataAddr {
stop := uint32(argvPtrLen << 3) // argvPtrLen * 8
if offset+stop >= wasmMinDataAddr {
err = errors.New("total length of command line and environment variables exceeds limit")
}
buf := util.MustRead(mem, "argvPtrs", argv, stop)
pos := uint32(0)
for _, ptr := range argvPtrs {
le.PutUint64(buf[pos:], uint64(ptr))
pos += 8
}
return
}

View File

@@ -2,21 +2,15 @@ package gojs
import (
"net/http"
"github.com/tetratelabs/wazero/internal/gojs/goos"
)
const (
// predefined
idValueNaN uint32 = iota
idValueZero
idValueNull
idValueTrue
idValueFalse
idValueGlobal
idJsGo
// The below are derived from analyzing `*_js.go` source.
idObjectConstructor
idObjectConstructor uint32 = goos.NextID + iota
idArrayConstructor
idJsProcess
idJsfs
@@ -31,25 +25,17 @@ const (
)
const (
refValueUndefined = ref(0)
refValueNaN = (nanHead|ref(typeFlagNone))<<32 | ref(idValueNaN)
refValueZero = (nanHead|ref(typeFlagNone))<<32 | ref(idValueZero)
refValueNull = (nanHead|ref(typeFlagNone))<<32 | ref(idValueNull)
refValueTrue = (nanHead|ref(typeFlagNone))<<32 | ref(idValueTrue)
refValueFalse = (nanHead|ref(typeFlagNone))<<32 | ref(idValueFalse)
refValueGlobal = (nanHead|ref(typeFlagObject))<<32 | ref(idValueGlobal)
refJsGo = (nanHead|ref(typeFlagObject))<<32 | ref(idJsGo)
refObjectConstructor = (nanHead|ref(typeFlagFunction))<<32 | ref(idObjectConstructor)
refArrayConstructor = (nanHead|ref(typeFlagFunction))<<32 | ref(idArrayConstructor)
refJsProcess = (nanHead|ref(typeFlagObject))<<32 | ref(idJsProcess)
refJsfs = (nanHead|ref(typeFlagObject))<<32 | ref(idJsfs)
refJsfsConstants = (nanHead|ref(typeFlagObject))<<32 | ref(idJsfsConstants)
refUint8ArrayConstructor = (nanHead|ref(typeFlagFunction))<<32 | ref(idUint8ArrayConstructor)
refJsCrypto = (nanHead|ref(typeFlagFunction))<<32 | ref(idJsCrypto)
refJsDateConstructor = (nanHead|ref(typeFlagFunction))<<32 | ref(idJsDateConstructor)
refJsDate = (nanHead|ref(typeFlagObject))<<32 | ref(idJsDate)
refHttpFetch = (nanHead|ref(typeFlagFunction))<<32 | ref(idHttpFetch)
refHttpHeadersConstructor = (nanHead|ref(typeFlagFunction))<<32 | ref(idHttpHeaders)
refObjectConstructor = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idObjectConstructor)
refArrayConstructor = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idArrayConstructor)
refJsProcess = (goos.NanHead|goos.Ref(goos.TypeFlagObject))<<32 | goos.Ref(idJsProcess)
refJsfs = (goos.NanHead|goos.Ref(goos.TypeFlagObject))<<32 | goos.Ref(idJsfs)
refJsfsConstants = (goos.NanHead|goos.Ref(goos.TypeFlagObject))<<32 | goos.Ref(idJsfsConstants)
refUint8ArrayConstructor = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idUint8ArrayConstructor)
refJsCrypto = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idJsCrypto)
refJsDateConstructor = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idJsDateConstructor)
refJsDate = (goos.NanHead|goos.Ref(goos.TypeFlagObject))<<32 | goos.Ref(idJsDate)
refHttpFetch = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idHttpFetch)
refHttpHeadersConstructor = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idHttpHeaders)
)
// newJsGlobal = js.Global() // js.go init
@@ -58,7 +44,7 @@ func newJsGlobal(rt http.RoundTripper) *jsVal {
if rt != nil {
fetchProperty = refHttpFetch
}
return newJsVal(refValueGlobal, "global").
return newJsVal(goos.RefValueGlobal, "global").
addProperties(map[string]interface{}{
"Object": objectConstructor,
"Array": arrayConstructor,
@@ -91,8 +77,8 @@ var (
// jsProcess = js.Global().Get("process") // fs_js.go init
jsProcess = newJsVal(refJsProcess, "process").
addProperties(map[string]interface{}{
"pid": float64(1), // Get("pid").Int() in syscall_js.go for syscall.Getpid
"ppid": refValueZero, // Get("ppid").Int() in syscall_js.go for syscall.Getppid
"pid": float64(1), // Get("pid").Int() in syscall_js.go for syscall.Getpid
"ppid": goos.RefValueZero, // Get("ppid").Int() in syscall_js.go for syscall.Getppid
}).
addFunction("cwd", &cwd{}). // syscall.Cwd in fs_js.go
addFunction("chdir", &chdir{}). // syscall.Chdir in fs_js.go

View File

@@ -26,8 +26,7 @@ func compileAndRun(ctx context.Context, arg string, config wazero.ModuleConfig)
ns := rt.NewNamespace(ctx)
builder := rt.NewHostModuleBuilder("go")
gojs.NewFunctionExporter().
ExportFunctions(builder)
gojs.NewFunctionExporter().ExportFunctions(builder)
if _, err = builder.Instantiate(ctx, ns); err != nil {
return
}

View File

@@ -20,7 +20,7 @@ var jsCrypto = newJsVal(refJsCrypto, "crypto").
type getRandomValues struct{}
// invoke implements jsFn.invoke
func (*getRandomValues) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
func (*getRandomValues) invoke(_ context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
randSource := mod.(*wasm.CallContext).Sys.RandSource()
r := args[0].(*byteArray)

View File

@@ -9,6 +9,7 @@ import (
"syscall"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/goos"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -252,7 +253,7 @@ func (*jsfsWrite) invoke(ctx context.Context, mod api.Module, args ...interface{
n, err := syscallWrite(mod, fd, fOffset, buf.slice[offset:offset+byteCount])
return callback.invoke(ctx, mod, refJsfs, err, n) // note: error first
}
return callback.invoke(ctx, mod, refJsfs, nil, refValueZero)
return callback.invoke(ctx, mod, refJsfs, nil, goos.RefValueZero)
}
// syscallWrite is like syscall.Write
@@ -313,15 +314,15 @@ func syscallReaddir(_ context.Context, mod api.Module, name string) (*objectArra
type returnZero struct{}
// invoke implements jsFn.invoke
func (*returnZero) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
return refValueZero, nil
func (*returnZero) invoke(context.Context, api.Module, ...interface{}) (interface{}, error) {
return goos.RefValueZero, nil
}
type returnSliceOfZero struct{}
// invoke implements jsFn.invoke
func (*returnSliceOfZero) invoke(context.Context, api.Module, ...interface{}) (interface{}, error) {
return &objectArray{slice: []interface{}{refValueZero}}, nil
return &objectArray{slice: []interface{}{goos.RefValueZero}}, nil
}
type returnArg0 struct{}
@@ -418,7 +419,7 @@ func (s *jsSt) get(_ context.Context, propertyKey string) interface{} {
}
// call implements jsCall.call
func (s *jsSt) call(ctx context.Context, mod api.Module, this ref, method string, args ...interface{}) (interface{}, error) {
func (s *jsSt) call(_ context.Context, _ api.Module, _ goos.Ref, method string, _ ...interface{}) (interface{}, error) {
if method == "isDirectory" {
return s.isDir, nil
}

View File

@@ -0,0 +1,192 @@
// Package goarch isolates code from runtime.GOARCH=wasm in a way that avoids
// cyclic dependencies when re-used from other packages.
package goarch
import (
"context"
"encoding/binary"
"fmt"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/util"
"github.com/tetratelabs/wazero/internal/wasm"
)
// StubFunction stubs functions not used in Go's main source tree.
// This traps (unreachable opcode) to ensure the function is never called.
func StubFunction(name string) *wasm.HostFunc {
return &wasm.HostFunc{
ExportNames: []string{name},
Name: name,
ParamTypes: []wasm.ValueType{wasm.ValueTypeI32},
ParamNames: []string{"sp"},
Code: &wasm.Code{IsHostFunction: true, Body: []byte{wasm.OpcodeUnreachable, wasm.OpcodeEnd}},
}
}
func NoopFunction(name string) *wasm.HostFunc {
return &wasm.HostFunc{
ExportNames: []string{name},
Name: name,
ParamTypes: []wasm.ValueType{wasm.ValueTypeI32},
ParamNames: []string{"sp"},
Code: &wasm.Code{IsHostFunction: true, Body: []byte{wasm.OpcodeEnd}},
}
}
var le = binary.LittleEndian
type Stack interface {
Param(i int) uint64
ParamName(i int) string
// ParamBytes reads a byte slice, given its memory offset and length (stack
// positions i, i+1)
ParamBytes(mem api.Memory, i int) []byte
// ParamString reads a string, given its memory offset and length (stack
// positions i, i+1)
ParamString(mem api.Memory, i int) string
ParamUint32(i int) uint32
// Refresh the stack from the current stack pointer (SP).
//
// Note: This is needed prior to storing a value when in an operation that
// can trigger a Go event handler.
Refresh(api.Module)
ResultName(i int) string
SetResult(i int, v uint64)
SetResultBool(i int, v bool)
SetResultI32(i int, v int32)
SetResultI64(i int, v int64)
SetResultUint32(i int, v uint32)
}
func NewStack(mem api.Memory, sp uint32, paramNames, resultNames []string) Stack {
s := &stack{paramNames: paramNames, resultNames: resultNames}
s.refresh(mem, sp)
return s
}
type stack struct {
paramNames, resultNames []string
buf []byte
}
// Param implements Stack.Param
func (s *stack) Param(i int) (res uint64) {
pos := i << 3
res = le.Uint64(s.buf[pos:])
return
}
// ParamName implements Stack.ParamName
func (s *stack) ParamName(i int) string {
return s.paramNames[i]
}
// ParamBytes implements Stack.ParamBytes
func (s *stack) ParamBytes(mem api.Memory, i int) (res []byte) {
offset := s.ParamUint32(i)
byteCount := s.ParamUint32(i + 1)
return mustRead(mem, s.paramNames[i], offset, byteCount)
}
// ParamString implements Stack.ParamString
func (s *stack) ParamString(mem api.Memory, i int) string {
return string(s.ParamBytes(mem, i))
}
// ParamUint32 implements Stack.ParamUint32
func (s *stack) ParamUint32(i int) uint32 {
return uint32(s.Param(i))
}
// Refresh implements Stack.Refresh
func (s *stack) Refresh(mod api.Module) {
s.refresh(mod.Memory(), getSP(mod))
}
func (s *stack) refresh(mem api.Memory, sp uint32) {
count := uint32(len(s.paramNames) + len(s.resultNames))
s.buf = mustRead(mem, "sp", sp+8, count<<3)
}
// SetResult implements Stack.SetResult
func (s *stack) SetResult(i int, v uint64) {
pos := (len(s.paramNames) + i) << 3
le.PutUint64(s.buf[pos:], v)
}
// ResultName implements Stack.ResultName
func (s *stack) ResultName(i int) string {
return s.resultNames[i]
}
// SetResultBool implements Stack.SetResultBool
func (s *stack) SetResultBool(i int, v bool) {
if v {
s.SetResultUint32(i, 1)
} else {
s.SetResultUint32(i, 0)
}
}
// SetResultI32 implements Stack.SetResultI32
func (s *stack) SetResultI32(i int, v int32) {
s.SetResult(i, uint64(v))
}
// SetResultI64 implements Stack.SetResultI64
func (s *stack) SetResultI64(i int, v int64) {
s.SetResult(i, uint64(v))
}
// SetResultUint32 implements Stack.SetResultUint32
func (s *stack) SetResultUint32(i int, v uint32) {
s.SetResult(i, uint64(v))
}
// getSP gets the stack pointer, which is needed prior to storing a value when
// in an operation that can trigger a Go event handler.
//
// See https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L210-L213
func getSP(mod api.Module) uint32 {
// Cheat by reading global[0] directly instead of through a function proxy.
// https://github.com/golang/go/blob/go1.19/src/runtime/rt0_js_wasm.s#L87-L90
return uint32(mod.(*wasm.CallContext).GlobalVal(0))
}
// mustRead is like api.Memory except that it panics if the offset and
// byteCount are out of range.
func mustRead(mem api.Memory, fieldName string, offset, byteCount uint32) []byte {
buf, ok := mem.Read(offset, byteCount)
if !ok {
panic(fmt.Errorf("out of memory reading %s", fieldName))
}
return buf
}
func NewFunc(name string, goFunc Func, paramNames, resultNames []string) *wasm.HostFunc {
return util.NewFunc(name, (&stackFunc{f: goFunc, paramNames: paramNames, resultNames: resultNames}).Call)
}
type Func func(context.Context, api.Module, Stack)
type stackFunc struct {
f Func
paramNames, resultNames []string
}
// Call implements the same method as defined on api.GoModuleFunction.
func (f *stackFunc) Call(ctx context.Context, mod api.Module, wasmStack []uint64) {
s := NewStack(mod.Memory(), uint32(wasmStack[0]), f.paramNames, f.resultNames)
f.f(ctx, mod, s)
}

221
internal/gojs/goos/js.go Normal file
View File

@@ -0,0 +1,221 @@
// Package goos isolates code from runtime.GOOS=js in a way that avoids cyclic
// dependencies when re-used from other packages.
package goos
import (
"context"
"encoding/binary"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/goarch"
"github.com/tetratelabs/wazero/internal/gojs/util"
"github.com/tetratelabs/wazero/internal/wasm"
)
// Ref is used to identify a JavaScript value, since the value itself cannot
// be passed to WebAssembly.
//
// The JavaScript value "undefined" is represented by the value 0.
//
// A JavaScript number (64-bit float, except 0 and NaN) is represented by its
// IEEE 754 binary representation.
//
// All other values are represented as an IEEE 754 binary representation of NaN
// with bits 0-31 used as an ID and bits 32-34 used to differentiate between
// string, symbol, function and object.
type Ref uint64
const (
IdValueNaN uint32 = iota
IdValueZero
IdValueNull
IdValueTrue
IdValueFalse
IdValueGlobal
IdJsGo
NextID
)
const (
RefValueUndefined = Ref(0)
RefValueNaN = (NanHead|Ref(TypeFlagNone))<<32 | Ref(IdValueNaN)
RefValueZero = (NanHead|Ref(TypeFlagNone))<<32 | Ref(IdValueZero)
RefValueNull = (NanHead|Ref(TypeFlagNone))<<32 | Ref(IdValueNull)
RefValueTrue = (NanHead|Ref(TypeFlagNone))<<32 | Ref(IdValueTrue)
RefValueFalse = (NanHead|Ref(TypeFlagNone))<<32 | Ref(IdValueFalse)
RefValueGlobal = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdValueGlobal)
RefJsGo = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsGo)
)
type TypeFlag byte
// the type flags need to be in sync with gojs.js
const (
TypeFlagNone TypeFlag = iota
TypeFlagObject
TypeFlagString
TypeFlagSymbol // nolint
TypeFlagFunction
)
func ValueRef(id uint32, typeFlag TypeFlag) Ref {
return (NanHead|Ref(typeFlag))<<32 | Ref(id)
}
var le = binary.LittleEndian
// NanHead are the upper 32 bits of a Ref which are set if the value is not encoded as an IEEE 754 number (see above).
const NanHead = 0x7FF80000
func (ref Ref) ParseFloat() (v float64, ok bool) {
if (ref>>32)&NanHead != NanHead {
v = api.DecodeF64(uint64(ref))
ok = true
}
return
}
type ValLoader func(context.Context, Ref) interface{}
type Stack interface {
goarch.Stack
ParamRef(i int) Ref
ParamRefs(mem api.Memory, i int) []Ref
ParamVal(ctx context.Context, i int, loader ValLoader) interface{}
ParamVals(ctx context.Context, mem api.Memory, i int, loader ValLoader) []interface{}
SetResultRef(i int, v Ref)
}
type stack struct {
s goarch.Stack
}
// Param implements the same method as documented on goarch.Stack
func (s *stack) Param(i int) uint64 {
return s.s.Param(i)
}
// ParamName implements the same method as documented on goarch.Stack
func (s *stack) ParamName(i int) string {
return s.s.ParamName(i)
}
// ParamBytes implements the same method as documented on goarch.Stack
func (s *stack) ParamBytes(mem api.Memory, i int) []byte {
return s.s.ParamBytes(mem, i)
}
// ParamRef implements Stack.ParamRef
func (s *stack) ParamRef(i int) Ref {
return Ref(s.s.Param(i))
}
// ParamRefs implements Stack.ParamRefs
func (s *stack) ParamRefs(mem api.Memory, i int) []Ref {
offset := s.s.ParamUint32(i)
size := s.s.ParamUint32(i + 1)
byteCount := size << 3 // size * 8
result := make([]Ref, 0, size)
buf := util.MustRead(mem, s.s.ParamName(i), offset, byteCount)
for pos := uint32(0); pos < byteCount; pos += 8 {
ref := Ref(le.Uint64(buf[pos:]))
result = append(result, ref)
}
return result
}
// ParamString implements the same method as documented on goarch.Stack
func (s *stack) ParamString(mem api.Memory, i int) string {
return s.s.ParamString(mem, i)
}
// ParamUint32 implements the same method as documented on goarch.Stack
func (s *stack) ParamUint32(i int) uint32 {
return s.s.ParamUint32(i)
}
// ParamVal implements Stack.ParamVal
func (s *stack) ParamVal(ctx context.Context, i int, loader ValLoader) interface{} {
ref := s.ParamRef(i)
return loader(ctx, ref)
}
// ParamVals implements Stack.ParamVals
func (s *stack) ParamVals(ctx context.Context, mem api.Memory, i int, loader ValLoader) []interface{} {
offset := s.s.ParamUint32(i)
size := s.s.ParamUint32(i + 1)
byteCount := size << 3 // size * 8
result := make([]interface{}, 0, size)
buf := util.MustRead(mem, s.s.ParamName(i), offset, byteCount)
for pos := uint32(0); pos < byteCount; pos += 8 {
ref := Ref(le.Uint64(buf[pos:]))
result = append(result, loader(ctx, ref))
}
return result
}
// Refresh implements the same method as documented on goarch.Stack
func (s *stack) Refresh(mod api.Module) {
s.s.Refresh(mod)
}
// ResultName implements the same method as documented on goarch.Stack
func (s *stack) ResultName(i int) string {
return s.s.ResultName(i)
}
// SetResult implements the same method as documented on goarch.Stack
func (s *stack) SetResult(i int, v uint64) {
s.s.SetResult(i, v)
}
// SetResultBool implements the same method as documented on goarch.Stack
func (s *stack) SetResultBool(i int, v bool) {
s.s.SetResultBool(i, v)
}
// SetResultI32 implements the same method as documented on goarch.Stack
func (s *stack) SetResultI32(i int, v int32) {
s.s.SetResultI32(i, v)
}
// SetResultI64 implements the same method as documented on goarch.Stack
func (s *stack) SetResultI64(i int, v int64) {
s.s.SetResultI64(i, v)
}
// SetResultRef implements Stack.SetResultRef
func (s *stack) SetResultRef(i int, v Ref) {
s.s.SetResult(i, uint64(v))
}
// SetResultUint32 implements the same method as documented on goarch.Stack
func (s *stack) SetResultUint32(i int, v uint32) {
s.s.SetResultUint32(i, v)
}
func NewFunc(name string, goFunc Func, paramNames, resultNames []string) *wasm.HostFunc {
return util.NewFunc(name, (&stackFunc{f: goFunc, paramNames: paramNames, resultNames: resultNames}).Call)
}
type Func func(context.Context, api.Module, Stack)
type stackFunc struct {
f Func
paramNames, resultNames []string
}
// Call implements the same method as defined on api.GoModuleFunction.
func (f *stackFunc) Call(ctx context.Context, mod api.Module, wasmStack []uint64) {
s := &stack{goarch.NewStack(mod.Memory(), uint32(wasmStack[0]), f.paramNames, f.resultNames)}
f.f(ctx, mod, s)
}

View File

@@ -9,6 +9,7 @@ import (
"sort"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/goos"
)
// headersConstructor = Get("Headers").New() // http.Roundtrip && "fetch"
@@ -57,7 +58,7 @@ type fetchPromise struct {
}
// call implements jsCall.call
func (p *fetchPromise) call(ctx context.Context, mod api.Module, this ref, method string, args ...interface{}) (interface{}, error) {
func (p *fetchPromise) call(ctx context.Context, mod api.Module, this goos.Ref, method string, args ...interface{}) (interface{}, error) {
if method == "then" {
if res, err := p.rt.RoundTrip(p.req); err != nil {
failure := args[1].(funcWrapper)
@@ -76,7 +77,7 @@ type fetchResult struct {
}
// get implements jsGet.get
func (s *fetchResult) get(ctx context.Context, propertyKey string) interface{} {
func (s *fetchResult) get(_ context.Context, propertyKey string) interface{} {
switch propertyKey {
case "headers":
names := make([]string, 0, len(s.res.Header))
@@ -97,7 +98,7 @@ func (s *fetchResult) get(ctx context.Context, propertyKey string) interface{} {
}
// call implements jsCall.call
func (s *fetchResult) call(ctx context.Context, _ api.Module, this ref, method string, _ ...interface{}) (interface{}, error) {
func (s *fetchResult) call(_ context.Context, _ api.Module, _ goos.Ref, method string, _ ...interface{}) (interface{}, error) {
switch method {
case "arrayBuffer":
v := &arrayPromise{reader: s.res.Body}
@@ -127,7 +128,7 @@ func (h *headers) get(_ context.Context, propertyKey string) interface{} {
}
// call implements jsCall.call
func (h *headers) call(_ context.Context, _ api.Module, this ref, method string, args ...interface{}) (interface{}, error) {
func (h *headers) call(_ context.Context, _ api.Module, _ goos.Ref, method string, args ...interface{}) (interface{}, error) {
switch method {
case "entries":
// Sort names for consistent iteration
@@ -150,7 +151,7 @@ type arrayPromise struct {
}
// call implements jsCall.call
func (p *arrayPromise) call(ctx context.Context, mod api.Module, this ref, method string, args ...interface{}) (interface{}, error) {
func (p *arrayPromise) call(ctx context.Context, mod api.Module, this goos.Ref, method string, args ...interface{}) (interface{}, error) {
switch method {
case "then":
defer p.reader.Close()

View File

@@ -5,19 +5,7 @@ import (
"fmt"
"github.com/tetratelabs/wazero/api"
)
// ref is used to identify a JavaScript value, since the value itself can not be passed to WebAssembly.
//
// The JavaScript value "undefined" is represented by the value 0.
// A JavaScript number (64-bit float, except 0 and NaN) is represented by its IEEE 754 binary representation.
// All other values are represented as an IEEE 754 binary representation of NaN with bits 0-31 used as
// an ID and bits 32-34 used to differentiate between string, symbol, function and object.
type ref uint64
const (
parameterSp = "sp"
functionDebug = "debug"
"github.com/tetratelabs/wazero/internal/gojs/goos"
)
// jsFn is a jsCall.call function, configured via jsVal.addFunction.
@@ -31,36 +19,18 @@ type jsGet interface {
// jsCall allows calling a method/function by name.
type jsCall interface {
call(ctx context.Context, mod api.Module, this ref, method string, args ...interface{}) (interface{}, error)
call(ctx context.Context, mod api.Module, this goos.Ref, method string, args ...interface{}) (interface{}, error)
}
// nanHead are the upper 32 bits of a ref which are set if the value is not encoded as an IEEE 754 number (see above).
const nanHead = 0x7FF80000
type typeFlag byte
const (
// the type flags need to be in sync with gojs.js
typeFlagNone typeFlag = iota
typeFlagObject
typeFlagString
typeFlagSymbol // nolint
typeFlagFunction
)
func valueRef(id uint32, typeFlag typeFlag) ref {
return (nanHead|ref(typeFlag))<<32 | ref(id)
}
func newJsVal(ref ref, name string) *jsVal {
func newJsVal(ref goos.Ref, name string) *jsVal {
return &jsVal{ref: ref, name: name, properties: map[string]interface{}{}, functions: map[string]jsFn{}}
}
// jsVal corresponds to a generic js.Value in go, when `GOOS=js`.
type jsVal struct {
// ref when is the constant reference used for built-in values, such as
// ref is the constant reference used for built-in values, such as
// objectConstructor.
ref
ref goos.Ref
name string
properties map[string]interface{}
functions map[string]jsFn
@@ -91,7 +61,7 @@ func (v *jsVal) get(_ context.Context, propertyKey string) interface{} {
}
// call implements jsCall.call
func (v *jsVal) call(ctx context.Context, mod api.Module, this ref, method string, args ...interface{}) (interface{}, error) {
func (v *jsVal) call(ctx context.Context, mod api.Module, this goos.Ref, method string, args ...interface{}) (interface{}, error) {
if v, ok := v.functions[method]; ok {
return v.invoke(ctx, mod, args...)
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/goarch"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -19,16 +20,21 @@ const (
getRandomDataName = "runtime.getRandomData"
)
// Debug has unknown use, so stubbed.
//
// See https://github.com/golang/go/blob/go1.19/src/cmd/link/internal/wasm/asm.go#L133-L138
var Debug = goarch.StubFunction("debug")
// WasmExit implements runtime.wasmExit which supports runtime.exit.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.go#L28
var WasmExit = newSPFunc(wasmExitName, wasmExit)
var WasmExit = goarch.NewFunc(wasmExitName, wasmExit,
[]string{"code"},
[]string{},
)
func wasmExit(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
// Read the code from offset SP+8
code := mustReadUint32Le(mem, "code", uint32(sp[0]+8))
func wasmExit(ctx context.Context, mod api.Module, stack goarch.Stack) {
code := stack.ParamUint32(0)
getState(ctx).clear()
_ = mod.CloseWithExitCode(ctx, code) // TODO: should ours be signed bit (like -1 == 255)?
@@ -38,25 +44,22 @@ func wasmExit(ctx context.Context, mod api.Module, sp []uint64) {
// runtime.writeErr. This implements `println`.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/os_js.go#L29
var WasmWrite = newSPFunc(wasmWriteName, wasmWrite)
var WasmWrite = goarch.NewFunc(wasmWriteName, wasmWrite,
[]string{"fd", "p", "p_len"},
[]string{},
)
func wasmWrite(_ context.Context, mod api.Module, stack goarch.Stack) {
fd := stack.ParamUint32(0)
p := stack.ParamBytes(mod.Memory(), 1 /*, 2 */)
func wasmWrite(_ context.Context, mod api.Module, sp []uint64) {
fsc := mod.(*wasm.CallContext).Sys.FS()
mem := mod.Memory()
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 24)
fd := le.Uint32(stack)
p := le.Uint32(stack[8:])
n := le.Uint32(stack[16:])
writer := fsc.FdWriter(fd)
if writer == nil {
panic(fmt.Errorf("unexpected fd %d", fd))
}
if _, err := writer.Write(mustRead(mod.Memory(), "p", p, n)); err != nil {
if _, err := writer.Write(p); err != nil {
panic(fmt.Errorf("error writing p: %w", err))
}
}
@@ -65,42 +68,37 @@ func wasmWrite(_ context.Context, mod api.Module, sp []uint64) {
// cached view of memory should be reset.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/mem_js.go#L82
var ResetMemoryDataView = &wasm.HostFunc{
ExportNames: []string{resetMemoryDataViewName},
Name: resetMemoryDataViewName,
ParamTypes: []wasm.ValueType{wasm.ValueTypeI32},
ParamNames: []string{parameterSp},
// TODO: Compiler-based memory.grow callbacks are ignored until we have a generic solution #601
Code: &wasm.Code{IsHostFunction: true, Body: []byte{wasm.OpcodeEnd}},
}
//
// TODO: Compiler-based memory.grow callbacks are ignored until we have a generic solution #601
var ResetMemoryDataView = goarch.NoopFunction(resetMemoryDataViewName)
// Nanotime1 implements runtime.nanotime which supports time.Since.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L184
var Nanotime1 = newSPFunc(nanotime1Name, nanotime1)
var Nanotime1 = goarch.NewFunc(nanotime1Name, nanotime1,
[]string{},
[]string{"nsec"},
)
func nanotime1(_ context.Context, mod api.Module, sp []uint64) {
time := mod.(*wasm.CallContext).Sys.Nanotime()
mem := mod.Memory()
func nanotime1(_ context.Context, mod api.Module, stack goarch.Stack) {
nsec := mod.(*wasm.CallContext).Sys.Nanotime()
// Write the result to offset SP+8
mustWriteUint64Le(mem, "time", uint32(sp[0]+8), api.EncodeI64(time))
stack.SetResultI64(0, nsec)
}
// Walltime implements runtime.walltime which supports time.Now.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L188
var Walltime = newSPFunc(walltimeName, walltime)
var Walltime = goarch.NewFunc(walltimeName, walltime,
[]string{},
[]string{"sec", "nsec"},
)
func walltime(_ context.Context, mod api.Module, sp []uint64) {
func walltime(_ context.Context, mod api.Module, stack goarch.Stack) {
sec, nsec := mod.(*wasm.CallContext).Sys.Walltime()
mem := mod.Memory()
// Write results starting at SP+8
results := mustRead(mem, "sp", uint32(sp[0]+8), 16)
le.PutUint64(results, uint64(sec))
le.PutUint32(results[8:], uint32(nsec))
stack.SetResultI64(0, sec)
stack.SetResultI32(1, nsec)
}
// ScheduleTimeoutEvent implements runtime.scheduleTimeoutEvent which supports
@@ -110,7 +108,7 @@ func walltime(_ context.Context, mod api.Module, sp []uint64) {
// goroutine and invokes code compiled into wasm "resume".
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L192
var ScheduleTimeoutEvent = stubFunction(scheduleTimeoutEventName)
var ScheduleTimeoutEvent = goarch.StubFunction(scheduleTimeoutEventName)
// ^^ stubbed because signal handling is not implemented in GOOS=js
@@ -118,7 +116,7 @@ var ScheduleTimeoutEvent = stubFunction(scheduleTimeoutEventName)
// runtime.notetsleepg used by runtime.signal_recv.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L196
var ClearTimeoutEvent = stubFunction(clearTimeoutEventName)
var ClearTimeoutEvent = goarch.StubFunction(clearTimeoutEventName)
// ^^ stubbed because signal handling is not implemented in GOOS=js
@@ -126,23 +124,20 @@ var ClearTimeoutEvent = stubFunction(clearTimeoutEventName)
// for runtime.fastrand.
//
// See https://github.com/golang/go/blob/go1.19/src/runtime/sys_wasm.s#L200
var GetRandomData = newSPFunc(getRandomDataName, getRandomData)
var GetRandomData = goarch.NewFunc(getRandomDataName, getRandomData,
[]string{"r", "r_len"},
[]string{},
)
func getRandomData(_ context.Context, mod api.Module, stack goarch.Stack) {
r := stack.ParamBytes(mod.Memory(), 0 /*, 1 */)
func getRandomData(_ context.Context, mod api.Module, sp []uint64) {
randSource := mod.(*wasm.CallContext).Sys.RandSource()
mem := mod.Memory()
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 16)
buf := le.Uint32(stack)
bufLen := le.Uint32(stack[8:])
r := mustRead(mod.Memory(), "r", buf, bufLen)
bufLen := len(r)
if n, err := randSource.Read(r); err != nil {
panic(fmt.Errorf("RandSource.Read(r /* len=%d */) failed: %w", bufLen, err))
} else if uint32(n) != bufLen {
} else if n != bufLen {
panic(fmt.Errorf("RandSource.Read(r /* len=%d */) read %d bytes", bufLen, n))
}
}

View File

@@ -6,6 +6,7 @@ import (
"math"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/goos"
)
func WithState(ctx context.Context) context.Context {
@@ -27,7 +28,7 @@ func getState(ctx context.Context) *state {
type event struct {
// id is the funcWrapper.id
id uint32
this ref
this goos.Ref
args *objectArray
result interface{}
}
@@ -54,23 +55,23 @@ var (
// value written by storeValue.
//
// See https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L122-L133
func loadValue(ctx context.Context, ref ref) interface{} { // nolint
func loadValue(ctx context.Context, ref goos.Ref) interface{} { // nolint
switch ref {
case 0:
return undefined
case refValueNaN:
case goos.RefValueNaN:
return NaN
case refValueZero:
case goos.RefValueZero:
return float64(0)
case refValueNull:
case goos.RefValueNull:
return nil
case refValueTrue:
case goos.RefValueTrue:
return true
case refValueFalse:
case goos.RefValueFalse:
return false
case refValueGlobal:
case goos.RefValueGlobal:
return getState(ctx).valueGlobal
case refJsGo:
case goos.RefJsGo:
return getState(ctx)
case refObjectConstructor:
return objectConstructor
@@ -93,74 +94,63 @@ func loadValue(ctx context.Context, ref ref) interface{} { // nolint
case refHttpHeadersConstructor:
return headersConstructor
default:
if (ref>>32)&nanHead != nanHead { // numbers are passed through as a ref
return api.DecodeF64(uint64(ref))
if f, ok := ref.ParseFloat(); ok { // numbers are passed through as a Ref
return f
}
return getState(ctx).values.get(uint32(ref))
}
}
// loadArgs returns a slice of `len` values at the memory offset `addr`. The
// returned slice is temporary, not stored in state.values.
//
// See https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L191-L199
func loadArgs(ctx context.Context, mod api.Module, sliceAddr, sliceLen uint32) []interface{} { // nolint
result := make([]interface{}, 0, sliceLen)
for i := uint32(0); i < sliceLen; i++ { // nolint
iRef := mustReadUint64Le(mod.Memory(), "iRef", sliceAddr+i*8)
result = append(result, loadValue(ctx, ref(iRef)))
}
return result
}
// storeRef stores a value prior to returning to wasm from a host function.
// This returns 8 bytes to represent either the value or a reference to it.
// Any side effects besides memory must be cleaned up on wasmExit.
//
// See https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L135-L183
func storeRef(ctx context.Context, v interface{}) uint64 { // nolint
func storeRef(ctx context.Context, v interface{}) goos.Ref { // nolint
// allow-list because we control all implementations
if v == undefined {
return uint64(refValueUndefined)
return goos.RefValueUndefined
} else if v == nil {
return uint64(refValueNull)
} else if r, ok := v.(ref); ok {
return uint64(r)
return goos.RefValueNull
} else if r, ok := v.(goos.Ref); ok {
return r
} else if b, ok := v.(bool); ok {
if b {
return uint64(refValueTrue)
return goos.RefValueTrue
} else {
return uint64(refValueFalse)
return goos.RefValueFalse
}
} else if c, ok := v.(*jsVal); ok {
return uint64(c.ref) // already stored
return c.ref // already stored
} else if _, ok := v.(*event); ok {
id := getState(ctx).values.increment(v)
return uint64(valueRef(id, typeFlagFunction))
return goos.ValueRef(id, goos.TypeFlagFunction)
} else if _, ok := v.(funcWrapper); ok {
id := getState(ctx).values.increment(v)
return uint64(valueRef(id, typeFlagFunction))
return goos.ValueRef(id, goos.TypeFlagFunction)
} else if _, ok := v.(jsFn); ok {
id := getState(ctx).values.increment(v)
return uint64(valueRef(id, typeFlagFunction))
return goos.ValueRef(id, goos.TypeFlagFunction)
} else if _, ok := v.(string); ok {
id := getState(ctx).values.increment(v)
return uint64(valueRef(id, typeFlagString))
return goos.ValueRef(id, goos.TypeFlagString)
} else if ui, ok := v.(uint32); ok {
if ui == 0 {
return uint64(refValueZero)
return goos.RefValueZero
}
return api.EncodeF64(float64(ui)) // numbers are encoded as float and passed through as a ref
// numbers are encoded as float and passed through as a Ref
return goos.Ref(api.EncodeF64(float64(ui)))
} else if u, ok := v.(uint64); ok {
return u // float is already encoded as a uint64, doesn't need to be stored.
// float is already encoded as a uint64, doesn't need to be stored.
return goos.Ref(u)
} else if f64, ok := v.(float64); ok {
if f64 == 0 {
return uint64(refValueZero)
return goos.RefValueZero
}
return api.EncodeF64(f64)
return goos.Ref(api.EncodeF64(f64))
}
id := getState(ctx).values.increment(v)
return uint64(valueRef(id, typeFlagObject))
return goos.ValueRef(id, goos.TypeFlagObject)
}
type values struct {
@@ -231,7 +221,7 @@ func (s *state) get(_ context.Context, propertyKey string) interface{} {
}
// call implements jsCall.call
func (s *state) call(_ context.Context, _ api.Module, this ref, method string, args ...interface{}) (interface{}, error) {
func (s *state) call(_ context.Context, _ api.Module, _ goos.Ref, method string, args ...interface{}) (interface{}, error) {
switch method {
case "_makeFuncWrapper":
return funcWrapper(args[0].(float64)), nil
@@ -250,7 +240,7 @@ func (s *state) clear() {
}
func toInt64(arg interface{}) int64 {
if arg == refValueZero || arg == undefined {
if arg == goos.RefValueZero || arg == undefined {
return 0
} else if u, ok := arg.(int64); ok {
return u
@@ -259,7 +249,7 @@ func toInt64(arg interface{}) int64 {
}
func toUint32(arg interface{}) uint32 {
if arg == refValueZero || arg == undefined {
if arg == goos.RefValueZero || arg == undefined {
return 0
} else if u, ok := arg.(uint32); ok {
return u

View File

@@ -2,7 +2,6 @@ package gojs
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io/fs"
@@ -10,6 +9,8 @@ import (
"syscall"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/goarch"
"github.com/tetratelabs/wazero/internal/gojs/goos"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/sys"
)
@@ -33,19 +34,16 @@ const (
copyBytesToJSName = "syscall/js.copyBytesToJS"
)
var le = binary.LittleEndian
// FinalizeRef implements js.finalizeRef, which is used as a
// runtime.SetFinalizer on the given reference.
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L61
var FinalizeRef = newSPFunc(finalizeRefName, finalizeRef)
var FinalizeRef = goos.NewFunc(finalizeRefName, finalizeRef, []string{"r"}, nil)
func finalizeRef(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
func finalizeRef(ctx context.Context, _ api.Module, stack goos.Stack) {
r := stack.ParamRef(0)
ref := mustReadUint64Le(mem, "ref", uint32(sp[0]+8))
id := uint32(ref) // 32-bits of the ref are the ID
id := uint32(r) // 32-bits of the ref are the ID
getState(ctx).values.decrement(id)
}
@@ -55,23 +53,14 @@ func finalizeRef(ctx context.Context, mod api.Module, sp []uint64) {
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L212
// and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L305-L308
var StringVal = newSPFunc(stringValName, stringVal)
var StringVal = goos.NewFunc(stringValName, stringVal, []string{"x", "x_len"}, []string{"r"})
func stringVal(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
func stringVal(ctx context.Context, mod api.Module, stack goos.Stack) {
x := stack.ParamString(mod.Memory(), 0)
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 24)
r := storeRef(ctx, x)
xAddr := le.Uint32(stack)
xLen := le.Uint32(stack[8:])
x := string(mustRead(mem, "x", xAddr, xLen))
ref := storeRef(ctx, x)
// Write the results to memory at positions after the parameters.
le.PutUint64(stack[16:], ref)
stack.SetResultRef(0, r)
}
// ValueGet implements js.valueGet, which is used to load a js.Value property
@@ -80,20 +69,14 @@ func stringVal(ctx context.Context, mod api.Module, sp []uint64) {
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L295
// and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L311-L316
var ValueGet = newSPFunc(valueGetName, valueGet)
var ValueGet = goos.NewFunc(valueGetName, valueGet,
[]string{"v", "p", "p_len"},
[]string{"r"},
)
func valueGet(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 32)
vRef := le.Uint64(stack)
pAddr := le.Uint32(stack[8:])
pLen := le.Uint32(stack[16:])
p := string(mustRead(mem, "p", pAddr, pLen))
v := loadValue(ctx, ref(vRef))
func valueGet(ctx context.Context, mod api.Module, stack goos.Stack) {
v := stack.ParamVal(ctx, 0, loadValue)
p := stack.ParamString(mod.Memory(), 1 /*, 2 */)
var result interface{}
if g, ok := v.(jsGet); ok {
@@ -111,10 +94,8 @@ func valueGet(ctx context.Context, mod api.Module, sp []uint64) {
panic(fmt.Errorf("TODO: valueGet(v=%v, p=%s)", v, p))
}
ref := storeRef(ctx, result)
// Write the results to memory at positions after the parameters.
le.PutUint64(stack[24:], ref)
r := storeRef(ctx, result)
stack.SetResultRef(0, r)
}
// ValueSet implements js.valueSet, which is used to store a js.Value property
@@ -123,23 +104,17 @@ func valueGet(ctx context.Context, mod api.Module, sp []uint64) {
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L309
// and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L318-L322
var ValueSet = newSPFunc(valueSetName, valueSet)
var ValueSet = goos.NewFunc(valueSetName, valueSet,
[]string{"v", "p", "p_len", "x"},
[]string{},
)
func valueSet(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
func valueSet(ctx context.Context, mod api.Module, stack goos.Stack) {
v := stack.ParamVal(ctx, 0, loadValue)
p := stack.ParamString(mod.Memory(), 1 /*, 2 */)
x := stack.ParamVal(ctx, 3, loadValue)
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 32)
vRef := le.Uint64(stack)
pAddr := le.Uint32(stack[8:])
pLen := le.Uint32(stack[16:])
xRef := le.Uint64(stack[24:])
v := loadValue(ctx, ref(vRef))
p := string(mustRead(mem, "p", pAddr, pLen))
x := loadValue(ctx, ref(xRef))
if v == getState(ctx) {
if p := p; v == getState(ctx) {
switch p {
case "_pendingEvent":
if x == nil { // syscall_js.handleEvent
@@ -163,7 +138,7 @@ func valueSet(ctx context.Context, mod api.Module, sp []uint64) {
// ValueDelete is stubbed as it isn't used in Go's main source tree.
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L321
var ValueDelete = stubFunction(valueDeleteName)
var ValueDelete = goarch.StubFunction(valueDeleteName)
// ValueIndex implements js.valueIndex, which is used to load a js.Value property
// by index, e.g. `v.Index(0)`. Notably, this is used by js.handleEvent to read
@@ -171,145 +146,124 @@ var ValueDelete = stubFunction(valueDeleteName)
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L334
// and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L331-L334
var ValueIndex = newSPFunc(valueIndexName, valueIndex)
var ValueIndex = goos.NewFunc(valueIndexName, valueIndex,
[]string{"v", "i"},
[]string{"r"},
)
func valueIndex(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
func valueIndex(ctx context.Context, _ api.Module, stack goos.Stack) {
v := stack.ParamVal(ctx, 0, loadValue)
i := stack.ParamUint32(1)
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 24)
vRef := le.Uint64(stack)
i := le.Uint32(stack[8:])
v := loadValue(ctx, ref(vRef))
result := v.(*objectArray).slice[i]
ref := storeRef(ctx, result)
// Write the results to memory at positions after the parameters.
le.PutUint64(stack[16:], ref)
r := storeRef(ctx, result)
stack.SetResultRef(0, r)
}
// ValueSetIndex is stubbed as it is only used for js.ValueOf when the input is
// []interface{}, which doesn't appear to occur in Go's source tree.
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L348
var ValueSetIndex = stubFunction(valueSetIndexName)
var ValueSetIndex = goarch.StubFunction(valueSetIndexName)
// ValueCall implements js.valueCall, which is used to call a js.Value function
// by name, e.g. `document.Call("createElement", "div")`.
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L394
// and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L343-L358
var ValueCall = newSPFunc(valueCallName, valueCall)
var ValueCall = goos.NewFunc(valueCallName, valueCall,
[]string{"v", "m", "m_len", "args", "args_len", "padding"},
[]string{"res", "ok"},
)
func valueCall(ctx context.Context, mod api.Module, sp []uint64) {
func valueCall(ctx context.Context, mod api.Module, stack goos.Stack) {
mem := mod.Memory()
vRef := stack.ParamRef(0)
m := stack.ParamString(mem, 1 /*, 2 */)
args := stack.ParamVals(ctx, mem, 3 /*, 4 */, loadValue)
// 5 = padding
// Read param count * 8 memory starting at SP+8
params := mustRead(mem, "sp", uint32(sp[0]+8), 40)
vRef := le.Uint64(params)
mAddr := le.Uint32(params[8:])
mLen := le.Uint32(params[16:])
argsArray := le.Uint32(params[24:])
argsLen := le.Uint32(params[32:])
this := ref(vRef)
v := loadValue(ctx, this)
m := string(mustRead(mem, "m", mAddr, mLen))
args := loadArgs(ctx, mod, argsArray, argsLen)
var xRef uint64
var ok uint32
if c, isCall := v.(jsCall); !isCall {
panic(fmt.Errorf("TODO: valueCall(v=%v, m=%v, args=%v)", v, m, args))
} else if result, err := c.call(ctx, mod, this, m, args...); err != nil {
xRef = storeRef(ctx, err)
ok = 0
} else {
xRef = storeRef(ctx, result)
ok = 1
v := loadValue(ctx, vRef)
c, isCall := v.(jsCall)
if !isCall {
panic(fmt.Errorf("TODO: valueCall(v=%v, m=%s, args=%v)", v, m, args))
}
// On refresh, start to write results 16 bytes after the last parameter.
results := mustRead(mem, "sp", refreshSP(mod)+56, 16)
var res goos.Ref
var ok bool
if result, err := c.call(ctx, mod, vRef, m, args...); err != nil {
res = storeRef(ctx, err)
} else {
res = storeRef(ctx, result)
ok = true
}
// Write the results back to the stack
le.PutUint64(results, xRef)
le.PutUint32(results[8:], ok)
stack.Refresh(mod)
stack.SetResultRef(0, res)
stack.SetResultBool(1, ok)
}
// ValueInvoke is stubbed as it isn't used in Go's main source tree.
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L413
var ValueInvoke = stubFunction(valueInvokeName)
var ValueInvoke = goarch.StubFunction(valueInvokeName)
// ValueNew implements js.valueNew, which is used to call a js.Value, e.g.
// `array.New(2)`.
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L432
// and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L380-L391
var ValueNew = newSPFunc(valueNewName, valueNew)
var ValueNew = goos.NewFunc(valueNewName, valueNew,
[]string{"v", "args", "args_len", "padding"},
[]string{"res", "ok"},
)
func valueNew(ctx context.Context, mod api.Module, sp []uint64) {
func valueNew(ctx context.Context, mod api.Module, stack goos.Stack) {
mem := mod.Memory()
vRef := stack.ParamRef(0)
args := stack.ParamVals(ctx, mem, 1 /*, 2 */, loadValue)
// 3 = padding
// Read param count * 8 memory starting at SP+8
params := mustRead(mem, "sp", uint32(sp[0]+8), 24)
vRef := le.Uint64(params)
argsArray := le.Uint32(params[8:])
argsLen := le.Uint32(params[16:])
args := loadArgs(ctx, mod, argsArray, argsLen)
ref := ref(vRef)
v := loadValue(ctx, ref)
var xRef uint64
var ok uint32
switch ref {
var res goos.Ref
var ok bool
switch vRef {
case refArrayConstructor:
result := &objectArray{}
xRef = storeRef(ctx, result)
ok = 1
res = storeRef(ctx, result)
ok = true
case refUint8ArrayConstructor:
var result *byteArray
if n, ok := args[0].(float64); ok {
var result interface{}
a := args[0]
if n, ok := a.(float64); ok {
result = &byteArray{make([]byte, uint32(n))}
} else if n, ok := args[0].(uint32); ok {
result = &byteArray{make([]byte, n)}
} else if b, ok := args[0].(*byteArray); ok {
// In case of below, in HTTP, return the same ref
} else if _, ok := a.(*byteArray); ok {
// In case of wrapping, increment the counter of the same ref.
// uint8arrayWrapper := uint8Array.New(args[0])
result = b
result = stack.ParamRefs(mem, 1)[0]
} else {
panic(fmt.Errorf("TODO: valueNew(v=%v, args=%v)", v, args))
panic(fmt.Errorf("TODO: valueNew(v=%v, args=%v)", vRef, args))
}
xRef = storeRef(ctx, result)
ok = 1
res = storeRef(ctx, result)
ok = true
case refObjectConstructor:
result := &object{properties: map[string]interface{}{}}
xRef = storeRef(ctx, result)
ok = 1
res = storeRef(ctx, result)
ok = true
case refHttpHeadersConstructor:
result := &headers{headers: http.Header{}}
xRef = storeRef(ctx, result)
ok = 1
res = storeRef(ctx, result)
ok = true
case refJsDateConstructor:
xRef = uint64(refJsDate)
ok = 1
res = refJsDate
ok = true
default:
panic(fmt.Errorf("TODO: valueNew(v=%v, args=%v)", v, args))
panic(fmt.Errorf("TODO: valueNew(v=%v, args=%v)", vRef, args))
}
// On refresh, start to write results 16 bytes after the last parameter.
results := mustRead(mem, "sp", refreshSP(mod)+40, 16)
// Write the results back to the stack
le.PutUint64(results, xRef)
le.PutUint32(results[8:], ok)
stack.Refresh(mod)
stack.SetResultRef(0, res)
stack.SetResultBool(1, ok)
}
// ValueLength implements js.valueLength, which is used to load the length
@@ -317,21 +271,17 @@ func valueNew(ctx context.Context, mod api.Module, sp []uint64) {
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L372
// and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L396-L397
var ValueLength = newSPFunc(valueLengthName, valueLength)
var ValueLength = goos.NewFunc(valueLengthName, valueLength,
[]string{"v"},
[]string{"len"},
)
func valueLength(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
func valueLength(ctx context.Context, _ api.Module, stack goos.Stack) {
v := stack.ParamVal(ctx, 0, loadValue)
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 16)
len := len(v.(*objectArray).slice)
vRef := le.Uint64(stack)
v := loadValue(ctx, ref(vRef))
l := uint32(len(v.(*objectArray).slice))
// Write the results to memory at positions after the parameters.
le.PutUint32(stack[8:], l)
stack.SetResultUint32(0, uint32(len))
}
// ValuePrepareString implements js.valuePrepareString, which is used to load
@@ -341,25 +291,21 @@ func valueLength(ctx context.Context, mod api.Module, sp []uint64) {
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L531
// and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L402-L405
var ValuePrepareString = newSPFunc(valuePrepareStringName, valuePrepareString)
var ValuePrepareString = goos.NewFunc(valuePrepareStringName, valuePrepareString,
[]string{"v"},
[]string{"str", "length"},
)
func valuePrepareString(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
func valuePrepareString(ctx context.Context, _ api.Module, stack goos.Stack) {
v := stack.ParamVal(ctx, 0, loadValue)
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 24)
vRef := le.Uint64(stack)
v := loadValue(ctx, ref(vRef))
s := valueString(v)
sRef := storeRef(ctx, s)
sLen := uint32(len(s))
// Write the results to memory at positions after the parameters.
le.PutUint64(stack[8:], sRef)
le.PutUint32(stack[16:], sLen)
stack.SetResultRef(0, sRef)
stack.SetResultUint32(1, sLen)
}
// ValueLoadString implements js.valueLoadString, which is used copy a string
@@ -368,28 +314,23 @@ func valuePrepareString(ctx context.Context, mod api.Module, sp []uint64) {
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L533
//
// https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L410-L412
var ValueLoadString = newSPFunc(valueLoadStringName, valueLoadString)
var ValueLoadString = goos.NewFunc(valueLoadStringName, valueLoadString,
[]string{"v", "b", "b_len"},
[]string{},
)
func valueLoadString(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
func valueLoadString(ctx context.Context, mod api.Module, stack goos.Stack) {
v := stack.ParamVal(ctx, 0, loadValue)
b := stack.ParamBytes(mod.Memory(), 1 /*, 2 */)
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 24)
vRef := le.Uint64(stack)
bAddr := le.Uint32(stack[8:])
bLen := le.Uint32(stack[16:])
v := loadValue(ctx, ref(vRef))
s := valueString(v)
b := mustRead(mem, "b", bAddr, bLen)
copy(b, s)
}
// ValueInstanceOf is stubbed as it isn't used in Go's main source tree.
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L543
var ValueInstanceOf = stubFunction(valueInstanceOfName)
var ValueInstanceOf = goarch.StubFunction(valueInstanceOfName)
// CopyBytesToGo copies a JavaScript managed byte array to linear memory.
// For example, this is used to read an HTTP response body.
@@ -401,31 +342,25 @@ var ValueInstanceOf = stubFunction(valueInstanceOfName)
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L569
// and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L424-L433
var CopyBytesToGo = newSPFunc(copyBytesToGoName, copyBytesToGo)
var CopyBytesToGo = goos.NewFunc(copyBytesToGoName, copyBytesToGo,
[]string{"dst", "dst_len", "padding", "src"},
[]string{"n", "ok"},
)
func copyBytesToGo(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
func copyBytesToGo(ctx context.Context, mod api.Module, stack goos.Stack) {
dst := stack.ParamBytes(mod.Memory(), 0 /*, 1 */)
// padding = 2
src := stack.ParamVal(ctx, 3, loadValue)
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 48)
dstAddr := le.Uint32(stack)
dstLen := le.Uint32(stack[8:])
/* unknown := le.Uint32(stack[16:]) */
srcRef := le.Uint64(stack[24:])
dst := mustRead(mem, "dst", dstAddr, dstLen)
v := loadValue(ctx, ref(srcRef))
var n, ok uint32
if src, isBuf := v.(*byteArray); isBuf {
var n uint32
var ok bool
if src, isBuf := src.(*byteArray); isBuf {
n = uint32(copy(dst, src.slice))
ok = 1
ok = true
}
// Write the results to memory at positions after the parameters.
le.PutUint32(stack[32:], n)
le.PutUint32(stack[40:], ok)
stack.SetResultUint32(0, n)
stack.SetResultBool(1, ok)
}
// CopyBytesToJS copies linear memory to a JavaScript managed byte array.
@@ -438,43 +373,27 @@ func copyBytesToGo(ctx context.Context, mod api.Module, sp []uint64) {
//
// See https://github.com/golang/go/blob/go1.19/src/syscall/js/js.go#L583
// and https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L438-L448
var CopyBytesToJS = newSPFunc(copyBytesToJSName, copyBytesToJS)
var CopyBytesToJS = goos.NewFunc(copyBytesToJSName, copyBytesToJS,
[]string{"dst", "src", "src_len", "padding"},
[]string{"n", "ok"},
)
func copyBytesToJS(ctx context.Context, mod api.Module, sp []uint64) {
mem := mod.Memory()
func copyBytesToJS(ctx context.Context, mod api.Module, stack goos.Stack) {
dst := stack.ParamVal(ctx, 0, loadValue)
src := stack.ParamBytes(mod.Memory(), 1 /*, 2 */)
// padding = 3
// Read (param + result count) * 8 memory starting at SP+8
stack := mustRead(mem, "sp", uint32(sp[0]+8), 48)
dstRef := le.Uint64(stack)
srcAddr := le.Uint32(stack[8:])
srcLen := le.Uint32(stack[16:])
/* unknown := le.Uint32(stack[24:]) */
src := mustRead(mem, "src", srcAddr, srcLen) // nolint
v := loadValue(ctx, ref(dstRef))
var n, ok uint32
if dst, isBuf := v.(*byteArray); isBuf {
var n uint32
var ok bool
if dst, isBuf := dst.(*byteArray); isBuf {
if dst != nil { // empty is possible on EOF
n = uint32(copy(dst.slice, src))
}
ok = 1
ok = true
}
// Write the results to memory at positions after the parameters.
le.PutUint32(stack[32:], n)
le.PutUint32(stack[40:], ok)
}
// refreshSP refreshes the stack pointer, which is needed prior to storeValue
// when in an operation that can trigger a Go event handler.
//
// See https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L210-L213
func refreshSP(mod api.Module) uint32 {
// Cheat by reading global[0] directly instead of through a function proxy.
// https://github.com/golang/go/blob/go1.19/src/runtime/rt0_js_wasm.s#L87-L90
return uint32(mod.(*wasm.CallContext).GlobalVal(0))
stack.SetResultUint32(0, n)
stack.SetResultBool(1, ok)
}
// syscallErr is a (GOARCH=wasm) error, which must match a key in mapJSError.
@@ -536,7 +455,7 @@ type funcWrapper uint32
// jsFn implements jsFn.invoke
func (f funcWrapper) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
e := &event{id: uint32(f), this: args[0].(ref)}
e := &event{id: uint32(f), this: args[0].(goos.Ref)}
if len(args) > 1 { // Ensure arguments are hashable.
e.args = &objectArray{args[1:]}
@@ -563,13 +482,3 @@ func (f funcWrapper) invoke(ctx context.Context, mod api.Module, args ...interfa
return e.result, nil
}
func newSPFunc(name string, goFunc api.GoModuleFunc) *wasm.HostFunc {
return &wasm.HostFunc{
ExportNames: []string{name},
Name: name,
ParamTypes: []api.ValueType{api.ValueTypeI32},
ParamNames: []string{"sp"},
Code: &wasm.Code{IsHostFunction: true, GoFunc: goFunc},
}
}

View File

@@ -21,6 +21,6 @@ var (
type getTimezoneOffset struct{}
// invoke implements jsFn.invoke
func (*getTimezoneOffset) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
func (*getTimezoneOffset) invoke(context.Context, api.Module, ...interface{}) (interface{}, error) {
return uint32(0), nil // UTC
}

View File

@@ -1,70 +0,0 @@
package gojs
import (
"fmt"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasm"
)
// Debug has unknown use, so stubbed.
//
// See https://github.com/golang/go/blob/go1.19/src/cmd/link/internal/wasm/asm.go#L133-L138
var Debug = stubFunction(functionDebug)
// stubFunction stubs functions not used in Go's main source tree.
func stubFunction(name string) *wasm.HostFunc {
return &wasm.HostFunc{
ExportNames: []string{name},
Name: name,
ParamTypes: []wasm.ValueType{wasm.ValueTypeI32},
ParamNames: []string{parameterSp},
Code: &wasm.Code{IsHostFunction: true, Body: []byte{wasm.OpcodeUnreachable, wasm.OpcodeEnd}},
}
}
// mustRead is like api.Memory except that it panics if the offset and
// byteCount are out of range.
func mustRead(mem api.Memory, fieldName string, offset, byteCount uint32) []byte {
buf, ok := mem.Read(offset, byteCount)
if !ok {
panic(fmt.Errorf("out of memory reading %s", fieldName))
}
return buf
}
// mustReadUint32Le is like api.Memory except that it panics if the offset
// is out of range.
func mustReadUint32Le(mem api.Memory, fieldName string, offset uint32) uint32 {
result, ok := mem.ReadUint32Le(offset)
if !ok {
panic(fmt.Errorf("out of memory reading %s", fieldName))
}
return result
}
// mustReadUint64Le is like api.Memory except that it panics if the offset
// is out of range.
func mustReadUint64Le(mem api.Memory, fieldName string, offset uint32) uint64 {
result, ok := mem.ReadUint64Le(offset)
if !ok {
panic(fmt.Errorf("out of memory reading %s", fieldName))
}
return result
}
// mustWrite is like api.Memory except that it panics if the offset
// is out of range.
func mustWrite(mem api.Memory, fieldName string, offset uint32, val []byte) {
if ok := mem.Write(offset, val); !ok {
panic(fmt.Errorf("out of memory writing %s", fieldName))
}
}
// mustWriteUint64Le is like api.Memory except that it panics if the offset
// is out of range.
func mustWriteUint64Le(mem api.Memory, fieldName string, offset uint32, val uint64) {
if ok := mem.WriteUint64Le(offset, val); !ok {
panic(fmt.Errorf("out of memory writing %s", fieldName))
}
}

View File

@@ -0,0 +1,36 @@
package util
import (
"fmt"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasm"
)
// MustWrite is like api.Memory except that it panics if the offset
// is out of range.
func MustWrite(mem api.Memory, fieldName string, offset uint32, val []byte) {
if ok := mem.Write(offset, val); !ok {
panic(fmt.Errorf("out of memory writing %s", fieldName))
}
}
// MustRead is like api.Memory except that it panics if the offset and
// byteCount are out of range.
func MustRead(mem api.Memory, fieldName string, offset, byteCount uint32) []byte {
buf, ok := mem.Read(offset, byteCount)
if !ok {
panic(fmt.Errorf("out of memory reading %s", fieldName))
}
return buf
}
func NewFunc(name string, goFunc api.GoModuleFunc) *wasm.HostFunc {
return &wasm.HostFunc{
ExportNames: []string{name},
Name: name,
ParamTypes: []api.ValueType{api.ValueTypeI32},
ParamNames: []string{"sp"},
Code: &wasm.Code{IsHostFunction: true, GoFunc: goFunc},
}
}