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 }