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 }