From af59cffa7f5621b7f540010644692d17910ac9ea Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 31 Aug 2024 17:27:03 +0200 Subject: [PATCH] wasip1: add `//go:wasmexport` support This adds support for the `//go:wasmexport` pragma as proposed here: https://github.com/golang/go/issues/65199 It is currently implemented only for wasip1, but it is certainly possible to extend it to other targets like GOOS=js, wasm-unknown, or wasip2. It is also currently limited to -buildmode=c-shared, this is a limitation that could easily be lifted in the future. --- builder/build.go | 8 + compileopts/config.go | 14 ++ compileopts/options.go | 10 ++ compiler/compiler.go | 6 + compiler/goroutine.go | 268 +++++++++++++++++++++++++--- compiler/symbol.go | 62 +++++-- main.go | 2 + src/runtime/runtime_wasip1.go | 11 +- src/runtime/runtime_wasm_command.go | 23 +++ src/runtime/runtime_wasm_reactor.go | 79 ++++++++ src/runtime/runtime_wasm_unknown.go | 3 + src/runtime/scheduler.go | 11 +- src/runtime/scheduler_any.go | 2 +- 13 files changed, 449 insertions(+), 50 deletions(-) create mode 100644 src/runtime/runtime_wasm_command.go create mode 100644 src/runtime/runtime_wasm_reactor.go diff --git a/builder/build.go b/builder/build.go index 780dc8df49..fc180a7e91 100644 --- a/builder/build.go +++ b/builder/build.go @@ -197,6 +197,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe ABI: config.ABI(), GOOS: config.GOOS(), GOARCH: config.GOARCH(), + BuildMode: config.BuildMode(), CodeModel: config.CodeModel(), RelocationModel: config.RelocationModel(), SizeLevel: sizeLevel, @@ -649,6 +650,13 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe result.Binary = result.Executable // final file ldflags := append(config.LDFlags(), "-o", result.Executable) + if config.Options.BuildMode == "c-shared" { + if config.GOOS() != "wasip1" { + return result, fmt.Errorf("buildmode c-shared is only supported on wasip1 at the moment") + } + ldflags = append(ldflags, "--no-entry") + } + // Add compiler-rt dependency if needed. Usually this is a simple load from // a cache. if config.Target.RTLib == "compiler-rt" { diff --git a/compileopts/config.go b/compileopts/config.go index 18d3c9e4d8..763abe2cdc 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -33,6 +33,14 @@ func (c *Config) CPU() string { return c.Target.CPU } +// The current build mode (like the `-buildmode` command line flag). +func (c *Config) BuildMode() string { + if c.Options.BuildMode != "" { + return c.Options.BuildMode + } + return "default" +} + // Features returns a list of features this CPU supports. For example, for a // RISC-V processor, that could be "+a,+c,+m". For many targets, an empty list // will be returned. @@ -91,6 +99,12 @@ func (c *Config) BuildTags() []string { for i := 1; i <= c.GoMinorVersion; i++ { tags = append(tags, fmt.Sprintf("go1.%d", i)) } + if c.BuildMode() == "c-shared" && strings.HasPrefix(c.Triple(), "wasm") { + // Use reactor mode, which means the application is initialized in + // _initialize and main.main is not called by default. See for example: + // https://github.com/WebAssembly/WASI/blob/main/legacy/application-abi.md + tags = append(tags, "tinygo_wasm_reactor") + } tags = append(tags, c.Options.Tags...) return tags } diff --git a/compileopts/options.go b/compileopts/options.go index 9601ae3221..990fdd7694 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -8,6 +8,7 @@ import ( ) var ( + validBuildModeOptions = []string{"default", "c-shared"} validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise"} validSchedulerOptions = []string{"none", "tasks", "asyncify"} validSerialOptions = []string{"none", "uart", "usb", "rtt"} @@ -26,6 +27,7 @@ type Options struct { GOMIPS string // environment variable (only used with GOARCH=mips and GOARCH=mipsle) Directory string // working dir, leave it unset to use the current working dir Target string + BuildMode string // -buildmode flag Opt string GC string PanicStrategy string @@ -60,6 +62,14 @@ type Options struct { // Verify performs a validation on the given options, raising an error if options are not valid. func (o *Options) Verify() error { + if o.BuildMode != "" { + valid := isInArray(validBuildModeOptions, o.BuildMode) + if !valid { + return fmt.Errorf(`invalid buildmode option '%s': valid values are %s`, + o.BuildMode, + strings.Join(validBuildModeOptions, ", ")) + } + } if o.GC != "" { valid := isInArray(validGCOptions, o.GC) if !valid { diff --git a/compiler/compiler.go b/compiler/compiler.go index 201605d78c..04086756d4 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -44,6 +44,7 @@ type Config struct { ABI string GOOS string GOARCH string + BuildMode string CodeModel string RelocationModel string SizeLevel int @@ -1384,6 +1385,11 @@ func (b *builder) createFunction() { b.llvmFn.SetLinkage(llvm.InternalLinkage) b.createFunction() } + + // Create wrapper function that can be called externally. + if b.info.wasmExport != "" { + b.createWasmExport() + } } // posser is an interface that's implemented by both ssa.Value and diff --git a/compiler/goroutine.go b/compiler/goroutine.go index 95abc77ff9..10305daa55 100644 --- a/compiler/goroutine.go +++ b/compiler/goroutine.go @@ -7,6 +7,7 @@ import ( "go/token" "go/types" + "github.com/tinygo-org/tinygo/compiler/llvmutil" "golang.org/x/tools/go/ssa" "tinygo.org/x/go-llvm" ) @@ -99,7 +100,7 @@ func (b *builder) createGo(instr *ssa.Go) { paramBundle := b.emitPointerPack(params) var stackSize llvm.Value - callee := b.createGoroutineStartWrapper(funcType, funcPtr, prefix, hasContext, instr.Pos()) + callee := b.createGoroutineStartWrapper(funcType, funcPtr, prefix, hasContext, false, instr.Pos()) if b.AutomaticStackSize { // The stack size is not known until after linking. Call a dummy // function that will be replaced with a load from a special ELF @@ -119,6 +120,142 @@ func (b *builder) createGo(instr *ssa.Go) { b.createCall(fnType, start, []llvm.Value{callee, paramBundle, stackSize, llvm.Undef(b.dataPtrType)}, "") } +// Create an exported wrapper function for functions with the //go:wasmexport +// pragma. This wrapper function is quite complex when the scheduler is enabled: +// it needs to start a new goroutine each time the exported function is called. +func (b *builder) createWasmExport() { + pos := b.info.wasmExportPos + if b.info.exported { + // //export really shouldn't be used anymore when //go:wasmexport is + // available, because //go:wasmexport is much better defined. + b.addError(pos, "cannot use //export and //go:wasmexport at the same time") + return + } + + const suffix = "#wasmexport" + + // Declare the exported function. + paramTypes := b.llvmFnType.ParamTypes() + exportedFnType := llvm.FunctionType(b.llvmFnType.ReturnType(), paramTypes[:len(paramTypes)-1], false) + exportedFn := llvm.AddFunction(b.mod, b.fn.RelString(nil)+suffix, exportedFnType) + b.addStandardAttributes(exportedFn) + llvmutil.AppendToGlobal(b.mod, "llvm.used", exportedFn) + exportedFn.AddFunctionAttr(b.ctx.CreateStringAttribute("wasm-export-name", b.info.wasmExport)) + + // Create a builder for this wrapper function. + builder := newBuilder(b.compilerContext, b.ctx.NewBuilder(), b.fn) + defer builder.Dispose() + + // Define this function as a separate function in DWARF + if b.Debug { + if b.fn.Syntax() != nil { + // Create debug info file if needed. + pos := b.program.Fset.Position(pos) + builder.difunc = builder.attachDebugInfoRaw(b.fn, exportedFn, suffix, pos.Filename, pos.Line) + } + builder.setDebugLocation(pos) + } + + // Create a single basic block inside of it. + bb := llvm.AddBasicBlock(exportedFn, "entry") + builder.SetInsertPointAtEnd(bb) + + if b.Scheduler == "none" { + // When the scheduler has been disabled, this is really trivial: just + // call the function. + params := exportedFn.Params() + params = append(params, llvm.ConstNull(b.dataPtrType)) // context parameter + retval := builder.CreateCall(b.llvmFnType, b.llvmFn, params, "") + if b.fn.Signature.Results() == nil { + builder.CreateRetVoid() + } else { + builder.CreateRet(retval) + } + + } else { + // The scheduler is enabled, so we need to start a new goroutine, wait + // for it to complete, and read the result value. + + // Build a function that looks like this: + // + // func foo#wasmexport(param0, param1, ..., paramN) { + // var state *stateStruct + // + // // 'done' must be explicitly initialized ('state' is not zeroed) + // state.done = false + // + // // store the parameters in the state object + // state.param0 = param0 + // state.param1 = param1 + // ... + // state.paramN = paramN + // + // // create a goroutine and push it to the runqueue + // task.start(uintptr(gowrapper), &state) + // + // // run the scheduler + // runtime.wasmExportRun(&state.done) + // + // // if there is a return value, load it and return + // return state.result + // } + + hasReturn := b.fn.Signature.Results() != nil + + // Build the state struct type. + // It stores the function parameters, the 'done' flag, and reserves + // space for a return value if needed. + stateFields := exportedFnType.ParamTypes() + numParams := len(stateFields) + stateFields = append(stateFields, b.ctx.Int1Type()) // 'done' field + if hasReturn { + stateFields = append(stateFields, b.llvmFnType.ReturnType()) + } + stateStruct := b.ctx.StructType(stateFields, false) + + // Allocate the state struct on the stack. + statePtr := builder.CreateAlloca(stateStruct, "status") + + // Initialize the 'done' field. + doneGEP := builder.CreateInBoundsGEP(stateStruct, statePtr, []llvm.Value{ + llvm.ConstInt(b.ctx.Int32Type(), 0, false), + llvm.ConstInt(b.ctx.Int32Type(), uint64(numParams), false), + }, "done.gep") + builder.CreateStore(llvm.ConstNull(b.ctx.Int1Type()), doneGEP) + + // Store all parameters in the state object. + for i, param := range exportedFn.Params() { + gep := builder.CreateInBoundsGEP(stateStruct, statePtr, []llvm.Value{ + llvm.ConstInt(b.ctx.Int32Type(), 0, false), + llvm.ConstInt(b.ctx.Int32Type(), uint64(i), false), + }, "") + builder.CreateStore(param, gep) + } + + // Create a new goroutine and add it to the runqueue. + wrapper := b.createGoroutineStartWrapper(b.llvmFnType, b.llvmFn, "", false, true, pos) + stackSize := llvm.ConstInt(b.uintptrType, b.DefaultStackSize, false) + taskStartFnType, taskStartFn := builder.getFunction(b.program.ImportedPackage("internal/task").Members["start"].(*ssa.Function)) + builder.createCall(taskStartFnType, taskStartFn, []llvm.Value{wrapper, statePtr, stackSize, llvm.Undef(b.dataPtrType)}, "") + + // Run the scheduler. + builder.createRuntimeCall("wasmExportRun", []llvm.Value{doneGEP}, "") + + // Read the return value (if any) and return to the caller of the + // //go:wasmexport function. + if hasReturn { + gep := builder.CreateInBoundsGEP(stateStruct, statePtr, []llvm.Value{ + llvm.ConstInt(b.ctx.Int32Type(), 0, false), + llvm.ConstInt(b.ctx.Int32Type(), uint64(numParams)+1, false), + }, "") + retval := builder.CreateLoad(b.llvmFnType.ReturnType(), gep, "retval") + builder.CreateRet(retval) + } else { + builder.CreateRetVoid() + } + } +} + // createGoroutineStartWrapper creates a wrapper for the task-based // implementation of goroutines. For example, to call a function like this: // @@ -142,7 +279,7 @@ func (b *builder) createGo(instr *ssa.Go) { // to last parameter of the function) is used for this wrapper. If hasContext is // false, the parameter bundle is assumed to have no context parameter and undef // is passed instead. -func (c *compilerContext) createGoroutineStartWrapper(fnType llvm.Type, fn llvm.Value, prefix string, hasContext bool, pos token.Pos) llvm.Value { +func (c *compilerContext) createGoroutineStartWrapper(fnType llvm.Type, fn llvm.Value, prefix string, hasContext, isWasmExport bool, pos token.Pos) llvm.Value { var wrapper llvm.Value b := &builder{ @@ -160,14 +297,18 @@ func (c *compilerContext) createGoroutineStartWrapper(fnType llvm.Type, fn llvm. if !fn.IsAFunction().IsNil() { // See whether this wrapper has already been created. If so, return it. name := fn.Name() - wrapper = c.mod.NamedFunction(name + "$gowrapper") + wrapperName := name + "$gowrapper" + if isWasmExport { + wrapperName += "-wasmexport" + } + wrapper = c.mod.NamedFunction(wrapperName) if !wrapper.IsNil() { return llvm.ConstPtrToInt(wrapper, c.uintptrType) } // Create the wrapper. wrapperType := llvm.FunctionType(c.ctx.VoidType(), []llvm.Type{c.dataPtrType}, false) - wrapper = llvm.AddFunction(c.mod, name+"$gowrapper", wrapperType) + wrapper = llvm.AddFunction(c.mod, wrapperName, wrapperType) c.addStandardAttributes(wrapper) wrapper.SetLinkage(llvm.LinkOnceODRLinkage) wrapper.SetUnnamedAddr(true) @@ -197,23 +338,110 @@ func (c *compilerContext) createGoroutineStartWrapper(fnType llvm.Type, fn llvm. b.SetCurrentDebugLocation(uint(pos.Line), uint(pos.Column), difunc, llvm.Metadata{}) } - // Create the list of params for the call. - paramTypes := fnType.ParamTypes() - if !hasContext { - paramTypes = paramTypes[:len(paramTypes)-1] // strip context parameter - } - params := b.emitPointerUnpack(wrapper.Param(0), paramTypes) - if !hasContext { - params = append(params, llvm.Undef(c.dataPtrType)) // add dummy context parameter - } + if !isWasmExport { + // Regular 'go' instruction. - // Create the call. - b.CreateCall(fnType, fn, params, "") + // Create the list of params for the call. + paramTypes := fnType.ParamTypes() + if !hasContext { + paramTypes = paramTypes[:len(paramTypes)-1] // strip context parameter + } - if c.Scheduler == "asyncify" { - b.CreateCall(deadlockType, deadlock, []llvm.Value{ - llvm.Undef(c.dataPtrType), - }, "") + params := b.emitPointerUnpack(wrapper.Param(0), paramTypes) + if !hasContext { + params = append(params, llvm.Undef(c.dataPtrType)) // add dummy context parameter + } + + // Create the call. + b.CreateCall(fnType, fn, params, "") + + if c.Scheduler == "asyncify" { + b.CreateCall(deadlockType, deadlock, []llvm.Value{ + llvm.Undef(c.dataPtrType), + }, "") + } + } else { + // Goroutine started from a //go:wasmexport pragma. + // The function looks like this: + // + // func foo$gowrapper-wasmexport(state *stateStruct) { + // // load values + // param0 := state.params[0] + // param1 := state.params[1] + // + // // call wrapped functions + // result := foo(param0, param1, ...) + // + // // store result value (if there is any) + // state.result = result + // + // // finish exported function + // state.done = true + // runtime.wasmExportExit() + // } + // + // The state object here looks like: + // + // struct state { + // param0 + // param1 + // param* // etc + // done bool + // result returnType + // } + + returnType := fnType.ReturnType() + hasReturn := returnType != b.ctx.VoidType() + statePtr := wrapper.Param(0) + + // Create the state struct (it must match the type in createWasmExport). + stateFields := fnType.ParamTypes() + numParams := len(stateFields) - 1 + stateFields = stateFields[:numParams:numParams] // strip 'context' parameter + stateFields = append(stateFields, c.ctx.Int1Type()) // 'done' bool + if hasReturn { + stateFields = append(stateFields, returnType) + } + stateStruct := b.ctx.StructType(stateFields, false) + + // Extract parameters from the state object, and call the function + // that's being wrapped. + var callParams []llvm.Value + for i := 0; i < numParams; i++ { + gep := b.CreateInBoundsGEP(stateStruct, statePtr, []llvm.Value{ + llvm.ConstInt(b.ctx.Int32Type(), 0, false), + llvm.ConstInt(b.ctx.Int32Type(), uint64(i), false), + }, "") + param := b.CreateLoad(stateFields[i], gep, "") + callParams = append(callParams, param) + } + callParams = append(callParams, llvm.ConstNull(c.dataPtrType)) // add 'context' parameter + result := b.CreateCall(fnType, fn, callParams, "result") + + // Store the return value back into the shared state. + // Unlike regular goroutines, these special //go:wasmexport + // goroutines can return a value. + if hasReturn { + gep := b.CreateInBoundsGEP(stateStruct, statePtr, []llvm.Value{ + llvm.ConstInt(c.ctx.Int32Type(), 0, false), + llvm.ConstInt(c.ctx.Int32Type(), uint64(numParams)+1, false), + }, "result.ptr") + b.CreateStore(result, gep) + } + + // Mark this function as having finished executing. + // This is important so the runtime knows the exported function + // didn't block. + doneGEP := b.CreateInBoundsGEP(stateStruct, statePtr, []llvm.Value{ + llvm.ConstInt(c.ctx.Int32Type(), 0, false), + llvm.ConstInt(c.ctx.Int32Type(), uint64(numParams), false), + }, "done.gep") + b.CreateStore(llvm.ConstInt(b.ctx.Int1Type(), 1, false), doneGEP) + + // Call back into the runtime. This will exit the goroutine, switch + // back to the scheduler, which will in turn return from the + // //go:wasmexport function. + b.createRuntimeCall("wasmExportExit", nil, "") } } else { @@ -295,5 +523,5 @@ func (c *compilerContext) createGoroutineStartWrapper(fnType llvm.Type, fn llvm. } // Return a ptrtoint of the wrapper, not the function itself. - return b.CreatePtrToInt(wrapper, c.uintptrType, "") + return llvm.ConstPtrToInt(wrapper, c.uintptrType) } diff --git a/compiler/symbol.go b/compiler/symbol.go index 29f0095208..da00462e0f 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -23,15 +23,17 @@ import ( // The linkName value contains a valid link name, even if //go:linkname is not // present. type functionInfo struct { - wasmModule string // go:wasm-module - wasmName string // wasm-export-name or wasm-import-name in the IR - linkName string // go:linkname, go:export - the IR function name - section string // go:section - object file section name - exported bool // go:export, CGo - interrupt bool // go:interrupt - nobounds bool // go:nobounds - variadic bool // go:variadic (CGo only) - inline inlineType // go:inline + wasmModule string // go:wasm-module + wasmName string // wasm-export-name or wasm-import-name in the IR + wasmExport string // go:wasmexport is defined (export is unset, this adds an exported wrapper) + wasmExportPos token.Pos // position of //go:wasmexport comment + linkName string // go:linkname, go:export - the IR function name + section string // go:section - object file section name + exported bool // go:export, CGo + interrupt bool // go:interrupt + nobounds bool // go:nobounds + variadic bool // go:variadic (CGo only) + inline inlineType // go:inline } type inlineType int @@ -292,10 +294,39 @@ func (c *compilerContext) parsePragmas(info *functionInfo, f *ssa.Function) { if len(parts) != 3 { continue } - c.checkWasmImport(f, comment.Text) + if f.Blocks != nil { + // Defined functions cannot be exported. + c.addError(f.Pos(), "can only use //go:wasmimport on declarations") + continue + } + c.checkWasmImportExport(f, comment.Text) info.exported = true info.wasmModule = parts[1] info.wasmName = parts[2] + case "//go:wasmexport": + if f.Blocks == nil { + c.addError(f.Pos(), "can only use //go:wasmexport on definitions") + continue + } + if len(parts) != 2 { + c.addError(f.Pos(), fmt.Sprintf("expected one parameter to //go:wasmimport, not %d", len(parts)-1)) + continue + } + name := parts[1] + if name == "_start" || name == "_initialize" { + c.addError(f.Pos(), fmt.Sprintf("//go:wasmexport does not allow %#v", name)) + continue + } + if c.BuildMode != "c-shared" && f.RelString(nil) == "main.main" { + c.addError(f.Pos(), fmt.Sprintf("//go:wasmexport does not allow main.main to be exported with -buildmode=%s", c.BuildMode)) + continue + } + if c.GOOS != "wasip1" { + c.addError(f.Pos(), "//go:wasmexport is currently only supported with GOOS=wasip1") + } + c.checkWasmImportExport(f, comment.Text) + info.wasmExport = name + info.wasmExportPos = comment.Slash case "//go:inline": info.inline = inlineHint case "//go:noinline": @@ -342,22 +373,17 @@ func (c *compilerContext) parsePragmas(info *functionInfo, f *ssa.Function) { } } -// Check whether this function cannot be used in //go:wasmimport. It will add an -// error if this is the case. +// Check whether this function can be used in //go:wasmimport or +// //go:wasmexport. It will add an error if this is not the case. // // The list of allowed types is based on this proposal: // https://github.com/golang/go/issues/59149 -func (c *compilerContext) checkWasmImport(f *ssa.Function, pragma string) { +func (c *compilerContext) checkWasmImportExport(f *ssa.Function, pragma string) { if c.pkg.Path() == "runtime" || c.pkg.Path() == "syscall/js" || c.pkg.Path() == "syscall" { // The runtime is a special case. Allow all kinds of parameters // (importantly, including pointers). return } - if f.Blocks != nil { - // Defined functions cannot be exported. - c.addError(f.Pos(), "can only use //go:wasmimport on declarations") - return - } if f.Signature.Results().Len() > 1 { c.addError(f.Signature.Results().At(1).Pos(), fmt.Sprintf("%s: too many return values", pragma)) } else if f.Signature.Results().Len() == 1 { diff --git a/main.go b/main.go index 51e62aaaee..8568c0be9b 100644 --- a/main.go +++ b/main.go @@ -1392,6 +1392,7 @@ func main() { var tags buildutil.TagsFlag flag.Var(&tags, "tags", "a space-separated list of extra build tags") target := flag.String("target", "", "chip/board name or JSON target specification file") + buildMode := flag.String("buildmode", "", "build mode to use (default, c-shared)") var stackSize uint64 flag.Func("stack-size", "goroutine stack size (if unknown at compile time)", func(s string) error { size, err := bytesize.Parse(s) @@ -1499,6 +1500,7 @@ func main() { GOARM: goenv.Get("GOARM"), GOMIPS: goenv.Get("GOMIPS"), Target: *target, + BuildMode: *buildMode, StackSize: stackSize, Opt: *opt, GC: *gc, diff --git a/src/runtime/runtime_wasip1.go b/src/runtime/runtime_wasip1.go index 4a1afb2abd..b2a23901af 100644 --- a/src/runtime/runtime_wasip1.go +++ b/src/runtime/runtime_wasip1.go @@ -13,15 +13,6 @@ type timeUnit int64 //export __wasm_call_ctors func __wasm_call_ctors() -//export _start -func _start() { - // These need to be initialized early so that the heap can be initialized. - heapStart = uintptr(unsafe.Pointer(&heapStartSymbol)) - heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize) - run() - __stdio_exit() -} - // Read the command line arguments from WASI. // For example, they can be passed to a program with wasmtime like this: // @@ -89,7 +80,7 @@ var ( sleepTicksNEvents uint32 ) -func sleepTicks(d timeUnit) { +func wasiSleepTicks(d timeUnit) { sleepTicksSubscription.u.u.timeout = uint64(d) poll_oneoff(&sleepTicksSubscription, &sleepTicksResult, 1, &sleepTicksNEvents) } diff --git a/src/runtime/runtime_wasm_command.go b/src/runtime/runtime_wasm_command.go new file mode 100644 index 0000000000..0b5f9289c6 --- /dev/null +++ b/src/runtime/runtime_wasm_command.go @@ -0,0 +1,23 @@ +//go:build wasip1 && !tinygo_wasm_reactor + +package runtime + +import "unsafe" + +//export _start +func _start() { + // These need to be initialized early so that the heap can be initialized. + heapStart = uintptr(unsafe.Pointer(&heapStartSymbol)) + heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize) + run() + __stdio_exit() +} + +func sleepTicks(d timeUnit) { + wasiSleepTicks(d) +} + +// TODO: we should define wasmExportRun and wasmExportExit here so that +// //go:wasmexport also works without -buildmode=c-shared (for when a +// //go:wasmexport call). Calling //go:wasmexport functions is only allowed +// before main.main returns. diff --git a/src/runtime/runtime_wasm_reactor.go b/src/runtime/runtime_wasm_reactor.go new file mode 100644 index 0000000000..7256d56ad3 --- /dev/null +++ b/src/runtime/runtime_wasm_reactor.go @@ -0,0 +1,79 @@ +//go:build wasip1 && tinygo_wasm_reactor + +package runtime + +import ( + "internal/task" + "unsafe" +) + +var packagesInitialized = false + +//export _initialize +func _initialize() { + // This function is called before any //go:wasmexport functions are called + // to initialize everything. It must not block. + + // Initialize the heap. + heapStart = uintptr(unsafe.Pointer(&heapStartSymbol)) + heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize) + initHeap() + + if hasScheduler { + // A package initializer might do funky stuff like start a goroutine and + // wait until it completes, so we have to run package initializers in a + // goroutine. + go func() { + initAll() + packagesInitialized = true + }() + scheduler(true) + if !packagesInitialized { + // Unlikely, but if package initializers do something blocking (like + // time.Sleep()), that's a bug. + runtimePanic("package initializer blocks") + } + } else { + // There are no goroutines (except for the main one, if you can call it + // that), so we can just run all the package initializers. + initAll() + } +} + +func sleepTicks(d timeUnit) { + // See the proposal: + // > When the goroutine running the exported function blocks for any reason, + // > the function will yield to the Go runtime. The Go runtime will schedule + // > other goroutines as necessary. If there are no other goroutines, the + // > application will crash with a deadlock, as there is no way to proceed, + // > and Wasm code cannot block. + // We can only get here when there are no runnable goroutines. In other + // words, when the exported function blocks and there are no other + // goroutines to run. So we crash with a deadlock. + runtimePanic("all goroutines are asleep - deadlock!") +} + +// Called from within a //go:wasmexport wrapper (the one that's exported from +// the wasm module) after the goroutine has been queued. Just run the scheduler, +// and check that the goroutine finished when the scheduler is idle (as required +// by the //go:wasmexport proposal). +// +// This function is not called when the scheduler is disabled. +func wasmExportRun(done *bool) { + scheduler(true) + if !*done { + runtimePanic("//go:wasmexport function did not finish") + } +} + +// Called from the goroutine wrapper for the //go:wasmexport function. It just +// signals to the runtime that the //go:wasmexport call has finished, and can +// switch back to the wasmExportRun function. +// +// This function is not called when the scheduler is disabled. +func wasmExportExit() { + task.Pause() + + // TODO: we could cache the allocated stack so we don't have to keep + // allocating a new stack on every //go:wasmexport call. +} diff --git a/src/runtime/runtime_wasm_unknown.go b/src/runtime/runtime_wasm_unknown.go index d307a4f87e..9cd12a083c 100644 --- a/src/runtime/runtime_wasm_unknown.go +++ b/src/runtime/runtime_wasm_unknown.go @@ -2,6 +2,9 @@ package runtime +// TODO: this is essentially reactor mode wasm. So we might want to support +// -buildmode=c-shared (and default to it). + import "unsafe" type timeUnit int64 diff --git a/src/runtime/scheduler.go b/src/runtime/scheduler.go index 30b2da8a62..2f22876527 100644 --- a/src/runtime/scheduler.go +++ b/src/runtime/scheduler.go @@ -157,7 +157,13 @@ func removeTimer(tim *timer) bool { } // Run the scheduler until all tasks have finished. -func scheduler() { +// There are a few special cases: +// - When returnAtDeadlock is true, it also returns when there are no more +// runnable goroutines. +// - When using the asyncify scheduler, it returns when it has to wait +// (JavaScript uses setTimeout so the scheduler must return to the JS +// environment). +func scheduler(returnAtDeadlock bool) { // Main scheduler loop. var now timeUnit for !schedulerDone { @@ -193,6 +199,9 @@ func scheduler() { t := runqueue.Pop() if t == nil { if sleepQueue == nil && timerQueue == nil { + if returnAtDeadlock { + return + } if asyncScheduler { // JavaScript is treated specially, see below. return diff --git a/src/runtime/scheduler_any.go b/src/runtime/scheduler_any.go index 0911a2dc73..5e969f84ff 100644 --- a/src/runtime/scheduler_any.go +++ b/src/runtime/scheduler_any.go @@ -25,7 +25,7 @@ func run() { callMain() schedulerDone = true }() - scheduler() + scheduler(false) } const hasScheduler = true