diff options
author | Ayke <[email protected]> | 2024-10-05 00:33:47 +0200 |
---|---|---|
committer | GitHub <[email protected]> | 2024-10-04 15:33:47 -0700 |
commit | 9da8b5c786880e47f6f96b82be7c410af6f9011b (patch) | |
tree | 47bf837bbda8f5e174447b165f8bb4ad8d7f523e /compiler | |
parent | 407889864f1749fee46312fa191bf53ff59137ce (diff) | |
download | tinygo-9da8b5c786880e47f6f96b82be7c410af6f9011b.tar.gz tinygo-9da8b5c786880e47f6f96b82be7c410af6f9011b.zip |
wasm: add `//go:wasmexport` support (#4451)
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 and wasm-unknown, but it is
certainly possible to extend it to other targets like GOOS=js and
wasip2.
Diffstat (limited to 'compiler')
-rw-r--r-- | compiler/compiler.go | 6 | ||||
-rw-r--r-- | compiler/goroutine.go | 273 | ||||
-rw-r--r-- | compiler/symbol.go | 76 |
3 files changed, 317 insertions, 38 deletions
diff --git a/compiler/compiler.go b/compiler/compiler.go index 6756fe969..752e4a5c6 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 a23556345..701797152 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 32eb55107..4a080cbbc 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.archFamily() != "wasm32" { + c.addError(f.Pos(), "//go:wasmexport is only supported on wasm") + } + 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 { |