aboutsummaryrefslogtreecommitdiffhomepage
path: root/compiler
diff options
context:
space:
mode:
authorAyke <[email protected]>2024-10-05 00:33:47 +0200
committerGitHub <[email protected]>2024-10-04 15:33:47 -0700
commit9da8b5c786880e47f6f96b82be7c410af6f9011b (patch)
tree47bf837bbda8f5e174447b165f8bb4ad8d7f523e /compiler
parent407889864f1749fee46312fa191bf53ff59137ce (diff)
downloadtinygo-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.go6
-rw-r--r--compiler/goroutine.go273
-rw-r--r--compiler/symbol.go76
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 {