From 02e95377bdc23a6876551c4f38215339a6e790c1 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. --- builder/build.go | 8 + compileopts/config.go | 8 + compileopts/options.go | 10 + compiler/compiler.go | 6 + compiler/goroutine.go | 273 +++++++++++++++++++++-- compiler/symbol.go | 76 +++++-- go.mod | 1 + go.sum | 7 +- main.go | 2 + main_test.go | 106 +++++++++ src/runtime/runtime_wasip1.go | 95 +++++++- src/runtime/runtime_wasm_js_scheduler.go | 4 +- src/runtime/runtime_wasm_unknown.go | 3 + src/runtime/scheduler.go | 11 +- src/runtime/scheduler_any.go | 2 +- testdata/wasmexport.go | 53 +++++ testdata/wasmexport.txt | 11 + 17 files changed, 621 insertions(+), 55 deletions(-) create mode 100644 testdata/wasmexport.go create mode 100644 testdata/wasmexport.txt 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 cc1f4d61ce..2329ef16ab 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. diff --git a/compileopts/options.go b/compileopts/options.go index 980097200d..b83f6f63ba 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 @@ -61,6 +63,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 6756fe9693..752e4a5c62 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 a235563450..701797152c 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" ) @@ -101,7 +102,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 @@ -121,6 +122,147 @@ 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) + + // Insert an assertion to make sure this //go:wasmexport function is not + // called at a time when it is not allowed (for example, before the runtime + // is initialized). + builder.createRuntimeCall("wasmExportCheckRun", nil, "") + + 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: // @@ -144,7 +286,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{ @@ -162,14 +304,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) @@ -199,23 +345,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, "") + + // 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 { @@ -297,5 +530,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 32eb55107c..9f74beb686 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 @@ -241,8 +243,22 @@ func (c *compilerContext) getFunctionInfo(f *ssa.Function) functionInfo { // Pick the default linkName. linkName: f.RelString(nil), } + + // Check for a few runtime functions that are treated specially. + if info.linkName == "runtime.wasmEntryReactor" && c.BuildMode == "c-shared" { + info.linkName = "_initialize" + info.wasmName = "_initialize" + info.exported = true + } + if info.linkName == "runtime.wasmEntryCommand" && c.BuildMode == "default" { + info.linkName = "_start" + info.wasmName = "_start" + info.exported = true + } + // Check for //go: pragmas, which may change the link name (among others). c.parsePragmas(&info, f) + c.functionInfos[f] = info return info } @@ -296,10 +312,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": @@ -346,22 +391,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/go.mod b/go.mod index bf85ef3ad1..a4de141365 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-tty v0.0.4 github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 + github.com/tetratelabs/wazero v1.6.0 go.bug.st/serial v1.6.0 golang.org/x/net v0.26.0 golang.org/x/sys v0.21.0 diff --git a/go.sum b/go.sum index 7a6d1f4a97..d3c1bd310c 100644 --- a/go.sum +++ b/go.sum @@ -12,7 +12,6 @@ github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moA github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= @@ -45,19 +44,18 @@ github.com/mattn/go-tty v0.0.4/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3px github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5 h1:1SoBaSPudixRecmlHXb/GxmaD3fLMtHIDN13QujwQuc= github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g= +github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= go.bug.st/serial v1.6.0 h1:mAbRGN4cKE2J5gMwsMHC2KQisdLRQssO9WSM+rbZJ8A= go.bug.st/serial v1.6.0/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -76,6 +74,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= tinygo.org/x/go-llvm v0.0.0-20240627184919-3b50c76783a8 h1:bLsZXRUBavt++CJlMN7sppNziqu3LyamESLhFJcpqFQ= tinygo.org/x/go-llvm v0.0.0-20240627184919-3b50c76783a8/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0= diff --git a/main.go b/main.go index 6f257d1b21..fe8a3fb15a 100644 --- a/main.go +++ b/main.go @@ -1500,6 +1500,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) @@ -1608,6 +1609,7 @@ func main() { GOARM: goenv.Get("GOARM"), GOMIPS: goenv.Get("GOMIPS"), Target: *target, + BuildMode: *buildMode, StackSize: stackSize, Opt: *opt, GC: *gc, diff --git a/main_test.go b/main_test.go index 62eb5c51b9..c7f1a787d6 100644 --- a/main_test.go +++ b/main_test.go @@ -6,6 +6,7 @@ package main import ( "bufio" "bytes" + "context" "errors" "flag" "io" @@ -21,6 +22,9 @@ import ( "time" "github.com/aykevl/go-wasm" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" "github.com/tinygo-org/tinygo/builder" "github.com/tinygo-org/tinygo/compileopts" "github.com/tinygo-org/tinygo/diagnostics" @@ -523,6 +527,108 @@ func TestWebAssembly(t *testing.T) { } } +func TestWasmExport(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + target string + buildMode string + } + + for _, tc := range []testCase{ + {name: "WASIp1-reactor", target: "wasip1", buildMode: "c-shared"}, + } { + t.Run(tc.name, func(t *testing.T) { + // Build the wasm binary. + tmpdir := t.TempDir() + options := optionsFromTarget(tc.target, sema) + options.BuildMode = tc.buildMode + buildConfig, err := builder.NewConfig(&options) + if err != nil { + t.Fatal(err) + } + result, err := builder.Build("testdata/wasmexport.go", ".wasm", tmpdir, buildConfig) + if err != nil { + t.Fatal("failed to build binary:", err) + } + + // Read the wasm binary back into memory. + data, err := os.ReadFile(result.Binary) + if err != nil { + t.Fatal("could not read wasm binary: ", err) + } + + // Set up the wazero runtime. + output := &bytes.Buffer{} + ctx := context.Background() + r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter()) + defer r.Close(ctx) + config := wazero.NewModuleConfig(). + WithStdout(output).WithStderr(output). + WithStartFunctions() + + var mod api.Module + + // Add wasip1 module. + wasi_snapshot_preview1.MustInstantiate(ctx, r) + + // Add custom "tester" module. + callOutside := func(a, b int32) int32 { + results, err := mod.ExportedFunction("add").Call(ctx, uint64(a), uint64(b)) + if err != nil { + t.Error("could not call exported add function:", err) + } + return int32(results[0]) + } + builder := r.NewHostModuleBuilder("tester") + builder.NewFunctionBuilder().WithFunc(callOutside).Export("callOutside") + _, err = builder.Instantiate(ctx) + if err != nil { + t.Fatal(err) + } + + // Parse and instantiate the wasm. + mod, err = r.InstantiateWithConfig(ctx, data, config) + if err != nil { + t.Fatal("could not instantiate wasm module:", err) + } + + // Run the _initialize call, because this is reactor mode wasm. + mod.ExportedFunction("_initialize").Call(ctx) + + mustCall := func(results []uint64, err error) []uint64 { + if err != nil { + t.Error("failed to run function:", err) + } + return results + } + + // Test an exported function without params or return value. + mustCall(mod.ExportedFunction("hello").Call(ctx)) + + // Test that we can call an exported function more than once. + t.Logf("add(3, 5) = %v", mustCall(mod.ExportedFunction("add").Call(ctx, 3, 5))) + t.Logf("add(7, 9) = %v", mustCall(mod.ExportedFunction("add").Call(ctx, 7, 9))) + t.Logf("add(6, 1) = %v", mustCall(mod.ExportedFunction("add").Call(ctx, 6, 1))) + + // Test that imported functions can call exported functions again. + t.Logf("reentrantCall(2, 3) = %v", mustCall(mod.ExportedFunction("reentrantCall").Call(ctx, 2, 3))) + t.Logf("reentrantCall(1, 8) = %v", mustCall(mod.ExportedFunction("reentrantCall").Call(ctx, 1, 8))) + + expectedOutput, err := os.ReadFile("testdata/wasmexport.txt") + if err != nil { + t.Fatal("could not read output file:", err) + } + + actual := output.Bytes() + if !bytes.Equal(actual, expectedOutput) { + t.Error(string(Diff("expected", expectedOutput, "actual", actual))) + } + }) + } +} + func TestTest(t *testing.T) { t.Parallel() diff --git a/src/runtime/runtime_wasip1.go b/src/runtime/runtime_wasip1.go index 4a1afb2abd..10660a0d1d 100644 --- a/src/runtime/runtime_wasip1.go +++ b/src/runtime/runtime_wasip1.go @@ -3,6 +3,7 @@ package runtime import ( + "internal/task" "unsafe" ) @@ -13,21 +14,54 @@ type timeUnit int64 //export __wasm_call_ctors func __wasm_call_ctors() -//export _start -func _start() { +// Read the command line arguments from WASI. +// For example, they can be passed to a program with wasmtime like this: +// +// wasmtime run ./program.wasm arg1 arg2 +func init() { + __wasm_call_ctors() +} + +// This is the _start entry point, when using -buildmode=default. +func wasmEntryCommand() { // 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) + wasmExportState = wasmExportStateInMain run() + wasmExportState = wasmExportStateExited __stdio_exit() } -// Read the command line arguments from WASI. -// For example, they can be passed to a program with wasmtime like this: -// -// wasmtime run ./program.wasm arg1 arg2 -func init() { - __wasm_call_ctors() +// This is the _initialize entry point, when using -buildmode=c-shared. +func wasmEntryReactor() { + // 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() + wasmExportState = wasmExportStateReactor + }() + scheduler(true) + if wasmExportState != wasmExportStateReactor { + // 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() + } } var args []string @@ -100,6 +134,51 @@ func ticks() timeUnit { return timeUnit(nano) } +// Track which state we're in: before (or during) init, running inside +// main.main, after main.main returned, or reactor mode (after init). +var wasmExportState uint8 + +const ( + wasmExportStateInit = iota + wasmExportStateInMain + wasmExportStateExited + wasmExportStateReactor +) + +func wasmExportCheckRun() { + switch wasmExportState { + case wasmExportStateInit: + runtimePanic("//go:wasmexport function called before runtime initialization") + case wasmExportStateExited: + runtimePanic("//go:wasmexport function called after main.main returned") + } +} + +// 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. +} + // Implementations of WASI APIs //go:wasmimport wasi_snapshot_preview1 args_get diff --git a/src/runtime/runtime_wasm_js_scheduler.go b/src/runtime/runtime_wasm_js_scheduler.go index 0edde29eb5..94018336e4 100644 --- a/src/runtime/runtime_wasm_js_scheduler.go +++ b/src/runtime/runtime_wasm_js_scheduler.go @@ -14,7 +14,7 @@ func resume() { } wasmNested = true - scheduler() + scheduler(false) wasmNested = false } @@ -26,6 +26,6 @@ func go_scheduler() { } wasmNested = true - scheduler() + scheduler(false) wasmNested = false } 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 diff --git a/testdata/wasmexport.go b/testdata/wasmexport.go new file mode 100644 index 0000000000..732152a99d --- /dev/null +++ b/testdata/wasmexport.go @@ -0,0 +1,53 @@ +package main + +import "time" + +// This is a limited test for //go:wasmexport. +// It tests various things, like init() and scheduling but notably it does _not_ +// test -buildmode=default which has somewhat different semantics. + +func init() { + println("called init") + go adder() +} + +func main() { + // main.main is not used when using -buildmode=c-shared. + println("called main") +} + +//go:wasmexport hello +func hello() { + println("hello!") +} + +//go:wasmexport add +func add(a, b int) int { + println("called add:", a, b) + addInputs <- a + addInputs <- b + return <-addOutput +} + +var addInputs = make(chan int) +var addOutput = make(chan int) + +func adder() { + for { + a := <-addInputs + b := <-addInputs + time.Sleep(time.Millisecond) + addOutput <- a + b + } +} + +//go:wasmimport tester callOutside +func callOutside(a, b int) int + +//go:wasmexport reentrantCall +func reentrantCall(a, b int) int { + println("reentrantCall:", a, b) + result := callOutside(a, b) + println("reentrantCall result:", result) + return result +} diff --git a/testdata/wasmexport.txt b/testdata/wasmexport.txt new file mode 100644 index 0000000000..484a0ce8d8 --- /dev/null +++ b/testdata/wasmexport.txt @@ -0,0 +1,11 @@ +called init +hello! +called add: 3 5 +called add: 7 9 +called add: 6 1 +reentrantCall: 2 3 +called add: 2 3 +reentrantCall result: 5 +reentrantCall: 1 8 +called add: 1 8 +reentrantCall result: 9