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:
Anuraag Agrawal
2022-05-20 04:24:23 +09:00
committed by GitHub
parent 9a9b361ac8
commit b3fc76ed6e
20 changed files with 963 additions and 52 deletions

View File

@@ -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
View File

@@ -22,3 +22,7 @@ go.work
# Goland
.idea
# AssemblyScript
node_modules
package-lock.json

View File

@@ -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)

View 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
}
}

View 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
}

View File

@@ -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)
}

View File

@@ -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
}

View 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
```

View 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")
}
}

View 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)
}

View 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");
}

Binary file not shown.

View 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"
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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},

View 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

View File

@@ -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},

View File

@@ -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 {

View File

@@ -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) {