mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-14 07:53:40 +00:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
+253
-20
@@ -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)
|
||||
}
|
||||
|
||||
+58
-18
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user