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:
Ayke
2024-10-05 00:33:47 +02:00
committed by GitHub
parent 407889864f
commit 9da8b5c786
22 changed files with 765 additions and 67 deletions
+10 -2
View File
@@ -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 !strings.HasPrefix(config.Triple(), "wasm32-") {
return result, fmt.Errorf("buildmode c-shared is only supported on wasm 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" {
@@ -880,7 +888,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
err := cmd.Run()
if err != nil {
return fmt.Errorf("wasm-tools failed: %w", err)
return fmt.Errorf("`wasm-tools component embed` failed: %w", err)
}
// wasm-tools component new embedded.wasm -o component.wasm
@@ -902,7 +910,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
err = cmd.Run()
if err != nil {
return fmt.Errorf("wasm-tools failed: %w", err)
return fmt.Errorf("`wasm-tools component new` failed: %w", err)
}
}
+11
View File
@@ -33,6 +33,17 @@ 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
}
if c.Target.BuildMode != "" {
return c.Target.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.
+10
View File
@@ -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 {
+1
View File
@@ -32,6 +32,7 @@ type TargetSpec struct {
GOARCH string `json:"goarch,omitempty"`
SoftFloat bool // used for non-baremetal systems (GOMIPS=softfloat etc)
BuildTags []string `json:"build-tags,omitempty"`
BuildMode string `json:"buildmode,omitempty"` // default build mode (if nothing specified)
GC string `json:"gc,omitempty"`
Scheduler string `json:"scheduler,omitempty"`
Serial string `json:"serial,omitempty"` // which serial output to use (uart, usb, none)
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+1
View File
@@ -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
+2 -5
View File
@@ -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=
+2
View File
@@ -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,
+187
View File
@@ -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"
@@ -524,6 +528,189 @@ func TestWebAssembly(t *testing.T) {
}
}
func TestWasmExport(t *testing.T) {
t.Parallel()
type testCase struct {
name string
target string
buildMode string
scheduler string
file string
noOutput bool
command bool // call _start (command mode) instead of _initialize
}
tests := []testCase{
// "command mode" WASI
{
name: "WASIp1-command",
target: "wasip1",
command: true,
},
// "reactor mode" WASI (with -buildmode=c-shared)
{
name: "WASIp1-reactor",
target: "wasip1",
buildMode: "c-shared",
},
// Make sure reactor mode also works without a scheduler.
{
name: "WASIp1-reactor-noscheduler",
target: "wasip1",
buildMode: "c-shared",
scheduler: "none",
file: "wasmexport-noscheduler.go",
},
// Test -target=wasm-unknown with the default build mode (which is
// c-shared).
{
name: "wasm-unknown-reactor",
target: "wasm-unknown",
file: "wasmexport-noscheduler.go",
noOutput: true, // wasm-unknown cannot produce output
},
// Test -target=wasm-unknown with -buildmode=default, which makes it run
// in command mode.
{
name: "wasm-unknown-command",
target: "wasm-unknown",
buildMode: "default",
file: "wasmexport-noscheduler.go",
noOutput: true, // wasm-unknown cannot produce output
command: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Build the wasm binary.
tmpdir := t.TempDir()
options := optionsFromTarget(tc.target, sema)
options.BuildMode = tc.buildMode
options.Scheduler = tc.scheduler
buildConfig, err := builder.NewConfig(&options)
if err != nil {
t.Fatal(err)
}
filename := "wasmexport.go"
if tc.file != "" {
filename = tc.file
}
result, err := builder.Build("testdata/"+filename, ".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()
// Prepare for testing.
var mod api.Module
mustCall := func(results []uint64, err error) []uint64 {
if err != nil {
t.Error("failed to run function:", err)
}
return results
}
checkResult := func(name string, results []uint64, expected []uint64) {
if len(results) != len(expected) {
t.Errorf("%s: expected %v but got %v", name, expected, results)
}
for i, result := range results {
if result != expected[i] {
t.Errorf("%s: expected %v but got %v", name, expected, results)
break
}
}
}
runTests := func() {
// Test an exported function without params or return value.
checkResult("hello()", mustCall(mod.ExportedFunction("hello").Call(ctx)), nil)
// Test that we can call an exported function more than once.
checkResult("add(3, 5)", mustCall(mod.ExportedFunction("add").Call(ctx, 3, 5)), []uint64{8})
checkResult("add(7, 9)", mustCall(mod.ExportedFunction("add").Call(ctx, 7, 9)), []uint64{16})
checkResult("add(6, 1)", mustCall(mod.ExportedFunction("add").Call(ctx, 6, 1)), []uint64{7})
// Test that imported functions can call exported functions
// again.
checkResult("reentrantCall(2, 3)", mustCall(mod.ExportedFunction("reentrantCall").Call(ctx, 2, 3)), []uint64{5})
checkResult("reentrantCall(1, 8)", mustCall(mod.ExportedFunction("reentrantCall").Call(ctx, 1, 8)), []uint64{9})
}
// 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])
}
callTestMain := func() {
runTests()
}
builder := r.NewHostModuleBuilder("tester")
builder.NewFunctionBuilder().WithFunc(callOutside).Export("callOutside")
builder.NewFunctionBuilder().WithFunc(callTestMain).Export("callTestMain")
_, 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)
}
// Initialize the module and run the tests.
if tc.command {
// Call _start (the entry point), which calls
// tester.callTestMain, which then runs all the tests.
mustCall(mod.ExportedFunction("_start").Call(ctx))
} else {
// Run the _initialize call, because this is reactor mode wasm.
mustCall(mod.ExportedFunction("_initialize").Call(ctx))
runTests()
}
// Check that the output matches the expected output.
// (Skip this for wasm-unknown because it can't produce output).
if !tc.noOutput {
expectedOutput, err := os.ReadFile("testdata/wasmexport.txt")
if err != nil {
t.Fatal("could not read output file:", err)
}
actual := output.Bytes()
expectedOutput = bytes.ReplaceAll(expectedOutput, []byte("\r\n"), []byte("\n"))
actual = bytes.ReplaceAll(actual, []byte("\r\n"), []byte("\n"))
if !bytes.Equal(actual, expectedOutput) {
t.Error(string(Diff("expected", expectedOutput, "actual", actual)))
}
}
})
}
}
func TestTest(t *testing.T) {
t.Parallel()
+3
View File
@@ -1,5 +1,8 @@
//go:build tinygo.wasm && !wasm_unknown && !wasip2
// This file is for wasm/wasip1 and for wasm/js, which both use much of the
// WASIp1 API.
package runtime
import (
+4 -9
View File
@@ -13,15 +13,6 @@ type timeUnit int64
//export __wasm_call_ctors
func __wasm_call_ctors()
//export _start
func _start() {
// These need to be initialized early so that the heap can be initialized.
heapStart = uintptr(unsafe.Pointer(&heapStartSymbol))
heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize)
run()
__stdio_exit()
}
// Read the command line arguments from WASI.
// For example, they can be passed to a program with wasmtime like this:
//
@@ -100,6 +91,10 @@ func ticks() timeUnit {
return timeUnit(nano)
}
func beforeExit() {
__stdio_exit()
}
// Implementations of WASI APIs
//go:wasmimport wasi_snapshot_preview1 args_get
+2 -2
View File
@@ -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
}
+5 -9
View File
@@ -2,7 +2,8 @@
package runtime
import "unsafe"
// TODO: this is essentially reactor mode wasm. So we might want to support
// -buildmode=c-shared (and default to it).
type timeUnit int64
@@ -11,14 +12,6 @@ type timeUnit int64
//export __wasm_call_ctors
func __wasm_call_ctors()
//export _initialize
func _initialize() {
// 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)
initAll()
}
func init() {
__wasm_call_ctors()
}
@@ -40,3 +33,6 @@ func sleepTicks(d timeUnit) {
func ticks() timeUnit {
return timeUnit(0)
}
func beforeExit() {
}
+100
View File
@@ -0,0 +1,100 @@
//go:build tinygo.wasm && !wasip2 && !js
package runtime
// Entry points for WebAssembly modules, and runtime support for
// //go:wasmexport: runtime.wasmExport* function calls are inserted by the
// compiler for //go:wasmexport support.
import (
"internal/task"
"unsafe"
)
// 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
beforeExit()
}
// 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()
wasmExportState = wasmExportStateReactor
}
}
// 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.
}
+10 -1
View File
@@ -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
+1 -1
View File
@@ -25,7 +25,7 @@ func run() {
callMain()
schedulerDone = true
}()
scheduler()
scheduler(false)
}
const hasScheduler = true
+1
View File
@@ -3,6 +3,7 @@
"cpu": "generic",
"features": "+mutable-globals,+nontrapping-fptoint,+sign-ext,-bulk-memory",
"build-tags": ["tinygo.wasm", "wasm_unknown"],
"buildmode": "c-shared",
"goos": "linux",
"goarch": "arm",
"linker": "wasm-ld",
+35
View File
@@ -0,0 +1,35 @@
package main
func init() {
println("called init")
}
//go:wasmimport tester callTestMain
func callTestMain()
func main() {
// main.main is not used when using -buildmode=c-shared.
callTestMain()
}
//go:wasmexport hello
func hello() {
println("hello!")
}
//go:wasmexport add
func add(a, b int) int {
println("called add:", a, b)
return 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
}
+52
View File
@@ -0,0 +1,52 @@
package main
import "time"
func init() {
println("called init")
go adder()
}
//go:wasmimport tester callTestMain
func callTestMain()
func main() {
// main.main is not used when using -buildmode=c-shared.
callTestMain()
}
//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
}
+11
View File
@@ -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