wa-lang.org/wazero@v1.0.2/imports/assemblyscript/assemblyscript.go (about)

     1  // Package assemblyscript contains Go-defined special functions imported by
     2  // AssemblyScript under the module name "env".
     3  //
     4  // # Special Functions
     5  //
     6  // AssemblyScript code import the below special functions when not using WASI.
     7  // Note: Sometimes only "abort" is imported.
     8  //
     9  //   - "abort" - exits with 255 with an abort message written to
    10  //     wazero.ModuleConfig WithStderr.
    11  //   - "trace" - no output unless.
    12  //   - "seed" - uses wazero.ModuleConfig WithRandSource as the source of seed
    13  //     values.
    14  //
    15  // See https://www.assemblyscript.org/concepts.html#special-imports
    16  //
    17  // # Relationship to WASI
    18  //
    19  // AssemblyScript supports compiling JavaScript functions that use I/O, such
    20  // as `console.log("hello")`. However, WASI is not built-in to AssemblyScript.
    21  // Use the `wasi-shim` to compile if you get import errors.
    22  //
    23  // See https://github.com/AssemblyScript/wasi-shim#usage and
    24  // wasi_snapshot_preview1.Instantiate for more.
    25  package assemblyscript
    26  
    27  import (
    28  	"context"
    29  	"encoding/binary"
    30  	"fmt"
    31  	"io"
    32  	"strconv"
    33  	"strings"
    34  	"unicode/utf16"
    35  
    36  	"wa-lang.org/wazero"
    37  	"wa-lang.org/wazero/api"
    38  	"wa-lang.org/wazero/internal/wasm"
    39  	"wa-lang.org/wazero/sys"
    40  )
    41  
    42  const (
    43  	i32, f64 = wasm.ValueTypeI32, wasm.ValueTypeF64
    44  
    45  	functionAbort = "abort"
    46  	functionTrace = "trace"
    47  	functionSeed  = "seed"
    48  )
    49  
    50  // MustInstantiate calls Instantiate or panics on error.
    51  //
    52  // This is a simpler function for those who know the module "env" is not
    53  // already instantiated, and don't need to unload it.
    54  func MustInstantiate(ctx context.Context, r wazero.Runtime) {
    55  	if _, err := Instantiate(ctx, r); err != nil {
    56  		panic(err)
    57  	}
    58  }
    59  
    60  // Instantiate instantiates the "env" module used by AssemblyScript into the
    61  // runtime default namespace.
    62  //
    63  // # Notes
    64  //
    65  //   - Failure cases are documented on wazero.Namespace InstantiateModule.
    66  //   - Closing the wazero.Runtime has the same effect as closing the result.
    67  //   - To add more functions to the "env" module, use FunctionExporter.
    68  //   - To instantiate into another wazero.Namespace, use FunctionExporter.
    69  func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) {
    70  	builder := r.NewHostModuleBuilder("env")
    71  	NewFunctionExporter().ExportFunctions(builder)
    72  	return builder.Instantiate(ctx, r)
    73  }
    74  
    75  // FunctionExporter configures the functions in the "env" module used by
    76  // AssemblyScript.
    77  type FunctionExporter interface {
    78  	// WithAbortMessageDisabled configures the AssemblyScript abort function to
    79  	// discard any message.
    80  	WithAbortMessageDisabled() FunctionExporter
    81  
    82  	// WithTraceToStdout configures the AssemblyScript trace function to output
    83  	// messages to Stdout, as configured by wazero.ModuleConfig WithStdout.
    84  	WithTraceToStdout() FunctionExporter
    85  
    86  	// WithTraceToStderr configures the AssemblyScript trace function to output
    87  	// messages to Stderr, as configured by wazero.ModuleConfig WithStderr.
    88  	//
    89  	// Because of the potential volume of trace messages, it is often more
    90  	// appropriate to use WithTraceToStdout instead.
    91  	WithTraceToStderr() FunctionExporter
    92  
    93  	// ExportFunctions builds functions to export with a wazero.HostModuleBuilder
    94  	// named "env".
    95  	ExportFunctions(wazero.HostModuleBuilder)
    96  }
    97  
    98  // NewFunctionExporter returns a FunctionExporter object with trace disabled.
    99  func NewFunctionExporter() FunctionExporter {
   100  	return &functionExporter{abortFn: abortMessageEnabled, traceFn: traceDisabled}
   101  }
   102  
   103  type functionExporter struct {
   104  	abortFn, traceFn *wasm.HostFunc
   105  }
   106  
   107  // WithAbortMessageDisabled implements FunctionExporter.WithAbortMessageDisabled
   108  func (e *functionExporter) WithAbortMessageDisabled() FunctionExporter {
   109  	return &functionExporter{abortFn: abortMessageDisabled, traceFn: e.traceFn}
   110  }
   111  
   112  // WithTraceToStdout implements FunctionExporter.WithTraceToStdout
   113  func (e *functionExporter) WithTraceToStdout() FunctionExporter {
   114  	return &functionExporter{abortFn: e.abortFn, traceFn: traceStdout}
   115  }
   116  
   117  // WithTraceToStderr implements FunctionExporter.WithTraceToStderr
   118  func (e *functionExporter) WithTraceToStderr() FunctionExporter {
   119  	return &functionExporter{abortFn: e.abortFn, traceFn: traceStderr}
   120  }
   121  
   122  // ExportFunctions implements FunctionExporter.ExportFunctions
   123  func (e *functionExporter) ExportFunctions(builder wazero.HostModuleBuilder) {
   124  	exporter := builder.(wasm.HostFuncExporter)
   125  	exporter.ExportHostFunc(e.abortFn)
   126  	exporter.ExportHostFunc(e.traceFn)
   127  	exporter.ExportHostFunc(seed)
   128  }
   129  
   130  // abort is called on unrecoverable errors. This is typically present in Wasm
   131  // compiled from AssemblyScript, if assertions are enabled or errors are
   132  // thrown.
   133  //
   134  // The implementation writes the message to stderr, unless
   135  // abortMessageDisabled, and closes the module with exit code 255.
   136  //
   137  // Here's the import in a user's module that ends up using this, in WebAssembly
   138  // 1.0 (MVP) Text Format:
   139  //
   140  //	(import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
   141  //
   142  // See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L18
   143  var abortMessageEnabled = &wasm.HostFunc{
   144  	ExportNames: []string{functionAbort},
   145  	Name:        "~lib/builtins/abort",
   146  	ParamTypes:  []api.ValueType{i32, i32, i32, i32},
   147  	ParamNames:  []string{"message", "fileName", "lineNumber", "columnNumber"},
   148  	Code: &wasm.Code{
   149  		IsHostFunction: true,
   150  		GoFunc:         api.GoModuleFunc(abortWithMessage),
   151  	},
   152  }
   153  
   154  var abortMessageDisabled = abortMessageEnabled.WithGoModuleFunc(abort)
   155  
   156  // abortWithMessage implements functionAbort
   157  func abortWithMessage(ctx context.Context, mod api.Module, stack []uint64) {
   158  	message := uint32(stack[0])
   159  	fileName := uint32(stack[1])
   160  	lineNumber := uint32(stack[2])
   161  	columnNumber := uint32(stack[3])
   162  	sysCtx := mod.(*wasm.CallContext).Sys
   163  	mem := mod.Memory()
   164  	// Don't panic if there was a problem reading the message
   165  	if msg, msgOk := readAssemblyScriptString(ctx, mem, message); msgOk {
   166  		if fn, fnOk := readAssemblyScriptString(ctx, mem, fileName); fnOk {
   167  			_, _ = fmt.Fprintf(sysCtx.Stderr(), "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber)
   168  		}
   169  	}
   170  	abort(ctx, mod, stack)
   171  }
   172  
   173  // abortWithMessage implements functionAbort ignoring the message.
   174  func abort(ctx context.Context, mod api.Module, _ []uint64) {
   175  	// AssemblyScript expects the exit code to be 255
   176  	// See https://github.com/AssemblyScript/assemblyscript/blob/v0.20.13/tests/compiler/wasi/abort.js#L14
   177  	exitCode := uint32(255)
   178  
   179  	// Ensure other callers see the exit code.
   180  	_ = mod.CloseWithExitCode(ctx, exitCode)
   181  
   182  	// Prevent any code from executing after this function.
   183  	panic(sys.NewExitError(mod.Name(), exitCode))
   184  }
   185  
   186  // traceDisabled ignores the input.
   187  var traceDisabled = traceStdout.WithWasm([]byte{wasm.OpcodeEnd})
   188  
   189  // traceStdout implements trace to the configured Stdout.
   190  var traceStdout = &wasm.HostFunc{
   191  	ExportNames: []string{functionTrace},
   192  	Name:        "~lib/builtins/trace",
   193  	ParamTypes:  []api.ValueType{i32, i32, f64, f64, f64, f64, f64},
   194  	ParamNames:  []string{"message", "nArgs", "arg0", "arg1", "arg2", "arg3", "arg4"},
   195  	Code: &wasm.Code{
   196  		IsHostFunction: true,
   197  		GoFunc: api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) {
   198  			traceTo(ctx, mod, stack, mod.(*wasm.CallContext).Sys.Stdout())
   199  		}),
   200  	},
   201  }
   202  
   203  // traceStderr implements trace to the configured Stderr.
   204  var traceStderr = traceStdout.WithGoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) {
   205  	traceTo(ctx, mod, stack, mod.(*wasm.CallContext).Sys.Stderr())
   206  })
   207  
   208  // traceTo implements the function "trace" in AssemblyScript. e.g.
   209  //
   210  //	trace('Hello World!')
   211  //
   212  // Here's the import in a user's module that ends up using this, in WebAssembly
   213  // 1.0 (MVP) Text Format:
   214  //
   215  //	(import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64)))
   216  //
   217  // See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L61
   218  func traceTo(ctx context.Context, mod api.Module, params []uint64, writer io.Writer) {
   219  	message := uint32(params[0])
   220  	nArgs := uint32(params[1])
   221  	arg0 := api.DecodeF64(params[2])
   222  	arg1 := api.DecodeF64(params[3])
   223  	arg2 := api.DecodeF64(params[4])
   224  	arg3 := api.DecodeF64(params[5])
   225  	arg4 := api.DecodeF64(params[6])
   226  
   227  	msg, ok := readAssemblyScriptString(ctx, mod.Memory(), message)
   228  	if !ok {
   229  		return // don't panic if unable to trace
   230  	}
   231  	var ret strings.Builder
   232  	ret.WriteString("trace: ")
   233  	ret.WriteString(msg)
   234  	if nArgs >= 1 {
   235  		ret.WriteString(" ")
   236  		ret.WriteString(formatFloat(arg0))
   237  	}
   238  	if nArgs >= 2 {
   239  		ret.WriteString(",")
   240  		ret.WriteString(formatFloat(arg1))
   241  	}
   242  	if nArgs >= 3 {
   243  		ret.WriteString(",")
   244  		ret.WriteString(formatFloat(arg2))
   245  	}
   246  	if nArgs >= 4 {
   247  		ret.WriteString(",")
   248  		ret.WriteString(formatFloat(arg3))
   249  	}
   250  	if nArgs >= 5 {
   251  		ret.WriteString(",")
   252  		ret.WriteString(formatFloat(arg4))
   253  	}
   254  	ret.WriteByte('\n')
   255  	_, _ = writer.Write([]byte(ret.String())) // don't crash if trace logging fails
   256  }
   257  
   258  func formatFloat(f float64) string {
   259  	return strconv.FormatFloat(f, 'g', -1, 64)
   260  }
   261  
   262  // seed is called when the AssemblyScript's random number generator needs to be
   263  // seeded.
   264  //
   265  // Here's the import in a user's module that ends up using this, in WebAssembly
   266  // 1.0 (MVP) Text Format:
   267  //
   268  //	(import "env" "seed" (func $~lib/builtins/seed (result f64)))
   269  //
   270  // See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L111
   271  var seed = &wasm.HostFunc{
   272  	ExportNames: []string{functionSeed},
   273  	Name:        "~lib/builtins/seed",
   274  	ResultTypes: []api.ValueType{f64},
   275  	Code: &wasm.Code{
   276  		IsHostFunction: true,
   277  		GoFunc: api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) {
   278  			r := mod.(*wasm.CallContext).Sys.RandSource()
   279  			buf := make([]byte, 8)
   280  			_, err := io.ReadFull(r, buf)
   281  			if err != nil {
   282  				panic(fmt.Errorf("error reading random seed: %w", err))
   283  			}
   284  
   285  			// the caller interprets the result as a float64
   286  			stack[0] = binary.LittleEndian.Uint64(buf)
   287  		}),
   288  	},
   289  }
   290  
   291  // readAssemblyScriptString reads a UTF-16 string created by AssemblyScript.
   292  func readAssemblyScriptString(ctx context.Context, mem api.Memory, offset uint32) (string, bool) {
   293  	// Length is four bytes before pointer.
   294  	byteCount, ok := mem.ReadUint32Le(ctx, offset-4)
   295  	if !ok || byteCount%2 != 0 {
   296  		return "", false
   297  	}
   298  	buf, ok := mem.Read(ctx, offset, byteCount)
   299  	if !ok {
   300  		return "", false
   301  	}
   302  	return decodeUTF16(buf), true
   303  }
   304  
   305  func decodeUTF16(b []byte) string {
   306  	u16s := make([]uint16, len(b)/2)
   307  
   308  	lb := len(b)
   309  	for i := 0; i < lb; i += 2 {
   310  		u16s[i/2] = uint16(b[i]) + (uint16(b[i+1]) << 8)
   311  	}
   312  
   313  	return string(utf16.Decode(u16s))
   314  }