aboutsummaryrefslogtreecommitdiffhomepage
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
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.
-rw-r--r--builder/build.go12
-rw-r--r--compileopts/config.go11
-rw-r--r--compileopts/options.go10
-rw-r--r--compileopts/target.go1
-rw-r--r--compiler/compiler.go6
-rw-r--r--compiler/goroutine.go273
-rw-r--r--compiler/symbol.go76
-rw-r--r--go.mod1
-rw-r--r--go.sum7
-rw-r--r--main.go2
-rw-r--r--main_test.go187
-rw-r--r--src/runtime/runtime_tinygowasm.go3
-rw-r--r--src/runtime/runtime_wasip1.go13
-rw-r--r--src/runtime/runtime_wasm_js_scheduler.go4
-rw-r--r--src/runtime/runtime_wasm_unknown.go14
-rw-r--r--src/runtime/runtime_wasmentry.go100
-rw-r--r--src/runtime/scheduler.go11
-rw-r--r--src/runtime/scheduler_any.go2
-rw-r--r--targets/wasm-unknown.json1
-rw-r--r--testdata/wasmexport-noscheduler.go35
-rw-r--r--testdata/wasmexport.go52
-rw-r--r--testdata/wasmexport.txt11
22 files changed, 765 insertions, 67 deletions
diff --git a/builder/build.go b/builder/build.go
index 3f12b9991..4c49fe13c 100644
--- a/builder/build.go
+++ b/builder/build.go
@@ -197,6 +197,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
ABI: config.ABI(),
GOOS: config.GOOS(),
GOARCH: config.GOARCH(),
+ BuildMode: config.BuildMode(),
CodeModel: config.CodeModel(),
RelocationModel: config.RelocationModel(),
SizeLevel: sizeLevel,
@@ -649,6 +650,13 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
result.Binary = result.Executable // final file
ldflags := append(config.LDFlags(), "-o", result.Executable)
+ if config.Options.BuildMode == "c-shared" {
+ if !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)
}
}
diff --git a/compileopts/config.go b/compileopts/config.go
index cc1f4d61c..44d3b005c 100644
--- a/compileopts/config.go
+++ b/compileopts/config.go
@@ -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.
diff --git a/compileopts/options.go b/compileopts/options.go
index 980097200..b83f6f63b 100644
--- a/compileopts/options.go
+++ b/compileopts/options.go
@@ -8,6 +8,7 @@ import (
)
var (
+ validBuildModeOptions = []string{"default", "c-shared"}
validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise"}
validSchedulerOptions = []string{"none", "tasks", "asyncify"}
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
@@ -26,6 +27,7 @@ type Options struct {
GOMIPS string // environment variable (only used with GOARCH=mips and GOARCH=mipsle)
Directory string // working dir, leave it unset to use the current working dir
Target string
+ BuildMode string // -buildmode flag
Opt string
GC string
PanicStrategy string
@@ -61,6 +63,14 @@ type Options struct {
// Verify performs a validation on the given options, raising an error if options are not valid.
func (o *Options) Verify() error {
+ if o.BuildMode != "" {
+ valid := isInArray(validBuildModeOptions, o.BuildMode)
+ if !valid {
+ return fmt.Errorf(`invalid buildmode option '%s': valid values are %s`,
+ o.BuildMode,
+ strings.Join(validBuildModeOptions, ", "))
+ }
+ }
if o.GC != "" {
valid := isInArray(validGCOptions, o.GC)
if !valid {
diff --git a/compileopts/target.go b/compileopts/target.go
index 41a7babd9..ab9f871f3 100644
--- a/compileopts/target.go
+++ b/compileopts/target.go
@@ -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)
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 {
diff --git a/go.mod b/go.mod
index bf85ef3ad..a4de14136 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-tty v0.0.4
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
+ github.com/tetratelabs/wazero v1.6.0
go.bug.st/serial v1.6.0
golang.org/x/net v0.26.0
golang.org/x/sys v0.21.0
diff --git a/go.sum b/go.sum
index 7a6d1f4a9..d3c1bd310 100644
--- a/go.sum
+++ b/go.sum
@@ -12,7 +12,6 @@ github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moA
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
@@ -45,19 +44,18 @@ github.com/mattn/go-tty v0.0.4/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3px
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5 h1:1SoBaSPudixRecmlHXb/GxmaD3fLMtHIDN13QujwQuc=
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g=
+github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
go.bug.st/serial v1.6.0 h1:mAbRGN4cKE2J5gMwsMHC2KQisdLRQssO9WSM+rbZJ8A=
go.bug.st/serial v1.6.0/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
-golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -76,6 +74,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tinygo.org/x/go-llvm v0.0.0-20240627184919-3b50c76783a8 h1:bLsZXRUBavt++CJlMN7sppNziqu3LyamESLhFJcpqFQ=
tinygo.org/x/go-llvm v0.0.0-20240627184919-3b50c76783a8/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0=
diff --git a/main.go b/main.go
index 6f257d1b2..fe8a3fb15 100644
--- a/main.go
+++ b/main.go
@@ -1500,6 +1500,7 @@ func main() {
var tags buildutil.TagsFlag
flag.Var(&tags, "tags", "a space-separated list of extra build tags")
target := flag.String("target", "", "chip/board name or JSON target specification file")
+ buildMode := flag.String("buildmode", "", "build mode to use (default, c-shared)")
var stackSize uint64
flag.Func("stack-size", "goroutine stack size (if unknown at compile time)", func(s string) error {
size, err := bytesize.Parse(s)
@@ -1608,6 +1609,7 @@ func main() {
GOARM: goenv.Get("GOARM"),
GOMIPS: goenv.Get("GOMIPS"),
Target: *target,
+ BuildMode: *buildMode,
StackSize: stackSize,
Opt: *opt,
GC: *gc,
diff --git a/main_test.go b/main_test.go
index c131070c8..3bbb31da3 100644
--- a/main_test.go
+++ b/main_test.go
@@ -6,6 +6,7 @@ package main
import (
"bufio"
"bytes"
+ "context"
"errors"
"flag"
"io"
@@ -21,6 +22,9 @@ import (
"time"
"github.com/aykevl/go-wasm"
+ "github.com/tetratelabs/wazero"
+ "github.com/tetratelabs/wazero/api"
+ "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
"github.com/tinygo-org/tinygo/builder"
"github.com/tinygo-org/tinygo/compileopts"
"github.com/tinygo-org/tinygo/diagnostics"
@@ -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()
diff --git a/src/runtime/runtime_tinygowasm.go b/src/runtime/runtime_tinygowasm.go
index 744e138af..f791ffacd 100644
--- a/src/runtime/runtime_tinygowasm.go
+++ b/src/runtime/runtime_tinygowasm.go
@@ -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 (
diff --git a/src/runtime/runtime_wasip1.go b/src/runtime/runtime_wasip1.go
index 4a1afb2ab..ad66b0d86 100644
--- a/src/runtime/runtime_wasip1.go
+++ b/src/runtime/runtime_wasip1.go
@@ -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
diff --git a/src/runtime/runtime_wasm_js_scheduler.go b/src/runtime/runtime_wasm_js_scheduler.go
index 0edde29eb..94018336e 100644
--- a/src/runtime/runtime_wasm_js_scheduler.go
+++ b/src/runtime/runtime_wasm_js_scheduler.go
@@ -14,7 +14,7 @@ func resume() {
}
wasmNested = true
- scheduler()
+ scheduler(false)
wasmNested = false
}
@@ -26,6 +26,6 @@ func go_scheduler() {
}
wasmNested = true
- scheduler()
+ scheduler(false)
wasmNested = false
}
diff --git a/src/runtime/runtime_wasm_unknown.go b/src/runtime/runtime_wasm_unknown.go
index d307a4f87..846b95d2a 100644
--- a/src/runtime/runtime_wasm_unknown.go
+++ b/src/runtime/runtime_wasm_unknown.go
@@ -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() {
+}
diff --git a/src/runtime/runtime_wasmentry.go b/src/runtime/runtime_wasmentry.go
new file mode 100644
index 000000000..5a135f428
--- /dev/null
+++ b/src/runtime/runtime_wasmentry.go
@@ -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.
+}
diff --git a/src/runtime/scheduler.go b/src/runtime/scheduler.go
index 30b2da8a6..2f2287652 100644
--- a/src/runtime/scheduler.go
+++ b/src/runtime/scheduler.go
@@ -157,7 +157,13 @@ func removeTimer(tim *timer) bool {
}
// Run the scheduler until all tasks have finished.
-func scheduler() {
+// There are a few special cases:
+// - When returnAtDeadlock is true, it also returns when there are no more
+// runnable goroutines.
+// - When using the asyncify scheduler, it returns when it has to wait
+// (JavaScript uses setTimeout so the scheduler must return to the JS
+// environment).
+func scheduler(returnAtDeadlock bool) {
// Main scheduler loop.
var now timeUnit
for !schedulerDone {
@@ -193,6 +199,9 @@ func scheduler() {
t := runqueue.Pop()
if t == nil {
if sleepQueue == nil && timerQueue == nil {
+ if returnAtDeadlock {
+ return
+ }
if asyncScheduler {
// JavaScript is treated specially, see below.
return
diff --git a/src/runtime/scheduler_any.go b/src/runtime/scheduler_any.go
index 0911a2dc7..5e969f84f 100644
--- a/src/runtime/scheduler_any.go
+++ b/src/runtime/scheduler_any.go
@@ -25,7 +25,7 @@ func run() {
callMain()
schedulerDone = true
}()
- scheduler()
+ scheduler(false)
}
const hasScheduler = true
diff --git a/targets/wasm-unknown.json b/targets/wasm-unknown.json
index 93a4d391e..92c166ee5 100644
--- a/targets/wasm-unknown.json
+++ b/targets/wasm-unknown.json
@@ -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",
diff --git a/testdata/wasmexport-noscheduler.go b/testdata/wasmexport-noscheduler.go
new file mode 100644
index 000000000..844c8d158
--- /dev/null
+++ b/testdata/wasmexport-noscheduler.go
@@ -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
+}
diff --git a/testdata/wasmexport.go b/testdata/wasmexport.go
new file mode 100644
index 000000000..9065d6e92
--- /dev/null
+++ b/testdata/wasmexport.go
@@ -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
+}
diff --git a/testdata/wasmexport.txt b/testdata/wasmexport.txt
new file mode 100644
index 000000000..484a0ce8d
--- /dev/null
+++ b/testdata/wasmexport.txt
@@ -0,0 +1,11 @@
+called init
+hello!
+called add: 3 5
+called add: 7 9
+called add: 6 1
+reentrantCall: 2 3
+called add: 2 3
+reentrantCall result: 5
+reentrantCall: 1 8
+called add: 1 8
+reentrantCall result: 9