Adds assemblyscript host module and example (#569)
Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com> Co-authored-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
7
.github/workflows/examples.yaml
vendored
7
.github/workflows/examples.yaml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- '.github/workflows/examples.yaml'
|
||||
- 'examples/*/testdata/*.go'
|
||||
- 'examples/*/*/testdata/*.go'
|
||||
- 'examples/*/testdata/*.ts'
|
||||
- 'Makefile'
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -31,9 +32,13 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# TinyGo -> Wasm is not idempotent, so we only check things build.
|
||||
- name: Build examples
|
||||
- name: Build TinyGO examples
|
||||
run: make build.examples
|
||||
|
||||
# AssemblyScript -> Wasm is not idempotent, so we only check things build.
|
||||
- name: Build AssemblyScript examples
|
||||
run: make build.examples.as
|
||||
|
||||
# TinyGo -> Wasm is not idempotent, so we only check things build.
|
||||
- name: Build bench cases
|
||||
run: make build.bench
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -22,3 +22,7 @@ go.work
|
||||
|
||||
# Goland
|
||||
.idea
|
||||
|
||||
# AssemblyScript
|
||||
node_modules
|
||||
package-lock.json
|
||||
|
||||
4
Makefile
4
Makefile
@@ -24,6 +24,10 @@ bench_testdata_dir := internal/integration_test/bench/testdata
|
||||
build.bench:
|
||||
@tinygo build -o $(bench_testdata_dir)/case.wasm -scheduler=none --no-debug -target=wasi $(bench_testdata_dir)/case.go
|
||||
|
||||
.PHONY: build.examples.as
|
||||
build.examples.as:
|
||||
@cd ./examples/assemblyscript/testdata && npm install && npm run build
|
||||
|
||||
tinygo_sources := $(wildcard examples/*/testdata/*.go examples/*/*/testdata/*.go)
|
||||
.PHONY: build.examples
|
||||
build.examples: $(tinygo_sources)
|
||||
|
||||
248
assemblyscript/assemblyscript.go
Normal file
248
assemblyscript/assemblyscript.go
Normal file
@@ -0,0 +1,248 @@
|
||||
// Package assemblyscript contains Go-defined special functions imported by AssemblyScript under the module name "env".
|
||||
//
|
||||
// Note: Some code will only import "env.abort", but even that isn't imported when "import wasi" is used in the source.
|
||||
// See https://www.assemblyscript.org/concepts.html#special-imports
|
||||
package assemblyscript
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/internal/ieee754"
|
||||
"github.com/tetratelabs/wazero/internal/wasm"
|
||||
)
|
||||
|
||||
// Instantiate instantiates a module implementing special functions defined by AssemblyScript:
|
||||
// * "env.abort" - exits with 255 with an abort message written to wazero.ModuleConfig WithStderr.
|
||||
// * "env.trace" - no output unless.
|
||||
// * "env.seed" - uses wazero.ModuleConfig WithRandSource as the source of seed values.
|
||||
//
|
||||
// Note: To customize behavior, use NewModuleBuilder instead.
|
||||
// Note: If the AssemblyScript program is configured to use WASI, by calling "import wasi" in any file, these
|
||||
// functions will not be used.
|
||||
// See NewModuleBuilder
|
||||
// See wasi.InstantiateSnapshotPreview1
|
||||
func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) {
|
||||
return NewModuleBuilder(r).Instantiate(ctx)
|
||||
}
|
||||
|
||||
// ModuleBuilder allows configuring the module that will export functions used automatically by AssemblyScript.
|
||||
type ModuleBuilder interface {
|
||||
// WithAbortMessageDisabled configures the AssemblyScript abort function to discard any message.
|
||||
WithAbortMessageDisabled() ModuleBuilder
|
||||
|
||||
// WithTraceToStdout configures the AssemblyScript trace function to output messages to Stdout, as configured by
|
||||
// wazero.ModuleConfig WithStdout.
|
||||
WithTraceToStdout() ModuleBuilder
|
||||
|
||||
// WithTraceToStderr configures the AssemblyScript trace function to output messages to Stderr, as configured by
|
||||
// wazero.ModuleConfig WithStderr. Because of the potential volume of trace messages, it is often more appropriate
|
||||
// to use WithTraceToStdout instead.
|
||||
WithTraceToStderr() ModuleBuilder
|
||||
|
||||
// Instantiate instantiates the module so that AssemblyScript can import from it.
|
||||
Instantiate(context.Context) (api.Closer, error)
|
||||
}
|
||||
|
||||
// NewModuleBuilder is an alternative to Instantiate which allows customization via ModuleBuilder.
|
||||
func NewModuleBuilder(r wazero.Runtime) ModuleBuilder {
|
||||
return &moduleBuilder{r: r, traceMode: traceDisabled}
|
||||
}
|
||||
|
||||
type traceMode int
|
||||
|
||||
const (
|
||||
traceDisabled traceMode = 0
|
||||
traceStdout traceMode = 1
|
||||
traceStderr traceMode = 2
|
||||
)
|
||||
|
||||
type moduleBuilder struct {
|
||||
r wazero.Runtime
|
||||
abortMessageDisabled bool
|
||||
traceMode traceMode
|
||||
}
|
||||
|
||||
// WithAbortMessageDisabled implements ModuleBuilder.WithAbortMessageDisabled
|
||||
func (m *moduleBuilder) WithAbortMessageDisabled() ModuleBuilder {
|
||||
ret := *m // copy
|
||||
ret.abortMessageDisabled = true
|
||||
return &ret
|
||||
}
|
||||
|
||||
// WithTraceToStdout implements ModuleBuilder.WithTraceToStdout
|
||||
func (m *moduleBuilder) WithTraceToStdout() ModuleBuilder {
|
||||
ret := *m // copy
|
||||
ret.traceMode = traceStdout
|
||||
return &ret
|
||||
}
|
||||
|
||||
// WithTraceToStderr implements ModuleBuilder.WithTraceToStderr
|
||||
func (m *moduleBuilder) WithTraceToStderr() ModuleBuilder {
|
||||
ret := *m // copy
|
||||
ret.traceMode = traceStderr
|
||||
return &ret
|
||||
}
|
||||
|
||||
// Instantiate implements ModuleBuilder.Instantiate
|
||||
func (m *moduleBuilder) Instantiate(ctx context.Context) (api.Closer, error) {
|
||||
env := &assemblyscript{abortMessageDisabled: m.abortMessageDisabled, traceMode: m.traceMode}
|
||||
return m.r.NewModuleBuilder("env").
|
||||
ExportFunction("abort", env.abort).
|
||||
ExportFunction("trace", env.trace).
|
||||
ExportFunction("seed", env.seed).
|
||||
Instantiate(ctx)
|
||||
}
|
||||
|
||||
// assemblyscript implements the AssemblyScript special functions.
|
||||
type assemblyscript struct {
|
||||
abortMessageDisabled bool
|
||||
traceMode traceMode
|
||||
}
|
||||
|
||||
// abort is called on unrecoverable errors. This is typically present in Wasm compiled from AssemblyScript, if
|
||||
// assertions are enabled or errors are thrown.
|
||||
//
|
||||
// The implementation writes the message to stderr, unless abortMessageDisabled, and closes the module with exit code
|
||||
// 255.
|
||||
//
|
||||
// Here's the import in a user's module that ends up using this, in WebAssembly 1.0 (MVP) Text Format:
|
||||
// (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
|
||||
//
|
||||
// See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L18
|
||||
func (a *assemblyscript) abort(
|
||||
ctx context.Context,
|
||||
mod api.Module,
|
||||
message uint32,
|
||||
fileName uint32,
|
||||
lineNumber uint32,
|
||||
columnNumber uint32,
|
||||
) {
|
||||
if !a.abortMessageDisabled {
|
||||
sys := sysCtx(mod)
|
||||
msg, err := readAssemblyScriptString(ctx, mod, message)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fn, err := readAssemblyScriptString(ctx, mod, fileName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprintf(sys.Stderr(), "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber)
|
||||
}
|
||||
_ = mod.CloseWithExitCode(ctx, 255)
|
||||
}
|
||||
|
||||
// trace implements the same named function in AssemblyScript (ex. trace('Hello World!'))
|
||||
//
|
||||
// Here's the import in a user's module that ends up using this, in WebAssembly 1.0 (MVP) Text Format:
|
||||
// (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64)))
|
||||
//
|
||||
// See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L61
|
||||
func (a *assemblyscript) trace(
|
||||
ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64,
|
||||
) {
|
||||
var writer io.Writer
|
||||
switch a.traceMode {
|
||||
case traceDisabled:
|
||||
return
|
||||
case traceStdout:
|
||||
writer = sysCtx(mod).Stdout()
|
||||
case traceStderr:
|
||||
writer = sysCtx(mod).Stderr()
|
||||
}
|
||||
|
||||
msg, err := readAssemblyScriptString(ctx, mod, message)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var ret strings.Builder
|
||||
ret.WriteString("trace: ")
|
||||
ret.WriteString(msg)
|
||||
if nArgs >= 1 {
|
||||
ret.WriteString(" ")
|
||||
ret.WriteString(formatFloat(arg0))
|
||||
}
|
||||
if nArgs >= 2 {
|
||||
ret.WriteString(",")
|
||||
ret.WriteString(formatFloat(arg1))
|
||||
}
|
||||
if nArgs >= 3 {
|
||||
ret.WriteString(",")
|
||||
ret.WriteString(formatFloat(arg2))
|
||||
}
|
||||
if nArgs >= 4 {
|
||||
ret.WriteString(",")
|
||||
ret.WriteString(formatFloat(arg3))
|
||||
}
|
||||
if nArgs >= 5 {
|
||||
ret.WriteString(",")
|
||||
ret.WriteString(formatFloat(arg4))
|
||||
}
|
||||
ret.WriteByte('\n')
|
||||
_, err = writer.Write([]byte(ret.String()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func formatFloat(f float64) string {
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
// seed is called when the AssemblyScript's random number generator needs to be seeded
|
||||
//
|
||||
// Here's the import in a user's module that ends up using this, in WebAssembly 1.0 (MVP) Text Format:
|
||||
// (import "env" "seed" (func $~lib/builtins/seed (result f64)))
|
||||
//
|
||||
// See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L111
|
||||
func (a *assemblyscript) seed(mod api.Module) float64 {
|
||||
source := sysCtx(mod).RandSource()
|
||||
v, err := ieee754.DecodeFloat64(source)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("error reading Module.RandSource: %w", err))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// readAssemblyScriptString reads a UTF-16 string created by AssemblyScript.
|
||||
func readAssemblyScriptString(ctx context.Context, m api.Module, offset uint32) (string, error) {
|
||||
// Length is four bytes before pointer.
|
||||
byteCount, ok := m.Memory().ReadUint32Le(ctx, offset-4)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Memory.ReadUint32Le(%d) out of range", offset-4)
|
||||
}
|
||||
if byteCount%2 != 0 {
|
||||
return "", fmt.Errorf("read an odd number of bytes for utf-16 string: %d", byteCount)
|
||||
}
|
||||
buf, ok := m.Memory().Read(ctx, offset, byteCount)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Memory.Read(%d, %d) out of range", offset, byteCount)
|
||||
}
|
||||
return decodeUTF16(buf), nil
|
||||
}
|
||||
|
||||
func decodeUTF16(b []byte) string {
|
||||
u16s := make([]uint16, len(b)/2)
|
||||
|
||||
lb := len(b)
|
||||
for i := 0; i < lb; i += 2 {
|
||||
u16s[i/2] = uint16(b[i]) + (uint16(b[i+1]) << 8)
|
||||
}
|
||||
|
||||
return string(utf16.Decode(u16s))
|
||||
}
|
||||
|
||||
func sysCtx(m api.Module) *wasm.SysContext {
|
||||
if internal, ok := m.(*wasm.CallContext); !ok {
|
||||
panic(fmt.Errorf("unsupported wasm.Module implementation: %v", m))
|
||||
} else {
|
||||
return internal.Sys
|
||||
}
|
||||
}
|
||||
464
assemblyscript/assemblyscript_test.go
Normal file
464
assemblyscript/assemblyscript_test.go
Normal file
@@ -0,0 +1,464 @@
|
||||
package assemblyscript
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
)
|
||||
|
||||
var abortWasm = []byte(`(module
|
||||
(import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
|
||||
(memory 1 1)
|
||||
(export "abort" (func 0))
|
||||
)`)
|
||||
|
||||
var seedWasm = []byte(`(module
|
||||
(import "env" "seed" (func $~lib/builtins/seed (result f64)))
|
||||
(memory 1 1)
|
||||
(export "seed" (func 0))
|
||||
)`)
|
||||
|
||||
var traceWasm = []byte(`(module
|
||||
(import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64)))
|
||||
(memory 1 1)
|
||||
(export "trace" (func 0))
|
||||
)`)
|
||||
|
||||
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
|
||||
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
|
||||
|
||||
func TestAbort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
enabled bool
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "enabled",
|
||||
enabled: true,
|
||||
expected: "message at filename:1:2\n",
|
||||
},
|
||||
{
|
||||
name: "disabled",
|
||||
enabled: false,
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := wazero.NewRuntime()
|
||||
defer r.Close(testCtx)
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
|
||||
if tc.enabled {
|
||||
_, err := Instantiate(testCtx, r)
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
_, err := NewModuleBuilder(r).WithAbortMessageDisabled().Instantiate(testCtx)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
code, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
mod, err := r.InstantiateModule(testCtx, code, wazero.NewModuleConfig().WithStderr(out))
|
||||
require.NoError(t, err)
|
||||
|
||||
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename"))
|
||||
|
||||
_, err = mod.ExportedFunction("abort").Call(testCtx, uint64(messageOff), uint64(filenameOff), 1, 2)
|
||||
require.Error(t, err)
|
||||
|
||||
require.Equal(t, tc.expected, out.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeed(t *testing.T) {
|
||||
r := wazero.NewRuntime()
|
||||
defer r.Close(testCtx)
|
||||
|
||||
seed := []byte{0, 1, 2, 3, 4, 5, 6, 7}
|
||||
|
||||
_, err := Instantiate(testCtx, r)
|
||||
require.NoError(t, err)
|
||||
|
||||
code, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
mod, err := r.InstantiateModule(testCtx, code, wazero.NewModuleConfig().WithRandSource(bytes.NewReader(seed)))
|
||||
require.NoError(t, err)
|
||||
|
||||
seedFn := mod.ExportedFunction("seed")
|
||||
|
||||
res, err := seedFn.Call(testCtx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// If this test doesn't break, the seed is deterministic.
|
||||
require.Equal(t, uint64(506097522914230528), res[0])
|
||||
}
|
||||
|
||||
func TestTrace(t *testing.T) {
|
||||
noArgs := []uint64{4, 0, 0, 0, 0, 0, 0}
|
||||
tests := []struct {
|
||||
name string
|
||||
mode traceMode
|
||||
params []uint64
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "stderr",
|
||||
mode: traceStderr,
|
||||
params: noArgs,
|
||||
expected: "trace: hello\n",
|
||||
},
|
||||
{
|
||||
name: "stdout",
|
||||
mode: traceStdout,
|
||||
params: noArgs,
|
||||
expected: "trace: hello\n",
|
||||
},
|
||||
{
|
||||
name: "disabled",
|
||||
mode: traceDisabled,
|
||||
params: noArgs,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "one",
|
||||
mode: traceStdout,
|
||||
params: []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0},
|
||||
expected: "trace: hello 1\n",
|
||||
},
|
||||
{
|
||||
name: "two",
|
||||
mode: traceStdout,
|
||||
params: []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0},
|
||||
expected: "trace: hello 1,2\n",
|
||||
},
|
||||
{
|
||||
name: "five",
|
||||
mode: traceStdout,
|
||||
params: []uint64{
|
||||
4,
|
||||
5,
|
||||
api.EncodeF64(1),
|
||||
api.EncodeF64(2),
|
||||
api.EncodeF64(3.3),
|
||||
api.EncodeF64(4.4),
|
||||
api.EncodeF64(5),
|
||||
},
|
||||
expected: "trace: hello 1,2,3.3,4.4,5\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := wazero.NewRuntime()
|
||||
defer r.Close(testCtx)
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
|
||||
as := NewModuleBuilder(r)
|
||||
modConfig := wazero.NewModuleConfig()
|
||||
switch tc.mode {
|
||||
case traceStderr:
|
||||
as = as.WithTraceToStderr()
|
||||
modConfig = modConfig.WithStderr(out)
|
||||
case traceStdout:
|
||||
as = as.WithTraceToStdout()
|
||||
modConfig = modConfig.WithStdout(out)
|
||||
case traceDisabled:
|
||||
// Set but not used
|
||||
modConfig = modConfig.WithStderr(out)
|
||||
modConfig = modConfig.WithStdout(out)
|
||||
}
|
||||
|
||||
_, err := as.Instantiate(testCtx)
|
||||
require.NoError(t, err)
|
||||
|
||||
code, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
mod, err := r.InstantiateModule(testCtx, code, modConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
message := encodeUTF16("hello")
|
||||
ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(message)))
|
||||
require.True(t, ok)
|
||||
ok = mod.Memory().Write(testCtx, uint32(4), message)
|
||||
require.True(t, ok)
|
||||
|
||||
_, err = mod.ExportedFunction("trace").Call(testCtx, tc.params...)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expected, out.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAssemblyScriptString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
memory func(api.Memory)
|
||||
offset int
|
||||
expected, expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
memory: func(memory api.Memory) {
|
||||
memory.WriteUint32Le(testCtx, 0, 10)
|
||||
b := encodeUTF16("hello")
|
||||
memory.Write(testCtx, 4, b)
|
||||
},
|
||||
offset: 4,
|
||||
expected: "hello",
|
||||
},
|
||||
{
|
||||
name: "can't read size",
|
||||
memory: func(memory api.Memory) {
|
||||
b := encodeUTF16("hello")
|
||||
memory.Write(testCtx, 0, b)
|
||||
},
|
||||
offset: 0, // will attempt to read size from offset -4
|
||||
expectedErr: "Memory.ReadUint32Le(4294967292) out of range",
|
||||
},
|
||||
{
|
||||
name: "odd size",
|
||||
memory: func(memory api.Memory) {
|
||||
memory.WriteUint32Le(testCtx, 0, 9)
|
||||
b := encodeUTF16("hello")
|
||||
memory.Write(testCtx, 4, b)
|
||||
},
|
||||
offset: 4,
|
||||
expectedErr: "read an odd number of bytes for utf-16 string: 9",
|
||||
},
|
||||
{
|
||||
name: "can't read string",
|
||||
memory: func(memory api.Memory) {
|
||||
memory.WriteUint32Le(testCtx, 0, 10_000_000) // set size to too large value
|
||||
b := encodeUTF16("hello")
|
||||
memory.Write(testCtx, 4, b)
|
||||
},
|
||||
offset: 4,
|
||||
expectedErr: "Memory.Read(4, 10000000) out of range",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := wazero.NewRuntime()
|
||||
defer r.Close(testCtx)
|
||||
|
||||
mod, err := r.NewModuleBuilder("mod").
|
||||
ExportMemory("memory", 1).
|
||||
Instantiate(testCtx)
|
||||
require.NoError(t, err)
|
||||
|
||||
tc.memory(mod.Memory())
|
||||
|
||||
s, err := readAssemblyScriptString(testCtx, mod, uint32(tc.offset))
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(t, err, tc.expectedErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expected, s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbort_error(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
messageUTF16 []byte
|
||||
fileNameUTF16 []byte
|
||||
}{
|
||||
{
|
||||
name: "bad message",
|
||||
messageUTF16: encodeUTF16("message")[:5],
|
||||
fileNameUTF16: encodeUTF16("filename"),
|
||||
},
|
||||
{
|
||||
name: "bad filename",
|
||||
messageUTF16: encodeUTF16("message"),
|
||||
fileNameUTF16: encodeUTF16("filename")[:5],
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := wazero.NewRuntime()
|
||||
defer r.Close(testCtx)
|
||||
|
||||
_, err := Instantiate(testCtx, r)
|
||||
require.NoError(t, err)
|
||||
|
||||
compiled, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
config := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(out)
|
||||
mod, err := r.InstantiateModule(testCtx, compiled, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
|
||||
|
||||
_, err = mod.ExportedFunction("abort").Call(testCtx, uint64(messageOff), uint64(filenameOff), 1, 2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", out.String()) // nothing output if strings fail to read.
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (int, int) {
|
||||
off := 0
|
||||
ok := mem.WriteUint32Le(testCtx, uint32(off), uint32(len(messageUTF16)))
|
||||
require.True(t, ok)
|
||||
off += 4
|
||||
messageOff := off
|
||||
ok = mem.Write(testCtx, uint32(off), messageUTF16)
|
||||
require.True(t, ok)
|
||||
off += len(messageUTF16)
|
||||
ok = mem.WriteUint32Le(testCtx, uint32(off), uint32(len(fileNameUTF16)))
|
||||
require.True(t, ok)
|
||||
off += 4
|
||||
filenameOff := off
|
||||
ok = mem.Write(testCtx, uint32(off), fileNameUTF16)
|
||||
require.True(t, ok)
|
||||
return messageOff, filenameOff
|
||||
}
|
||||
|
||||
func TestSeed_error(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source io.Reader
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "not 8 bytes",
|
||||
source: bytes.NewReader([]byte{0, 1}),
|
||||
expectedErr: `error reading Module.RandSource: unexpected EOF (recovered by wazero)
|
||||
wasm stack trace:
|
||||
env.seed() f64`,
|
||||
},
|
||||
{
|
||||
name: "error reading",
|
||||
source: iotest.ErrReader(errors.New("ice cream")),
|
||||
expectedErr: `error reading Module.RandSource: ice cream (recovered by wazero)
|
||||
wasm stack trace:
|
||||
env.seed() f64`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := wazero.NewRuntime()
|
||||
defer r.Close(testCtx)
|
||||
|
||||
_, err := Instantiate(testCtx, r)
|
||||
require.NoError(t, err)
|
||||
|
||||
compiled, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
config := wazero.NewModuleConfig().WithName(t.Name()).WithRandSource(tc.source)
|
||||
mod, err := r.InstantiateModule(testCtx, compiled, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = mod.ExportedFunction("seed").Call(testCtx)
|
||||
require.EqualError(t, err, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrace_error(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
message []byte
|
||||
out io.Writer
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "not 8 bytes",
|
||||
message: encodeUTF16("hello")[:5],
|
||||
out: &bytes.Buffer{},
|
||||
expectedErr: `read an odd number of bytes for utf-16 string: 5 (recovered by wazero)
|
||||
wasm stack trace:
|
||||
env.trace(i32,i32,f64,f64,f64,f64,f64)`,
|
||||
},
|
||||
{
|
||||
name: "error writing",
|
||||
message: encodeUTF16("hello"),
|
||||
out: &errWriter{err: errors.New("ice cream")},
|
||||
expectedErr: `ice cream (recovered by wazero)
|
||||
wasm stack trace:
|
||||
env.trace(i32,i32,f64,f64,f64,f64,f64)`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := wazero.NewRuntime()
|
||||
defer r.Close(testCtx)
|
||||
|
||||
_, err := NewModuleBuilder(r).WithTraceToStdout().Instantiate(testCtx)
|
||||
require.NoError(t, err)
|
||||
|
||||
compiled, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
config := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(tc.out)
|
||||
mod, err := r.InstantiateModule(testCtx, compiled, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(tc.message)))
|
||||
require.True(t, ok)
|
||||
ok = mod.Memory().Write(testCtx, uint32(4), tc.message)
|
||||
require.True(t, ok)
|
||||
|
||||
_, err = mod.ExportedFunction("trace").Call(testCtx, 4, 0, 0, 0, 0, 0, 0)
|
||||
require.EqualError(t, err, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func encodeUTF16(s string) []byte {
|
||||
runes := utf16.Encode([]rune(s))
|
||||
b := make([]byte, len(runes)*2)
|
||||
for i, r := range runes {
|
||||
b[i*2] = byte(r)
|
||||
b[i*2+1] = byte(r >> 8)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
type errWriter struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *errWriter) Write([]byte) (int, error) {
|
||||
return 0, w.err
|
||||
}
|
||||
18
config.go
18
config.go
@@ -416,6 +416,14 @@ type ModuleConfig interface {
|
||||
// See https://linux.die.net/man/3/stdout
|
||||
WithStdout(io.Writer) ModuleConfig
|
||||
|
||||
// WithRandSource configures a source of random bytes. Defaults to crypto/rand.Reader.
|
||||
//
|
||||
// This reader is most commonly used by the functions like "random_get" in "wasi_snapshot_preview1" or "seed" in
|
||||
// AssemblyScript standard "env" although it could be used by functions imported from other modules.
|
||||
//
|
||||
// Note: The caller is responsible to close any io.Reader they supply: It is not closed on api.Module Close.
|
||||
WithRandSource(io.Reader) ModuleConfig
|
||||
|
||||
// WithWorkDirFS indicates the file system to use for any paths beginning at "./". Defaults to the same as WithFS.
|
||||
//
|
||||
// Ex. This sets a read-only, embedded file-system as the root ("/"), and a mutable one as the working directory ("."):
|
||||
@@ -437,6 +445,7 @@ type moduleConfig struct {
|
||||
stdin io.Reader
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
randSource io.Reader
|
||||
args []string
|
||||
// environ is pair-indexed to retain order similar to os.Environ.
|
||||
environ []string
|
||||
@@ -523,6 +532,13 @@ func (c *moduleConfig) WithStdout(stdout io.Writer) ModuleConfig {
|
||||
return &ret
|
||||
}
|
||||
|
||||
// WithRandSource implements ModuleConfig.WithRandSource
|
||||
func (c *moduleConfig) WithRandSource(source io.Reader) ModuleConfig {
|
||||
ret := *c // copy
|
||||
ret.randSource = source
|
||||
return &ret
|
||||
}
|
||||
|
||||
// WithWorkDirFS implements ModuleConfig.WithWorkDirFS
|
||||
func (c *moduleConfig) WithWorkDirFS(fs fs.FS) ModuleConfig {
|
||||
ret := *c // copy
|
||||
@@ -582,5 +598,5 @@ func (c *moduleConfig) toSysContext() (sys *wasm.SysContext, err error) {
|
||||
preopens[c.preopenFD] = &wasm.FileEntry{Path: ".", FS: preopens[rootFD].FS}
|
||||
}
|
||||
|
||||
return wasm.NewSysContext(math.MaxUint32, c.args, environ, c.stdin, c.stdout, c.stderr, preopens)
|
||||
return wasm.NewSysContext(math.MaxUint32, c.args, environ, c.stdin, c.stdout, c.stderr, c.randSource, preopens)
|
||||
}
|
||||
|
||||
@@ -310,6 +310,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
),
|
||||
},
|
||||
@@ -323,6 +324,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
),
|
||||
},
|
||||
@@ -336,6 +338,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
),
|
||||
},
|
||||
@@ -349,6 +352,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
),
|
||||
},
|
||||
@@ -362,6 +366,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
),
|
||||
},
|
||||
@@ -375,6 +380,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
),
|
||||
},
|
||||
@@ -388,6 +394,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
),
|
||||
},
|
||||
@@ -401,6 +408,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
),
|
||||
},
|
||||
@@ -415,6 +423,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
),
|
||||
},
|
||||
@@ -428,6 +437,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
map[uint32]*wasm.FileEntry{ // openedFiles
|
||||
3: {Path: "/", FS: testFS},
|
||||
4: {Path: ".", FS: testFS},
|
||||
@@ -444,6 +454,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
map[uint32]*wasm.FileEntry{ // openedFiles
|
||||
3: {Path: "/", FS: testFS2},
|
||||
4: {Path: ".", FS: testFS2},
|
||||
@@ -460,6 +471,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
map[uint32]*wasm.FileEntry{ // openedFiles
|
||||
3: {Path: ".", FS: testFS},
|
||||
},
|
||||
@@ -475,6 +487,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
map[uint32]*wasm.FileEntry{ // openedFiles
|
||||
3: {Path: "/", FS: testFS},
|
||||
4: {Path: ".", FS: testFS2},
|
||||
@@ -491,6 +504,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
map[uint32]*wasm.FileEntry{ // openedFiles
|
||||
3: {Path: ".", FS: testFS},
|
||||
4: {Path: "/", FS: testFS2},
|
||||
@@ -562,8 +576,8 @@ func TestModuleConfig_toSysContext_Errors(t *testing.T) {
|
||||
}
|
||||
|
||||
// requireSysContext ensures wasm.NewSysContext doesn't return an error, which makes it usable in test matrices.
|
||||
func requireSysContext(t *testing.T, max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, openedFiles map[uint32]*wasm.FileEntry) *wasm.SysContext {
|
||||
sys, err := wasm.NewSysContext(max, args, environ, stdin, stdout, stderr, openedFiles)
|
||||
func requireSysContext(t *testing.T, max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randsource io.Reader, openedFiles map[uint32]*wasm.FileEntry) *wasm.SysContext {
|
||||
sys, err := wasm.NewSysContext(max, args, environ, stdin, stdout, stderr, randsource, openedFiles)
|
||||
require.NoError(t, err)
|
||||
return sys
|
||||
}
|
||||
|
||||
13
examples/assemblyscript/README.md
Normal file
13
examples/assemblyscript/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## AssemblyScript example
|
||||
|
||||
This example runs a WebAssembly program compiled using AssemblyScript, built with `npm install && npm run build`.
|
||||
The program exports two functions, `hello_world` which executes simple integer math, and `goodbye_world`, which
|
||||
throws an error that is logged using the AssemblyScript `abort` built-in function. Wazero is configured to export
|
||||
functions used by WebAssembly for reporting errors and trace messages.
|
||||
|
||||
Ex.
|
||||
```bash
|
||||
$ go run assemblyscript.go 7
|
||||
hello_world returned: 10
|
||||
sad sad world at assemblyscript.ts:7:3
|
||||
```
|
||||
80
examples/assemblyscript/assemblyscript.go
Normal file
80
examples/assemblyscript/assemblyscript.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/assemblyscript"
|
||||
)
|
||||
|
||||
// asWasm compiled using `npm install && npm run build`
|
||||
//go:embed testdata/assemblyscript.wasm
|
||||
var asWasm []byte
|
||||
|
||||
// main shows how to interact with a WebAssembly function that was compiled
|
||||
// from AssemblyScript
|
||||
//
|
||||
// See README.md for a full description.
|
||||
func main() {
|
||||
|
||||
// Choose the context to use for function calls.
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a new WebAssembly Runtime. AssemblyScript enables certain wasm 2.0 features by default, so
|
||||
// we go ahead and configure the runtime for wasm 2.0 compatibility.
|
||||
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig().
|
||||
WithWasmCore2())
|
||||
defer r.Close(ctx) // This closes everything this Runtime created.
|
||||
|
||||
// Instantiate a module implementing functions used by AssemblyScript.
|
||||
// Thrown errors will be logged to os.Stderr
|
||||
_, err := assemblyscript.Instantiate(ctx, r)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
// Compile the WebAssembly module using the default configuration.
|
||||
code, err := r.CompileModule(ctx, asWasm, wazero.NewCompileConfig())
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
// Instantiate a WebAssembly module that imports the "abort" and "trace" functions defined by
|
||||
// assemblyscript.Instantiate and exports functions we'll use in this example. We override the
|
||||
// default module config that discards stdout and stderr.
|
||||
mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout).WithStderr(os.Stderr))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
// Get references to WebAssembly functions we'll use in this example.
|
||||
helloWorld := mod.ExportedFunction("hello_world")
|
||||
goodbyeWorld := mod.ExportedFunction("goodbye_world")
|
||||
|
||||
// Let's use the argument to this main function in Wasm.
|
||||
numStr := os.Args[1]
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
// Call hello_world, which returns the input value incremented by 3. It includes a call to trace()
|
||||
// for detailed logging but the above assemblyscript.Instantiate does not enable it by default.
|
||||
results, err := helloWorld.Call(ctx, uint64(num))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
fmt.Printf("hello_world returned: %v", results[0])
|
||||
|
||||
// Call goodbye_world, which aborts with an error. assemblyscript.Instantiate configures abort
|
||||
// to print to stderr.
|
||||
results, err = goodbyeWorld.Call(ctx)
|
||||
if err == nil {
|
||||
log.Panicln("goodbye_world did not fail")
|
||||
}
|
||||
}
|
||||
17
examples/assemblyscript/assemblyscript_test.go
Normal file
17
examples/assemblyscript/assemblyscript_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/testing/maintester"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
)
|
||||
|
||||
// Test_main ensures the following will work:
|
||||
//
|
||||
// go run assemblyscript.go 7
|
||||
func Test_main(t *testing.T) {
|
||||
stdout, stderr := maintester.TestMain(t, main, "assemblyscript", "7")
|
||||
require.Equal(t, "hello_world returned: 10", stdout)
|
||||
require.Equal(t, "sad sad world at assemblyscript.ts:7:3\n", stderr)
|
||||
}
|
||||
8
examples/assemblyscript/testdata/assemblyscript.ts
vendored
Normal file
8
examples/assemblyscript/testdata/assemblyscript.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
export function hello_world(arg: u32): u32 {
|
||||
trace("hello world", 2, arg + 0.1, arg + 0.2);
|
||||
return arg + 3;
|
||||
}
|
||||
|
||||
export function goodbye_world(): void {
|
||||
throw new Error("sad sad world");
|
||||
}
|
||||
BIN
examples/assemblyscript/testdata/assemblyscript.wasm
vendored
Normal file
BIN
examples/assemblyscript/testdata/assemblyscript.wasm
vendored
Normal file
Binary file not shown.
10
examples/assemblyscript/testdata/package.json
vendored
Normal file
10
examples/assemblyscript/testdata/package.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "hello-assemblyscript",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "asc assemblyscript.ts --debug -b none -o assemblyscript.wasm"
|
||||
},
|
||||
"devDependencies": {
|
||||
"assemblyscript": "^0.20.6"
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,4 @@ type SysKey struct{}
|
||||
type Sys interface {
|
||||
// TimeNowUnixNano allows you to control the value otherwise returned by time.Now().UnixNano()
|
||||
TimeNowUnixNano() uint64
|
||||
|
||||
// RandSource allows you to control the value returned by rand.Read().
|
||||
RandSource([]byte) error
|
||||
}
|
||||
|
||||
@@ -24,13 +24,6 @@ func (d fakeSys) TimeNowUnixNano() uint64 {
|
||||
return epochNanos
|
||||
}
|
||||
|
||||
func (d fakeSys) RandSource(p []byte) error {
|
||||
s := rand.NewSource(seed)
|
||||
rng := rand.New(s)
|
||||
_, err := rng.Read(p)
|
||||
return err
|
||||
}
|
||||
|
||||
// This is a very basic integration of sys config. The main goal is to show how it is configured.
|
||||
func Example_sys() {
|
||||
// Set context to one that has experimental sys config
|
||||
@@ -54,7 +47,9 @@ func Example_sys() {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout))
|
||||
randSource := rand.New(rand.NewSource(seed))
|
||||
|
||||
mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig().WithStdout(os.Stdout).WithRandSource(randSource))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ func TestCallContext_Close(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
map[uint32]*FileEntry{ // openedFiles
|
||||
3: {Path: "."},
|
||||
4: {Path: path.Join(".", pathName), File: file},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -25,6 +26,7 @@ type SysContext struct {
|
||||
argsSize, environSize uint32
|
||||
stdin io.Reader
|
||||
stdout, stderr io.Writer
|
||||
randSource io.Reader
|
||||
|
||||
// openedFiles is a map of file descriptor numbers (>=3) to open files (or directories) and defaults to empty.
|
||||
// TODO: This is unguarded, so not goroutine-safe!
|
||||
@@ -47,7 +49,7 @@ func (c *SysContext) nextFD() uint32 {
|
||||
// Args is like os.Args and defaults to nil.
|
||||
//
|
||||
// Note: The count will never be more than math.MaxUint32.
|
||||
// See wazero.SysConfig WithArgs
|
||||
// See wazero.ModuleConfig WithArgs
|
||||
func (c *SysContext) Args() []string {
|
||||
return c.args
|
||||
}
|
||||
@@ -55,7 +57,7 @@ func (c *SysContext) Args() []string {
|
||||
// ArgsSize is the size to encode Args as Null-terminated strings.
|
||||
//
|
||||
// Note: To get the size without null-terminators, subtract the length of Args from this value.
|
||||
// See wazero.SysConfig WithArgs
|
||||
// See wazero.ModuleConfig WithArgs
|
||||
// See https://en.wikipedia.org/wiki/Null-terminated_string
|
||||
func (c *SysContext) ArgsSize() uint32 {
|
||||
return c.argsSize
|
||||
@@ -64,7 +66,7 @@ func (c *SysContext) ArgsSize() uint32 {
|
||||
// Environ are "key=value" entries like os.Environ and default to nil.
|
||||
//
|
||||
// Note: The count will never be more than math.MaxUint32.
|
||||
// See wazero.SysConfig WithEnviron
|
||||
// See wazero.ModuleConfig WithEnv
|
||||
func (c *SysContext) Environ() []string {
|
||||
return c.environ
|
||||
}
|
||||
@@ -72,30 +74,36 @@ func (c *SysContext) Environ() []string {
|
||||
// EnvironSize is the size to encode Environ as Null-terminated strings.
|
||||
//
|
||||
// Note: To get the size without null-terminators, subtract the length of Environ from this value.
|
||||
// See wazero.SysConfig WithEnviron
|
||||
// See wazero.ModuleConfig WithEnv
|
||||
// See https://en.wikipedia.org/wiki/Null-terminated_string
|
||||
func (c *SysContext) EnvironSize() uint32 {
|
||||
return c.environSize
|
||||
}
|
||||
|
||||
// Stdin is like exec.Cmd Stdin and defaults to a reader of os.DevNull.
|
||||
// See wazero.SysConfig WithStdin
|
||||
// See wazero.ModuleConfig WithStdin
|
||||
func (c *SysContext) Stdin() io.Reader {
|
||||
return c.stdin
|
||||
}
|
||||
|
||||
// Stdout is like exec.Cmd Stdout and defaults to io.Discard.
|
||||
// See wazero.SysConfig WithStdout
|
||||
// See wazero.ModuleConfig WithStdout
|
||||
func (c *SysContext) Stdout() io.Writer {
|
||||
return c.stdout
|
||||
}
|
||||
|
||||
// Stderr is like exec.Cmd Stderr and defaults to io.Discard.
|
||||
// See wazero.SysConfig WithStderr
|
||||
// See wazero.ModuleConfig WithStderr
|
||||
func (c *SysContext) Stderr() io.Writer {
|
||||
return c.stderr
|
||||
}
|
||||
|
||||
// RandSource is a source of random bytes and defaults to crypto/rand.Reader.
|
||||
// see wazero.ModuleConfig WithRandSource
|
||||
func (c *SysContext) RandSource() io.Reader {
|
||||
return c.randSource
|
||||
}
|
||||
|
||||
// eofReader is safer than reading from os.DevNull as it can never overrun operating system file descriptors.
|
||||
type eofReader struct{}
|
||||
|
||||
@@ -110,7 +118,7 @@ func (eofReader) Read([]byte) (int, error) {
|
||||
// Note: This isn't a constant because SysContext.openedFiles is currently mutable even when empty.
|
||||
// TODO: Make it an error to open or close files when no FS was assigned.
|
||||
func DefaultSysContext() *SysContext {
|
||||
if sys, err := NewSysContext(0, nil, nil, nil, nil, nil, nil); err != nil {
|
||||
if sys, err := NewSysContext(0, nil, nil, nil, nil, nil, nil, nil); err != nil {
|
||||
panic(fmt.Errorf("BUG: DefaultSysContext should never error: %w", err))
|
||||
} else {
|
||||
return sys
|
||||
@@ -121,7 +129,7 @@ var _ = DefaultSysContext() // Force panic on bug.
|
||||
|
||||
// NewSysContext is a factory function which helps avoid needing to know defaults or exporting all fields.
|
||||
// Note: max is exposed for testing. max is only used for env/args validation.
|
||||
func NewSysContext(max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, openedFiles map[uint32]*FileEntry) (sys *SysContext, err error) {
|
||||
func NewSysContext(max uint32, args, environ []string, stdin io.Reader, stdout, stderr io.Writer, randSource io.Reader, openedFiles map[uint32]*FileEntry) (sys *SysContext, err error) {
|
||||
sys = &SysContext{args: args, environ: environ}
|
||||
|
||||
if sys.argsSize, err = nullTerminatedByteCount(max, args); err != nil {
|
||||
@@ -150,6 +158,12 @@ func NewSysContext(max uint32, args, environ []string, stdin io.Reader, stdout,
|
||||
sys.stderr = stderr
|
||||
}
|
||||
|
||||
if randSource == nil {
|
||||
sys.randSource = rand.Reader
|
||||
} else {
|
||||
sys.randSource = randSource
|
||||
}
|
||||
|
||||
if openedFiles == nil {
|
||||
sys.openedFiles = map[uint32]*FileEntry{}
|
||||
sys.lastFD = 2 // STDERR
|
||||
|
||||
@@ -20,6 +20,7 @@ func TestDefaultSysContext(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -81,6 +82,7 @@ func TestNewSysContext_Args(t *testing.T) {
|
||||
bytes.NewReader(make([]byte, 0)), // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
)
|
||||
if tc.expectedErr == "" {
|
||||
@@ -139,6 +141,7 @@ func TestNewSysContext_Environ(t *testing.T) {
|
||||
bytes.NewReader(make([]byte, 0)), // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
nil, // openedFiles
|
||||
)
|
||||
if tc.expectedErr == "" {
|
||||
@@ -170,6 +173,7 @@ func TestSysContext_Close(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
map[uint32]*FileEntry{ // openedFiles
|
||||
3: {Path: "/", FS: testFS},
|
||||
4: {Path: ".", FS: testFS},
|
||||
@@ -199,6 +203,7 @@ func TestSysContext_Close(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
map[uint32]*FileEntry{ // no openedFiles
|
||||
3: {Path: "/", FS: testFS},
|
||||
4: {Path: ".", FS: testFS},
|
||||
@@ -223,6 +228,7 @@ func TestSysContext_Close(t *testing.T) {
|
||||
nil, // stdin
|
||||
nil, // stdout
|
||||
nil, // stderr
|
||||
nil, // randSource
|
||||
map[uint32]*FileEntry{ // openedFiles
|
||||
3: {Path: "/", FS: testFS},
|
||||
4: {Path: ".", FS: testFS},
|
||||
|
||||
11
wasi/wasi.go
11
wasi/wasi.go
@@ -6,7 +6,6 @@ package wasi
|
||||
|
||||
import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -1302,8 +1301,9 @@ func (a *snapshotPreview1) SchedYield(m api.Module) Errno {
|
||||
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-bufLen-size---errno
|
||||
func (a *snapshotPreview1) RandomGet(ctx context.Context, m api.Module, buf uint32, bufLen uint32) (errno Errno) {
|
||||
randomBytes := make([]byte, bufLen)
|
||||
err := a.sys.RandSource(randomBytes)
|
||||
if err != nil {
|
||||
sys := sysCtx(m)
|
||||
n, err := sys.RandSource().Read(randomBytes)
|
||||
if n != int(bufLen) || err != nil {
|
||||
// TODO: handle different errors that syscal to entropy source can return
|
||||
return ErrnoIo
|
||||
}
|
||||
@@ -1345,11 +1345,6 @@ func (d *defaultSys) TimeNowUnixNano() uint64 {
|
||||
return uint64(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (d *defaultSys) RandSource(bytes []byte) error {
|
||||
_, err := crand.Read(bytes)
|
||||
return err
|
||||
}
|
||||
|
||||
func newSnapshotPreview1(ctx context.Context) *snapshotPreview1 {
|
||||
if ctx != nil { // Test to see if internal code are using an experimental feature.
|
||||
if sys := ctx.Value(experimental.SysKey{}); sys != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"path"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"testing/iotest"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
@@ -37,13 +38,6 @@ func (d fakeSys) TimeNowUnixNano() uint64 {
|
||||
return epochNanos
|
||||
}
|
||||
|
||||
func (d fakeSys) RandSource(p []byte) error {
|
||||
s := rand.NewSource(seed)
|
||||
rng := rand.New(s)
|
||||
_, err := rng.Read(p)
|
||||
return err
|
||||
}
|
||||
|
||||
// testCtx ensures fakeSys is used for WASI functions.
|
||||
var testCtx = context.WithValue(context.Background(), experimental.SysKey{}, fakeSys{})
|
||||
|
||||
@@ -1875,10 +1869,14 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) {
|
||||
length := uint32(5) // arbitrary length,
|
||||
offset := uint32(1) // offset,
|
||||
|
||||
a, mod, fn := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, nil)
|
||||
defer mod.Close(testCtx)
|
||||
|
||||
t.Run("snapshotPreview1.RandomGet", func(t *testing.T) {
|
||||
source := rand.New(rand.NewSource(seed))
|
||||
sysCtx, err := wasm.NewSysContext(math.MaxUint32, nil, nil, new(bytes.Buffer), nil, nil, source, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
a, mod, _ := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, sysCtx)
|
||||
defer mod.Close(testCtx)
|
||||
|
||||
maskMemory(t, testCtx, mod, len(expectedMemory))
|
||||
|
||||
// Invoke RandomGet directly and check the memory side effects!
|
||||
@@ -1891,6 +1889,13 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run(functionRandomGet, func(t *testing.T) {
|
||||
source := rand.New(rand.NewSource(seed))
|
||||
sysCtx, err := wasm.NewSysContext(math.MaxUint32, nil, nil, new(bytes.Buffer), nil, nil, source, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, mod, fn := instantiateModule(testCtx, t, functionRandomGet, importRandomGet, sysCtx)
|
||||
defer mod.Close(testCtx)
|
||||
|
||||
maskMemory(t, testCtx, mod, len(expectedMemory))
|
||||
|
||||
results, err := fn.Call(testCtx, uint64(offset), uint64(length))
|
||||
@@ -1949,18 +1954,33 @@ func (d *fakeSysErr) TimeNowUnixNano() uint64 {
|
||||
panic(errors.New("TimeNowUnixNano error"))
|
||||
}
|
||||
|
||||
func (d *fakeSysErr) RandSource([]byte) error {
|
||||
return errors.New("RandSource error")
|
||||
}
|
||||
|
||||
func TestSnapshotPreview1_RandomGet_SourceError(t *testing.T) {
|
||||
var errCtx = context.WithValue(context.Background(), experimental.SysKey{}, &fakeSysErr{})
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
randSource io.Reader
|
||||
}{
|
||||
{
|
||||
name: "error",
|
||||
randSource: iotest.ErrReader(errors.New("RandSource error")),
|
||||
},
|
||||
{
|
||||
name: "incomplete",
|
||||
randSource: bytes.NewReader([]byte{1, 2}),
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var errCtx = context.WithValue(context.Background(), experimental.SysKey{}, &fakeSysErr{})
|
||||
|
||||
a, mod, _ := instantiateModule(errCtx, t, functionRandomGet, importRandomGet, nil)
|
||||
defer mod.Close(errCtx)
|
||||
sysCtx, err := wasm.NewSysContext(math.MaxUint32, nil, nil, new(bytes.Buffer), nil, nil, tc.randSource, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
errno := a.RandomGet(errCtx, mod, uint32(1), uint32(5)) // arbitrary offset and length
|
||||
require.Equal(t, ErrnoIo, errno, ErrnoName(errno))
|
||||
a, mod, _ := instantiateModule(errCtx, t, functionRandomGet, importRandomGet, sysCtx)
|
||||
defer mod.Close(errCtx)
|
||||
|
||||
errno := a.RandomGet(errCtx, mod, uint32(1), uint32(5)) // arbitrary offset and length
|
||||
require.Equal(t, ErrnoIo, errno, ErrnoName(errno))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotPreview1_SockRecv only tests it is stubbed for GrainLang per #271
|
||||
@@ -2057,7 +2077,7 @@ func instantiateModule(ctx context.Context, t *testing.T, wasifunction, wasiimpo
|
||||
}
|
||||
|
||||
func newSysContext(args, environ []string, openedFiles map[uint32]*wasm.FileEntry) (sysCtx *wasm.SysContext, err error) {
|
||||
return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, openedFiles)
|
||||
return wasm.NewSysContext(math.MaxUint32, args, environ, new(bytes.Buffer), nil, nil, nil, openedFiles)
|
||||
}
|
||||
|
||||
func createFile(t *testing.T, pathName string, data []byte) (fs.File, fs.FS) {
|
||||
|
||||
Reference in New Issue
Block a user