Files
wazero/assemblyscript/assemblyscript.go
Crypt Keeper b98a11e9c3 Refactors host function tests to stub with wasm (#710)
This refactors host functions with no-op or constant returns to be
implemented with wasm instead of the host function bridge. This allows
better performance.

This also breaks up and makes WASI tests consistent, in a way that shows
parameter name drifts easier.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2022-07-21 15:49:55 +08:00

292 lines
9.4 KiB
Go

// Package assemblyscript contains Go-defined special functions imported by
// AssemblyScript under the module name "env".
//
// Special Functions
//
// AssemblyScript code import the below special functions when not using WASI.
// Note: Sometimes only "abort" is imported.
//
// * "abort" - exits with 255 with an abort message written to
// wazero.ModuleConfig WithStderr.
// * "trace" - no output unless.
// * "seed" - uses wazero.ModuleConfig WithRandSource as the source of seed
// values.
//
// Relationship to WASI
//
// A program compiled to use WASI, via "import wasi" in any file, won't import
// these functions.
//
// See wasi_snapshot_preview1.Instantiate and
// * https://www.assemblyscript.org/concepts.html#special-imports
// * https://www.assemblyscript.org/concepts.html#targeting-wasi
// * https://www.assemblyscript.org/compiler.html#compiler-options
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"
"github.com/tetratelabs/wazero/sys"
)
// Instantiate instantiates the "env" module used by AssemblyScript into the
// runtime default namespace.
//
// Notes
//
// * Closing the wazero.Runtime has the same effect as closing the result.
// * To add more functions to the "env" module, use FunctionExporter.
// * To instantiate into another wazero.Namespace, use FunctionExporter.
func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) {
builder := r.NewModuleBuilder("env")
NewFunctionExporter().ExportFunctions(builder)
return builder.Instantiate(ctx, r)
}
// FunctionExporter configures the functions in the "env" module used by
// AssemblyScript.
type FunctionExporter interface {
// WithAbortMessageDisabled configures the AssemblyScript abort function to
// discard any message.
WithAbortMessageDisabled() FunctionExporter
// WithTraceToStdout configures the AssemblyScript trace function to output
// messages to Stdout, as configured by wazero.ModuleConfig WithStdout.
WithTraceToStdout() FunctionExporter
// 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() FunctionExporter
// ExportFunctions builds functions to export with a wazero.ModuleBuilder
// named "env".
ExportFunctions(builder wazero.ModuleBuilder)
}
// NewFunctionExporter returns a FunctionExporter object with trace disabled.
func NewFunctionExporter() FunctionExporter {
return &functionExporter{traceMode: traceDisabled}
}
type traceMode int
const (
traceDisabled traceMode = 0
traceStdout traceMode = 1
traceStderr traceMode = 2
)
type functionExporter struct {
abortMessageDisabled bool
traceMode traceMode
}
// WithAbortMessageDisabled implements FunctionExporter.WithAbortMessageDisabled
func (e *functionExporter) WithAbortMessageDisabled() FunctionExporter {
ret := *e // copy
ret.abortMessageDisabled = true
return &ret
}
// WithTraceToStdout implements FunctionExporter.WithTraceToStdout
func (e *functionExporter) WithTraceToStdout() FunctionExporter {
ret := *e // copy
ret.traceMode = traceStdout
return &ret
}
// WithTraceToStderr implements FunctionExporter.WithTraceToStderr
func (e *functionExporter) WithTraceToStderr() FunctionExporter {
ret := *e // copy
ret.traceMode = traceStderr
return &ret
}
// ExportFunctions implements FunctionExporter.ExportFunctions
func (e *functionExporter) ExportFunctions(builder wazero.ModuleBuilder) {
var abortFn fnAbort
if e.abortMessageDisabled {
abortFn = abort
} else {
abortFn = abortWithMessage
}
var traceFn interface{}
switch e.traceMode {
case traceDisabled:
traceFn = traceNoop
case traceStdout:
traceFn = traceToStdout
case traceStderr:
traceFn = traceToStderr
}
builder.ExportFunction("abort", abortFn, "~lib/builtins/abort",
"message", "fileName", "lineNumber", "columnNumber")
builder.ExportFunction("trace", traceFn, "~lib/builtins/trace",
"message", "nArgs", "arg0", "arg1", "arg2", "arg3", "arg4")
builder.ExportFunction("seed", seed, "~lib/builtins/seed")
}
// 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
type fnAbort func(
ctx context.Context, mod api.Module, message, fileName, lineNumber, columnNumber uint32,
)
// abortWithMessage implements fnAbort
func abortWithMessage(
ctx context.Context, mod api.Module, message, fileName, lineNumber, columnNumber uint32,
) {
sysCtx := mod.(*wasm.CallContext).Sys
msg := requireAssemblyScriptString(ctx, mod, "message", message)
fn := requireAssemblyScriptString(ctx, mod, "fileName", fileName)
_, _ = fmt.Fprintf(sysCtx.Stderr(), "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber)
abort(ctx, mod, message, fileName, lineNumber, columnNumber)
}
// abortWithMessage implements fnAbort ignoring the message.
func abort(
ctx context.Context, mod api.Module, message, fileName, lineNumber, columnNumber uint32,
) {
// AssemblyScript expects the exit code to be 255
// See https://github.com/AssemblyScript/assemblyscript/blob/v0.20.13/tests/compiler/wasi/abort.js#L14
exitCode := uint32(255)
// Ensure other callers see the exit code.
_ = mod.CloseWithExitCode(ctx, exitCode)
// Prevent any code from executing after this function.
panic(sys.NewExitError(mod.Name(), exitCode))
}
// traceNoop implements trace in wasm to avoid host call overhead on no-op.
var traceNoop = &wasm.Func{
Type: wasm.MustFunctionType(traceToStdout),
Code: &wasm.Code{Body: []byte{wasm.OpcodeEnd}},
}
// traceToStdout implements trace to the configured Stdout.
func traceToStdout(
ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64,
) {
traceTo(ctx, mod, message, nArgs, arg0, arg1, arg2, arg3, arg4, mod.(*wasm.CallContext).Sys.Stdout())
}
// traceToStdout implements trace to the configured Stderr.
func traceToStderr(
ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64,
) {
traceTo(ctx, mod, message, nArgs, arg0, arg1, arg2, arg3, arg4, mod.(*wasm.CallContext).Sys.Stderr())
}
// traceTo implements the function "trace" 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 traceTo(
ctx context.Context, mod api.Module, message uint32, nArgs uint32, arg0, arg1, arg2, arg3, arg4 float64,
writer io.Writer,
) {
var ret strings.Builder
ret.WriteString("trace: ")
ret.WriteString(requireAssemblyScriptString(ctx, mod, "message", message))
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')
if _, err := writer.Write([]byte(ret.String())); 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 seed(mod api.Module) float64 {
randSource := mod.(*wasm.CallContext).Sys.RandSource()
v, err := ieee754.DecodeFloat64(randSource)
if err != nil {
panic(fmt.Errorf("error reading random seed: %w", err))
}
return v
}
// requireAssemblyScriptString reads a UTF-16 string created by AssemblyScript.
func requireAssemblyScriptString(ctx context.Context, mod api.Module, fieldName string, offset uint32) string {
// Length is four bytes before pointer.
byteCount, ok := mod.Memory().ReadUint32Le(ctx, offset-4)
if !ok {
panic(fmt.Errorf("out of memory reading %s", fieldName))
}
if byteCount%2 != 0 {
panic(fmt.Errorf("invalid UTF-16 reading %s", fieldName))
}
buf, ok := mod.Memory().Read(ctx, offset, byteCount)
if !ok {
panic(fmt.Errorf("out of memory reading %s", fieldName))
}
return decodeUTF16(buf)
}
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))
}