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