From 641dcd7c16d60a53faf8b61b509057830441a765 Mon Sep 17 00:00:00 2001 From: Nia Waldvogel Date: Wed, 21 Jul 2021 14:30:01 -0400 Subject: [PATCH] internal/task: use asyncify on webassembly This change implements a new "scheduler" for WebAssembly using binaryen's asyncify transform. This is more reliable than the current "coroutines" transform, and works with non-Go code in the call stack. runtime (js/wasm): handle scheduler nesting If WASM calls into JS which calls back into WASM, it is possible for the scheduler to nest. The event from the callback must be handled immediately, so the task cannot simply be deferred to the outer scheduler. This creates a minimal scheduler loop which is used to handle such nesting. --- .circleci/config.yml | 94 ++++++-- .github/workflows/windows.yml | 15 +- .gitignore | 1 - .gitmodules | 3 + Dockerfile | 2 +- Makefile | 15 +- build/.gitignore | 2 + builder/build.go | 33 +++ compileopts/config.go | 2 +- compileopts/options.go | 2 +- compileopts/options_test.go | 2 +- compileopts/target.go | 5 + compiler/compiler_test.go | 48 ++-- compiler/goroutine.go | 34 ++- compiler/testdata/channel.ll | 3 +- .../testdata/{func.ll => func-coroutines.ll} | 0 compiler/testdata/gc.ll | 7 +- ...mu.ll => goroutine-cortex-m-qemu-tasks.ll} | 0 compiler/testdata/goroutine-wasm-asyncify.ll | 210 ++++++++++++++++++ ...e-wasm.ll => goroutine-wasm-coroutines.ll} | 0 goenv/goenv.go | 90 ++++++++ lib/binaryen | 1 + src/internal/task/task_asyncify.go | 127 +++++++++++ src/internal/task/task_asyncify_wasm.S | 99 +++++++++ src/runtime/gc_conservative.go | 1 + src/runtime/runtime_wasm_js.go | 22 ++ src/runtime/scheduler.go | 18 ++ targets/wasi.json | 2 + targets/wasm.json | 2 + tests/wasm/chan_test.go | 3 +- transform/optimizer.go | 4 +- 31 files changed, 785 insertions(+), 62 deletions(-) create mode 100644 build/.gitignore rename compiler/testdata/{func.ll => func-coroutines.ll} (100%) rename compiler/testdata/{goroutine-cortex-m-qemu.ll => goroutine-cortex-m-qemu-tasks.ll} (100%) create mode 100644 compiler/testdata/goroutine-wasm-asyncify.ll rename compiler/testdata/{goroutine-wasm.ll => goroutine-wasm-coroutines.ll} (100%) create mode 160000 lib/binaryen create mode 100644 src/internal/task/task_asyncify.go create mode 100644 src/internal/task/task_asyncify_wasm.S diff --git a/.circleci/config.yml b/.circleci/config.yml index de75739b..f3534edd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,7 +25,9 @@ commands: qemu-system-arm \ qemu-user \ gcc-avr \ - avr-libc + avr-libc \ + cmake \ + ninja-build install-node: steps: - run: @@ -49,6 +51,13 @@ commands: command: | curl https://wasmtime.dev/install.sh -sSf | bash sudo ln -s ~/.wasmtime/bin/wasmtime /usr/local/bin/wasmtime + install-cmake: + steps: + - run: + name: "Install CMake" + command: | + wget https://github.com/Kitware/CMake/releases/download/v3.21.4/cmake-3.21.4-linux-x86_64.tar.gz + sudo tar --strip-components=1 -C /usr/local -xf cmake-3.21.4-linux-x86_64.tar.gz install-xtensa-toolchain: parameters: variant: @@ -76,6 +85,39 @@ commands: - llvm-project/clang/include - llvm-project/lld/include - llvm-project/llvm/include + hack-ninja-jobs: + steps: + - run: + name: "Hack Ninja to use less jobs" + command: | + echo -e '#!/bin/sh\n/usr/bin/ninja -j3 "$@"' > /go/bin/ninja + chmod +x /go/bin/ninja + build-binaryen-linux: + steps: + - restore_cache: + keys: + - binaryen-linux-v1 + - run: + name: "Build Binaryen" + command: | + make binaryen + - save_cache: + key: binaryen-linux-v1 + paths: + - build/wasm-opt + build-binaryen-linux-stretch: + steps: + - restore_cache: + keys: + - binaryen-linux-stretch-v1 + - run: + name: "Build Binaryen" + command: | + CC=$PWD/llvm-build/bin/clang make binaryen + - save_cache: + key: binaryen-linux-stretch-v1 + paths: + - build/wasm-opt build-wasi-libc: steps: - restore_cache: @@ -100,6 +142,8 @@ commands: - install-node - install-chrome - install-wasmtime + - hack-ninja-jobs + - build-binaryen-linux - restore_cache: keys: - go-cache-v2-{{ checksum "go.mod" }}-{{ .Environment.CIRCLE_PREVIOUS_BUILD_NUM }} @@ -141,9 +185,13 @@ commands: qemu-system-arm \ qemu-user \ gcc-avr \ - avr-libc + avr-libc \ + ninja-build \ + python3 - install-node - install-wasmtime + - install-cmake + - hack-ninja-jobs - install-xtensa-toolchain: variant: "linux-amd64" - restore_cache: @@ -159,14 +207,9 @@ commands: command: | if [ ! -f llvm-build/lib/liblldELF.a ] then - # fetch LLVM source + # fetch LLVM source (may only have headers right now) rm -rf llvm-project make llvm-source - # install dependencies - sudo apt-get install cmake ninja-build - # hack ninja to use less jobs - echo -e '#!/bin/sh\n/usr/bin/ninja -j3 "$@"' > /go/bin/ninja - chmod +x /go/bin/ninja # build! make ASSERT=1 llvm-build find llvm-build -name CMakeFiles -prune -exec rm -r '{}' \; @@ -175,6 +218,7 @@ commands: key: llvm-build-11-linux-v4-assert paths: llvm-build + - build-binaryen-linux-stretch - run: make ASSERT=1 - build-wasi-libc - run: @@ -206,9 +250,13 @@ commands: qemu-system-arm \ qemu-user \ gcc-avr \ - avr-libc + avr-libc \ + ninja-build \ + python3 - install-node - install-wasmtime + - install-cmake + - hack-ninja-jobs - install-xtensa-toolchain: variant: "linux-amd64" - restore_cache: @@ -224,14 +272,9 @@ commands: command: | if [ ! -f llvm-build/lib/liblldELF.a ] then - # fetch LLVM source + # fetch LLVM source (may only have headers right now) rm -rf llvm-project make llvm-source - # install dependencies - sudo apt-get install cmake ninja-build - # hack ninja to use less jobs - echo -e '#!/bin/sh\n/usr/bin/ninja -j3 "$@"' > /go/bin/ninja - chmod +x /go/bin/ninja # build! make llvm-build find llvm-build -name CMakeFiles -prune -exec rm -r '{}' \; @@ -240,6 +283,7 @@ commands: key: llvm-build-11-linux-v4-noassert paths: llvm-build + - build-binaryen-linux-stretch - build-wasi-libc - run: name: "Test TinyGo" @@ -283,7 +327,7 @@ commands: curl https://dl.google.com/go/go1.17.darwin-amd64.tar.gz -o go1.17.darwin-amd64.tar.gz sudo tar -C /usr/local -xzf go1.17.darwin-amd64.tar.gz ln -s /usr/local/go/bin/go /usr/local/bin/go - HOMEBREW_NO_AUTO_UPDATE=1 brew install qemu + HOMEBREW_NO_AUTO_UPDATE=1 brew install qemu cmake ninja - install-xtensa-toolchain: variant: "macos" - restore_cache: @@ -311,11 +355,9 @@ commands: command: | if [ ! -f llvm-build/lib/liblldELF.a ] then - # fetch LLVM source + # fetch LLVM source (may only have headers right now) rm -rf llvm-project make llvm-source - # install dependencies - HOMEBREW_NO_AUTO_UPDATE=1 brew install cmake ninja # build! make llvm-build find llvm-build -name CMakeFiles -prune -exec rm -r '{}' \; @@ -324,6 +366,20 @@ commands: key: llvm-build-11-macos-v5 paths: llvm-build + - restore_cache: + keys: + - binaryen-macos-v1 + - run: + name: "Build Binaryen" + command: | + if [ ! -f build/wasm-opt ] + then + make binaryen + fi + - save_cache: + key: binaryen-macos-v1 + paths: + - build/wasm-opt - restore_cache: keys: - wasi-libc-sysroot-macos-v4 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index bdb27dca..4c1f8dd9 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -20,6 +20,10 @@ jobs: run: | choco install qemu --version=2020.06.12 echo "C:\Program Files\QEMU" >> $GITHUB_PATH + - name: Install Ninja + shell: bash + run: | + choco install ninja - name: Checkout uses: actions/checkout@v2 with: @@ -50,8 +54,6 @@ jobs: # fetch LLVM source rm -rf llvm-project make llvm-source - # install dependencies - choco install ninja # build! make llvm-build # Remove unnecessary object files (to reduce cache size). @@ -65,6 +67,15 @@ jobs: - name: Build wasi-libc if: steps.cache-wasi-libc.outputs.cache-hit != 'true' run: make wasi-libc + - name: Cache Binaryen + uses: actions/cache@v2 + id: cache-binaryen + with: + key: binaryen-v1 + path: build/binaryen + - name: Build Binaryen + if: steps.cache-binaryen.outputs.cache-hit != 'true' + run: make binaryen - name: Test TinyGo shell: bash run: make test diff --git a/.gitignore b/.gitignore index 483c789c..a5603ec7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -build docs/_build src/device/avr/*.go src/device/avr/*.ld diff --git a/.gitmodules b/.gitmodules index 4df14b5e..c126bd4b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -26,3 +26,6 @@ [submodule "lib/musl"] path = lib/musl url = git://git.musl-libc.org/musl +[submodule "lib/binaryen"] + path = lib/binaryen + url = https://github.com/WebAssembly/binaryen.git diff --git a/Dockerfile b/Dockerfile index f1b04d2f..f6c72122 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,7 @@ COPY --from=tinygo-base /tinygo/targets /tinygo/targets RUN cd /tinygo/ && \ apt-get update && \ apt-get install -y make clang-11 libllvm11 lld-11 && \ - make wasi-libc + make wasi-libc binaryen # tinygo-avr stage installs the needed dependencies to compile TinyGo programs for AVR microcontrollers. FROM tinygo-base AS tinygo-avr diff --git a/Makefile b/Makefile index 68b66c10..aaabc2e8 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,8 @@ ifeq ($(OS),Windows_NT) CGO_LDFLAGS += -static -static-libgcc -static-libstdc++ CGO_LDFLAGS_EXTRA += -lversion + BINARYEN_OPTION += -DCMAKE_EXE_LINKER_FLAGS='-static-libgcc -static-libstdc++' + LIBCLANG_NAME = libclang else ifeq ($(shell uname -s),Darwin) @@ -163,12 +165,18 @@ llvm-source: $(LLVM_PROJECTDIR)/llvm # Configure LLVM. TINYGO_SOURCE_DIR=$(shell pwd) $(LLVM_BUILDDIR)/build.ninja: llvm-source - mkdir -p $(LLVM_BUILDDIR); cd $(LLVM_BUILDDIR); cmake -G Ninja $(TINYGO_SOURCE_DIR)/$(LLVM_PROJECTDIR)/llvm "-DLLVM_TARGETS_TO_BUILD=X86;ARM;AArch64;RISCV;WebAssembly" "-DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=AVR;Xtensa" -DCMAKE_BUILD_TYPE=Release -DLIBCLANG_BUILD_STATIC=ON -DLLVM_ENABLE_TERMINFO=OFF -DLLVM_ENABLE_ZLIB=OFF -DLLVM_ENABLE_LIBEDIT=OFF -DLLVM_ENABLE_Z3_SOLVER=OFF -DLLVM_ENABLE_OCAMLDOC=OFF -DLLVM_ENABLE_LIBXML2=OFF -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_TOOL_CLANG_TOOLS_EXTRA_BUILD=OFF -DCLANG_ENABLE_STATIC_ANALYZER=OFF -DCLANG_ENABLE_ARCMT=OFF $(LLVM_OPTION) + mkdir -p $(LLVM_BUILDDIR) && cd $(LLVM_BUILDDIR) && cmake -G Ninja $(TINYGO_SOURCE_DIR)/$(LLVM_PROJECTDIR)/llvm "-DLLVM_TARGETS_TO_BUILD=X86;ARM;AArch64;RISCV;WebAssembly" "-DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=AVR;Xtensa" -DCMAKE_BUILD_TYPE=Release -DLIBCLANG_BUILD_STATIC=ON -DLLVM_ENABLE_TERMINFO=OFF -DLLVM_ENABLE_ZLIB=OFF -DLLVM_ENABLE_LIBEDIT=OFF -DLLVM_ENABLE_Z3_SOLVER=OFF -DLLVM_ENABLE_OCAMLDOC=OFF -DLLVM_ENABLE_LIBXML2=OFF -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_TOOL_CLANG_TOOLS_EXTRA_BUILD=OFF -DCLANG_ENABLE_STATIC_ANALYZER=OFF -DCLANG_ENABLE_ARCMT=OFF $(LLVM_OPTION) # Build LLVM. $(LLVM_BUILDDIR): $(LLVM_BUILDDIR)/build.ninja - cd $(LLVM_BUILDDIR); ninja $(NINJA_BUILD_TARGETS) + cd $(LLVM_BUILDDIR) && ninja $(NINJA_BUILD_TARGETS) +# Build Binaryen +.PHONY: binaryen +binaryen: build/wasm-opt +build/wasm-opt: + cd lib/binaryen && cmake -G Ninja . -DBUILD_STATIC_LIB=ON $(BINARYEN_OPTION) && ninja + cp lib/binaryen/bin/wasm-opt build/wasm-opt # Build wasi-libc sysroot .PHONY: wasi-libc @@ -476,7 +484,7 @@ endif wasmtest: $(GO) test ./tests/wasm -build/release: tinygo gen-device wasi-libc +build/release: tinygo gen-device wasi-libc binaryen @mkdir -p build/release/tinygo/bin @mkdir -p build/release/tinygo/lib/clang/include @mkdir -p build/release/tinygo/lib/CMSIS/CMSIS @@ -493,6 +501,7 @@ build/release: tinygo gen-device wasi-libc @mkdir -p build/release/tinygo/pkg/armv7em-unknown-unknown-eabi @echo copying source files @cp -p build/tinygo$(EXE) build/release/tinygo/bin + @cp -p build/wasm-opt$(EXE) build/release/tinygo/bin @cp -p $(abspath $(CLANG_SRC))/lib/Headers/*.h build/release/tinygo/lib/clang/include @cp -rp lib/CMSIS/CMSIS/Include build/release/tinygo/lib/CMSIS/CMSIS @cp -rp lib/CMSIS/README.md build/release/tinygo/lib/CMSIS diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/builder/build.go b/builder/build.go index 5c3150c1..25271496 100644 --- a/builder/build.go +++ b/builder/build.go @@ -16,9 +16,11 @@ import ( "io/ioutil" "math/bits" "os" + "os/exec" "path/filepath" "runtime" "sort" + "strconv" "strings" "github.com/tinygo-org/tinygo/cgo" @@ -658,6 +660,37 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil } } + // Run wasm-opt if necessary. + if config.Scheduler() == "asyncify" { + var optLevel, shrinkLevel int + switch config.Options.Opt { + case "none", "0": + case "1": + optLevel = 1 + case "2": + optLevel = 2 + case "s": + optLevel = 2 + shrinkLevel = 1 + case "z": + optLevel = 2 + shrinkLevel = 2 + default: + return fmt.Errorf("unknown opt level: %q", config.Options.Opt) + } + cmd := exec.Command(goenv.Get("WASMOPT"), "--asyncify", "-g", + "--optimize-level", strconv.Itoa(optLevel), + "--shrink-level", strconv.Itoa(shrinkLevel), + executable, "--output", executable) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + return fmt.Errorf("wasm-opt failed: %w", err) + } + } + // Print code size if requested. if config.Options.PrintSizes == "short" || config.Options.PrintSizes == "full" { packagePathMap := make(map[string]string, len(lprogram.Packages)) diff --git a/compileopts/config.go b/compileopts/config.go index 0ee4d1d0..2d68b60b 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -157,7 +157,7 @@ func (c *Config) OptLevels() (optLevel, sizeLevel int, inlinerThreshold uint) { // target. func (c *Config) FuncImplementation() string { switch c.Scheduler() { - case "tasks": + case "tasks", "asyncify": // A func value is implemented as a pair of pointers: // {context, function pointer} // where the context may be a pointer to a heap-allocated struct diff --git a/compileopts/options.go b/compileopts/options.go index fd041645..065eb97a 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -8,7 +8,7 @@ import ( var ( validGCOptions = []string{"none", "leaking", "extalloc", "conservative"} - validSchedulerOptions = []string{"none", "tasks", "coroutines"} + validSchedulerOptions = []string{"none", "tasks", "coroutines", "asyncify"} validSerialOptions = []string{"none", "uart", "usb"} validPrintSizeOptions = []string{"none", "short", "full"} validPanicStrategyOptions = []string{"print", "trap"} diff --git a/compileopts/options_test.go b/compileopts/options_test.go index 1ff532cc..64655821 100644 --- a/compileopts/options_test.go +++ b/compileopts/options_test.go @@ -10,7 +10,7 @@ import ( func TestVerifyOptions(t *testing.T) { expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, extalloc, conservative`) - expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, coroutines`) + expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, coroutines, asyncify`) expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full`) expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`) diff --git a/compileopts/target.go b/compileopts/target.go index f16df32c..0c8ce1d9 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -208,6 +208,11 @@ func LoadTarget(options *Options) (*TargetSpec, error) { if err != nil { return nil, err } + + if spec.Scheduler == "asyncify" { + spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_asyncify_wasm.S") + } + return spec, nil } diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index f1a0e00c..cbf89fe2 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -18,8 +18,9 @@ import ( var flagUpdate = flag.Bool("update", false, "update tests based on test output") type testCase struct { - file string - target string + file string + target string + scheduler string } // Basic tests for the compiler. Build some Go files and compare the output with @@ -41,20 +42,21 @@ func TestCompiler(t *testing.T) { } tests := []testCase{ - {"basic.go", ""}, - {"pointer.go", ""}, - {"slice.go", ""}, - {"string.go", ""}, - {"float.go", ""}, - {"interface.go", ""}, - {"func.go", ""}, - {"pragma.go", ""}, - {"goroutine.go", "wasm"}, - {"goroutine.go", "cortex-m-qemu"}, - {"channel.go", ""}, - {"intrinsics.go", "cortex-m-qemu"}, - {"intrinsics.go", "wasm"}, - {"gc.go", ""}, + {"basic.go", "", ""}, + {"pointer.go", "", ""}, + {"slice.go", "", ""}, + {"string.go", "", ""}, + {"float.go", "", ""}, + {"interface.go", "", ""}, + {"func.go", "", "coroutines"}, + {"pragma.go", "", ""}, + {"goroutine.go", "wasm", "asyncify"}, + {"goroutine.go", "wasm", "coroutines"}, + {"goroutine.go", "cortex-m-qemu", "tasks"}, + {"channel.go", "", ""}, + {"intrinsics.go", "cortex-m-qemu", ""}, + {"intrinsics.go", "wasm", ""}, + {"gc.go", "", ""}, } _, minor, err := goenv.GetGorootVersion(goenv.Get("GOROOT")) @@ -62,7 +64,7 @@ func TestCompiler(t *testing.T) { t.Fatal("could not read Go version:", err) } if minor >= 17 { - tests = append(tests, testCase{"go1.17.go", ""}) + tests = append(tests, testCase{"go1.17.go", "", ""}) } for _, tc := range tests { @@ -70,7 +72,10 @@ func TestCompiler(t *testing.T) { targetString := "wasm" if tc.target != "" { targetString = tc.target - name = tc.file + "-" + tc.target + name += "-" + tc.target + } + if tc.scheduler != "" { + name += "-" + tc.scheduler } t.Run(name, func(t *testing.T) { @@ -81,6 +86,9 @@ func TestCompiler(t *testing.T) { if err != nil { t.Fatal("failed to load target:", err) } + if tc.scheduler != "" { + options.Scheduler = tc.scheduler + } config := &compileopts.Config{ Options: options, Target: target, @@ -94,6 +102,7 @@ func TestCompiler(t *testing.T) { Scheduler: config.Scheduler(), FuncImplementation: config.FuncImplementation(), AutomaticStackSize: config.AutomaticStackSize(), + DefaultStackSize: config.Target.DefaultStackSize, } machine, err := NewTargetMachine(compilerConfig) if err != nil { @@ -142,6 +151,9 @@ func TestCompiler(t *testing.T) { if tc.target != "" { outFilePrefix += "-" + tc.target } + if tc.scheduler != "" { + outFilePrefix += "-" + tc.scheduler + } outPath := "./testdata/" + outFilePrefix + ".ll" // Update test if needed. Do not check the result. diff --git a/compiler/goroutine.go b/compiler/goroutine.go index b5848192..a77a9b86 100644 --- a/compiler/goroutine.go +++ b/compiler/goroutine.go @@ -100,7 +100,7 @@ func (b *builder) createGo(instr *ssa.Go) { switch b.Scheduler { case "none", "coroutines": // There are no additional parameters needed for the goroutine start operation. - case "tasks": + case "tasks", "asyncify": // Add the function pointer as a parameter to start the goroutine. params = append(params, funcPtr) default: @@ -112,7 +112,7 @@ func (b *builder) createGo(instr *ssa.Go) { paramBundle := b.emitPointerPack(params) var callee, stackSize llvm.Value switch b.Scheduler { - case "none", "tasks": + case "none", "tasks", "asyncify": callee = b.createGoroutineStartWrapper(funcPtr, prefix, hasContext, instr.Pos()) if b.AutomaticStackSize { // The stack size is not known until after linking. Call a dummy @@ -124,7 +124,7 @@ func (b *builder) createGo(instr *ssa.Go) { } else { // The stack size is fixed at compile time. By emitting it here as a // constant, it can be optimized. - if b.Scheduler == "tasks" && b.DefaultStackSize == 0 { + if (b.Scheduler == "tasks" || b.Scheduler == "asyncify") && b.DefaultStackSize == 0 { b.addError(instr.Pos(), "default stack size for goroutines is not set") } stackSize = llvm.ConstInt(b.uintptrType, b.DefaultStackSize, false) @@ -170,6 +170,11 @@ func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix stri builder := c.ctx.NewBuilder() defer builder.Dispose() + var deadlock llvm.Value + if c.Scheduler == "asyncify" { + deadlock = c.getFunction(c.program.ImportedPackage("runtime").Members["deadlock"].(*ssa.Function)) + } + if !fn.IsAFunction().IsNil() { // See whether this wrapper has already been created. If so, return it. name := fn.Name() @@ -225,6 +230,12 @@ func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix stri // Create the call. builder.CreateCall(fn, params, "") + if c.Scheduler == "asyncify" { + builder.CreateCall(deadlock, []llvm.Value{ + llvm.Undef(c.i8ptrType), llvm.Undef(c.i8ptrType), + }, "") + } + } else { // For a function pointer like this: // @@ -292,11 +303,22 @@ func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix stri // Create the call. builder.CreateCall(fnPtr, params, "") + + if c.Scheduler == "asyncify" { + builder.CreateCall(deadlock, []llvm.Value{ + llvm.Undef(c.i8ptrType), llvm.Undef(c.i8ptrType), + }, "") + } } - // Finish the function. Every basic block must end in a terminator, and - // because goroutines never return a value we can simply return void. - builder.CreateRetVoid() + if c.Scheduler == "asyncify" { + // The goroutine was terminated via deadlock. + builder.CreateUnreachable() + } else { + // Finish the function. Every basic block must end in a terminator, and + // because goroutines never return a value we can simply return void. + builder.CreateRetVoid() + } // Return a ptrtoint of the wrapper, not the function itself. return builder.CreatePtrToInt(wrapper, c.uintptrType, "") diff --git a/compiler/testdata/channel.ll b/compiler/testdata/channel.ll index 2d4f1475..04bfa4af 100644 --- a/compiler/testdata/channel.ll +++ b/compiler/testdata/channel.ll @@ -6,7 +6,8 @@ target triple = "wasm32-unknown-wasi" %runtime.channel = type { i32, i32, i8, %runtime.channelBlockedList*, i32, i32, i32, i8* } %runtime.channelBlockedList = type { %runtime.channelBlockedList*, %"internal/task.Task"*, %runtime.chanSelectState*, { %runtime.channelBlockedList*, i32, i32 } } %"internal/task.Task" = type { %"internal/task.Task"*, i8*, i64, %"internal/task.state" } -%"internal/task.state" = type { i8* } +%"internal/task.state" = type { i32, i8*, %"internal/task.stackState", i1 } +%"internal/task.stackState" = type { i32, i32 } %runtime.chanSelectState = type { %runtime.channel*, i8* } declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*, i8*) diff --git a/compiler/testdata/func.ll b/compiler/testdata/func-coroutines.ll similarity index 100% rename from compiler/testdata/func.ll rename to compiler/testdata/func-coroutines.ll diff --git a/compiler/testdata/gc.ll b/compiler/testdata/gc.ll index 7ac0aa1b..375763f3 100644 --- a/compiler/testdata/gc.ll +++ b/compiler/testdata/gc.ll @@ -5,7 +5,6 @@ target triple = "wasm32-unknown-wasi" %runtime.typecodeID = type { %runtime.typecodeID*, i32, %runtime.interfaceMethodInfo*, %runtime.typecodeID*, i32 } %runtime.interfaceMethodInfo = type { i8*, i32 } -%runtime.funcValue = type { i8*, i32 } %runtime._interface = type { i32, i8* } @main.scalar1 = hidden global i8* null, align 4 @@ -72,11 +71,11 @@ entry: } ; Function Attrs: nounwind -define hidden %runtime.funcValue* @main.newFuncValue(i8* %context, i8* %parentHandle) unnamed_addr #0 { +define hidden { i8*, void ()* }* @main.newFuncValue(i8* %context, i8* %parentHandle) unnamed_addr #0 { entry: %new = call i8* @runtime.alloc(i32 8, i8* nonnull inttoptr (i32 197 to i8*), i8* undef, i8* null) #0 - %0 = bitcast i8* %new to %runtime.funcValue* - ret %runtime.funcValue* %0 + %0 = bitcast i8* %new to { i8*, void ()* }* + ret { i8*, void ()* }* %0 } ; Function Attrs: nounwind diff --git a/compiler/testdata/goroutine-cortex-m-qemu.ll b/compiler/testdata/goroutine-cortex-m-qemu-tasks.ll similarity index 100% rename from compiler/testdata/goroutine-cortex-m-qemu.ll rename to compiler/testdata/goroutine-cortex-m-qemu-tasks.ll diff --git a/compiler/testdata/goroutine-wasm-asyncify.ll b/compiler/testdata/goroutine-wasm-asyncify.ll new file mode 100644 index 00000000..1ecf0c79 --- /dev/null +++ b/compiler/testdata/goroutine-wasm-asyncify.ll @@ -0,0 +1,210 @@ +; ModuleID = 'goroutine.go' +source_filename = "goroutine.go" +target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" +target triple = "wasm32-unknown-wasi" + +%runtime.channel = type { i32, i32, i8, %runtime.channelBlockedList*, i32, i32, i32, i8* } +%runtime.channelBlockedList = type { %runtime.channelBlockedList*, %"internal/task.Task"*, %runtime.chanSelectState*, { %runtime.channelBlockedList*, i32, i32 } } +%"internal/task.Task" = type { %"internal/task.Task"*, i8*, i64, %"internal/task.state" } +%"internal/task.state" = type { i32, i8*, %"internal/task.stackState", i1 } +%"internal/task.stackState" = type { i32, i32 } +%runtime.chanSelectState = type { %runtime.channel*, i8* } + +@"main$string" = internal unnamed_addr constant [4 x i8] c"test", align 1 + +declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*, i8*) + +; Function Attrs: nounwind +define hidden void @main.init(i8* %context, i8* %parentHandle) unnamed_addr #0 { +entry: + ret void +} + +; Function Attrs: nounwind +define hidden void @main.regularFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { +entry: + call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.regularFunction$gowrapper" to i32), i8* nonnull inttoptr (i32 5 to i8*), i32 8192, i8* undef, i8* null) #0 + ret void +} + +declare void @main.regularFunction(i32, i8*, i8*) + +declare void @runtime.deadlock(i8*, i8*) + +; Function Attrs: nounwind +define linkonce_odr void @"main.regularFunction$gowrapper"(i8* %0) unnamed_addr #1 { +entry: + %unpack.int = ptrtoint i8* %0 to i32 + call void @main.regularFunction(i32 %unpack.int, i8* undef, i8* undef) #0 + call void @runtime.deadlock(i8* undef, i8* undef) #0 + unreachable +} + +declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*) + +; Function Attrs: nounwind +define hidden void @main.inlineFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { +entry: + call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.inlineFunctionGoroutine$1$gowrapper" to i32), i8* nonnull inttoptr (i32 5 to i8*), i32 8192, i8* undef, i8* null) #0 + ret void +} + +; Function Attrs: nounwind +define hidden void @"main.inlineFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr #0 { +entry: + ret void +} + +; Function Attrs: nounwind +define linkonce_odr void @"main.inlineFunctionGoroutine$1$gowrapper"(i8* %0) unnamed_addr #2 { +entry: + %unpack.int = ptrtoint i8* %0 to i32 + call void @"main.inlineFunctionGoroutine$1"(i32 %unpack.int, i8* undef, i8* undef) + call void @runtime.deadlock(i8* undef, i8* undef) #0 + unreachable +} + +; Function Attrs: nounwind +define hidden void @main.closureFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { +entry: + %n = call i8* @runtime.alloc(i32 4, i8* nonnull inttoptr (i32 3 to i8*), i8* undef, i8* null) #0 + %0 = bitcast i8* %n to i32* + store i32 3, i32* %0, align 4 + %1 = call i8* @runtime.alloc(i32 8, i8* null, i8* undef, i8* null) #0 + %2 = bitcast i8* %1 to i32* + store i32 5, i32* %2, align 4 + %3 = getelementptr inbounds i8, i8* %1, i32 4 + %4 = bitcast i8* %3 to i8** + store i8* %n, i8** %4, align 4 + call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.closureFunctionGoroutine$1$gowrapper" to i32), i8* nonnull %1, i32 8192, i8* undef, i8* null) #0 + %5 = load i32, i32* %0, align 4 + call void @runtime.printint32(i32 %5, i8* undef, i8* null) #0 + ret void +} + +; Function Attrs: nounwind +define hidden void @"main.closureFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr #0 { +entry: + %unpack.ptr = bitcast i8* %context to i32* + store i32 7, i32* %unpack.ptr, align 4 + ret void +} + +; Function Attrs: nounwind +define linkonce_odr void @"main.closureFunctionGoroutine$1$gowrapper"(i8* %0) unnamed_addr #3 { +entry: + %1 = bitcast i8* %0 to i32* + %2 = load i32, i32* %1, align 4 + %3 = getelementptr inbounds i8, i8* %0, i32 4 + %4 = bitcast i8* %3 to i8** + %5 = load i8*, i8** %4, align 4 + call void @"main.closureFunctionGoroutine$1"(i32 %2, i8* %5, i8* undef) + call void @runtime.deadlock(i8* undef, i8* undef) #0 + unreachable +} + +declare void @runtime.printint32(i32, i8*, i8*) + +; Function Attrs: nounwind +define hidden void @main.funcGoroutine(i8* %fn.context, void ()* %fn.funcptr, i8* %context, i8* %parentHandle) unnamed_addr #0 { +entry: + %0 = call i8* @runtime.alloc(i32 12, i8* null, i8* undef, i8* null) #0 + %1 = bitcast i8* %0 to i32* + store i32 5, i32* %1, align 4 + %2 = getelementptr inbounds i8, i8* %0, i32 4 + %3 = bitcast i8* %2 to i8** + store i8* %fn.context, i8** %3, align 4 + %4 = getelementptr inbounds i8, i8* %0, i32 8 + %5 = bitcast i8* %4 to void ()** + store void ()* %fn.funcptr, void ()** %5, align 4 + call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @main.funcGoroutine.gowrapper to i32), i8* nonnull %0, i32 8192, i8* undef, i8* null) #0 + ret void +} + +; Function Attrs: nounwind +define linkonce_odr void @main.funcGoroutine.gowrapper(i8* %0) unnamed_addr #4 { +entry: + %1 = bitcast i8* %0 to i32* + %2 = load i32, i32* %1, align 4 + %3 = getelementptr inbounds i8, i8* %0, i32 4 + %4 = bitcast i8* %3 to i8** + %5 = load i8*, i8** %4, align 4 + %6 = getelementptr inbounds i8, i8* %0, i32 8 + %7 = bitcast i8* %6 to void (i32, i8*, i8*)** + %8 = load void (i32, i8*, i8*)*, void (i32, i8*, i8*)** %7, align 4 + call void %8(i32 %2, i8* %5, i8* undef) #0 + call void @runtime.deadlock(i8* undef, i8* undef) #0 + unreachable +} + +; Function Attrs: nounwind +define hidden void @main.recoverBuiltinGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { +entry: + ret void +} + +; Function Attrs: nounwind +define hidden void @main.copyBuiltinGoroutine(i8* %dst.data, i32 %dst.len, i32 %dst.cap, i8* %src.data, i32 %src.len, i32 %src.cap, i8* %context, i8* %parentHandle) unnamed_addr #0 { +entry: + %copy.n = call i32 @runtime.sliceCopy(i8* %dst.data, i8* %src.data, i32 %dst.len, i32 %src.len, i32 1, i8* undef, i8* null) #0 + ret void +} + +declare i32 @runtime.sliceCopy(i8* nocapture writeonly, i8* nocapture readonly, i32, i32, i32, i8*, i8*) + +; Function Attrs: nounwind +define hidden void @main.closeBuiltinGoroutine(%runtime.channel* dereferenceable_or_null(32) %ch, i8* %context, i8* %parentHandle) unnamed_addr #0 { +entry: + call void @runtime.chanClose(%runtime.channel* %ch, i8* undef, i8* null) #0 + ret void +} + +declare void @runtime.chanClose(%runtime.channel* dereferenceable_or_null(32), i8*, i8*) + +; Function Attrs: nounwind +define hidden void @main.startInterfaceMethod(i32 %itf.typecode, i8* %itf.value, i8* %context, i8* %parentHandle) unnamed_addr #0 { +entry: + %0 = call i8* @runtime.alloc(i32 16, i8* null, i8* undef, i8* null) #0 + %1 = bitcast i8* %0 to i8** + store i8* %itf.value, i8** %1, align 4 + %2 = getelementptr inbounds i8, i8* %0, i32 4 + %.repack = bitcast i8* %2 to i8** + store i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"main$string", i32 0, i32 0), i8** %.repack, align 4 + %.repack1 = getelementptr inbounds i8, i8* %0, i32 8 + %3 = bitcast i8* %.repack1 to i32* + store i32 4, i32* %3, align 4 + %4 = getelementptr inbounds i8, i8* %0, i32 12 + %5 = bitcast i8* %4 to i32* + store i32 %itf.typecode, i32* %5, align 4 + call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"interface:{Print:func:{basic:string}{}}.Print$invoke$gowrapper" to i32), i8* nonnull %0, i32 8192, i8* undef, i8* null) #0 + ret void +} + +declare void @"interface:{Print:func:{basic:string}{}}.Print$invoke"(i8*, i8*, i32, i32, i8*, i8*) #5 + +; Function Attrs: nounwind +define linkonce_odr void @"interface:{Print:func:{basic:string}{}}.Print$invoke$gowrapper"(i8* %0) unnamed_addr #6 { +entry: + %1 = bitcast i8* %0 to i8** + %2 = load i8*, i8** %1, align 4 + %3 = getelementptr inbounds i8, i8* %0, i32 4 + %4 = bitcast i8* %3 to i8** + %5 = load i8*, i8** %4, align 4 + %6 = getelementptr inbounds i8, i8* %0, i32 8 + %7 = bitcast i8* %6 to i32* + %8 = load i32, i32* %7, align 4 + %9 = getelementptr inbounds i8, i8* %0, i32 12 + %10 = bitcast i8* %9 to i32* + %11 = load i32, i32* %10, align 4 + call void @"interface:{Print:func:{basic:string}{}}.Print$invoke"(i8* %2, i8* %5, i32 %8, i32 %11, i8* undef, i8* undef) #0 + call void @runtime.deadlock(i8* undef, i8* undef) #0 + unreachable +} + +attributes #0 = { nounwind } +attributes #1 = { nounwind "tinygo-gowrapper"="main.regularFunction" } +attributes #2 = { nounwind "tinygo-gowrapper"="main.inlineFunctionGoroutine$1" } +attributes #3 = { nounwind "tinygo-gowrapper"="main.closureFunctionGoroutine$1" } +attributes #4 = { nounwind "tinygo-gowrapper" } +attributes #5 = { "tinygo-invoke"="reflect/methods.Print(string)" "tinygo-methods"="reflect/methods.Print(string)" } +attributes #6 = { nounwind "tinygo-gowrapper"="interface:{Print:func:{basic:string}{}}.Print$invoke" } diff --git a/compiler/testdata/goroutine-wasm.ll b/compiler/testdata/goroutine-wasm-coroutines.ll similarity index 100% rename from compiler/testdata/goroutine-wasm.ll rename to compiler/testdata/goroutine-wasm-coroutines.ll diff --git a/goenv/goenv.go b/goenv/goenv.go index 3f1352a7..0ef18ba5 100644 --- a/goenv/goenv.go +++ b/goenv/goenv.go @@ -3,12 +3,15 @@ package goenv import ( + "bytes" + "errors" "fmt" "os" "os/exec" "os/user" "path/filepath" "runtime" + "strings" ) // Keys is a slice of all available environment variable keys. @@ -67,11 +70,98 @@ func Get(name string) string { return "1" case "TINYGOROOT": return sourceDir() + case "WASMOPT": + if path := os.Getenv("WASMOPT"); path != "" { + err := wasmOptCheckVersion(path) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot use %q as wasm-opt (from WASMOPT environment variable): %s", path, err.Error()) + os.Exit(1) + } + + return path + } + + return findWasmOpt() default: return "" } } +// Find wasm-opt, or exit with an error. +func findWasmOpt() string { + tinygoroot := sourceDir() + searchPaths := []string{ + tinygoroot + "/bin/wasm-opt", + tinygoroot + "/build/wasm-opt", + } + + var paths []string + for _, path := range searchPaths { + if runtime.GOOS == "windows" { + path += ".exe" + } + + _, err := os.Stat(path) + if err != nil && os.IsNotExist(err) { + continue + } + + paths = append(paths, path) + } + + if path, err := exec.LookPath("wasm-opt"); err == nil { + paths = append(paths, path) + } + + if len(paths) == 0 { + fmt.Fprintln(os.Stderr, "error: could not find wasm-opt, set the WASMOPT environment variable to override") + os.Exit(1) + } + + errs := make([]error, len(paths)) + for i, path := range paths { + err := wasmOptCheckVersion(path) + if err == nil { + return path + } + + errs[i] = err + } + fmt.Fprintln(os.Stderr, "no usable wasm-opt found, update or run \"make binaryen\"") + for i, path := range paths { + fmt.Fprintf(os.Stderr, "\t%s: %s\n", path, errs[i].Error()) + } + os.Exit(1) + panic("unreachable") +} + +// wasmOptCheckVersion checks if a copy of wasm-opt is usable. +func wasmOptCheckVersion(path string) error { + cmd := exec.Command(path, "--version") + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return err + } + + str := buf.String() + if strings.Contains(str, "(") { + // The git tag may be placed in parentheses after the main version string. + str = strings.Split(str, "(")[0] + } + + str = strings.TrimSpace(str) + var ver uint + _, err = fmt.Sscanf(str, "wasm-opt version %d", &ver) + if err != nil || ver < 102 { + return errors.New("incompatible wasm-opt (need 102 or newer)") + } + + return nil +} + // Return the TINYGOROOT, or exit with an error. func sourceDir() string { // Use $TINYGOROOT as root, if available. diff --git a/lib/binaryen b/lib/binaryen new file mode 160000 index 00000000..96f7acf0 --- /dev/null +++ b/lib/binaryen @@ -0,0 +1 @@ +Subproject commit 96f7acf09aae1ec6e8bc573dfa8f309c4f892a40 diff --git a/src/internal/task/task_asyncify.go b/src/internal/task/task_asyncify.go new file mode 100644 index 00000000..d67f0e1c --- /dev/null +++ b/src/internal/task/task_asyncify.go @@ -0,0 +1,127 @@ +//go:build scheduler.asyncify +// +build scheduler.asyncify + +package task + +import ( + "unsafe" +) + +// Stack canary, to detect a stack overflow. The number is a random number +// generated by random.org. The bit fiddling dance is necessary because +// otherwise Go wouldn't allow the cast to a smaller integer size. +const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0))) + +//go:linkname runtimePanic runtime.runtimePanic +func runtimePanic(str string) + +// state is a structure which holds a reference to the state of the task. +// When the task is suspended, the stack pointers are saved here. +type state struct { + // entry is the entry function of the task. + // This is needed every time the function is invoked so that asyncify knows what to rewind. + entry uintptr + + // args are a pointer to a struct holding the arguments of the function. + args unsafe.Pointer + + // stackState is the state of the stack while unwound. + stackState + + launched bool +} + +// stackState is the saved state of a stack while unwound. +// The stack is arranged with asyncify at the bottom, C stack at the top, and a gap of available stack space between the two. +type stackState struct { + // asyncify is the stack pointer of the asyncify stack. + // This starts from the bottom and grows upwards. + asyncifysp uintptr + + // asyncify is stack pointer of the C stack. + // This starts from the top and grows downwards. + csp uintptr +} + +// start creates and starts a new goroutine with the given function and arguments. +// The new goroutine is immediately started. +func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) { + t := &Task{} + t.state.initialize(fn, args, stackSize) + runqueuePushBack(t) +} + +//export tinygo_launch +func (*state) launch() + +//go:linkname align runtime.align +func align(p uintptr) uintptr + +// initialize the state and prepare to call the specified function with the specified argument bundle. +func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) { + // Save the entry call. + s.entry = fn + s.args = args + + // Create a stack. + stack := make([]uintptr, stackSize/unsafe.Sizeof(uintptr(0))) + + // Calculate stack base addresses. + s.asyncifysp = uintptr(unsafe.Pointer(&stack[0])) + s.csp = uintptr(unsafe.Pointer(&stack[0])) + uintptr(len(stack))*unsafe.Sizeof(uintptr(0)) + stack[0] = stackCanary +} + +//go:linkname runqueuePushBack runtime.runqueuePushBack +func runqueuePushBack(*Task) + +// currentTask is the current running task, or nil if currently in the scheduler. +var currentTask *Task + +// Current returns the current active task. +func Current() *Task { + return currentTask +} + +// Pause suspends the current task and returns to the scheduler. +// This function may only be called when running on a goroutine stack, not when running on the system stack. +func Pause() { + // This is mildly unsafe but this is also the only place we can do this. + if *(*uintptr)(unsafe.Pointer(currentTask.state.asyncifysp)) != stackCanary { + runtimePanic("stack overflow") + } + + currentTask.state.unwind() + + *(*uintptr)(unsafe.Pointer(currentTask.state.asyncifysp)) = stackCanary +} + +//export tinygo_unwind +func (*stackState) unwind() + +// Resume the task until it pauses or completes. +// This may only be called from the scheduler. +func (t *Task) Resume() { + // The current task must be saved and restored because this can nest on WASM with JS. + prevTask := currentTask + currentTask = t + if !t.state.launched { + t.state.launch() + t.state.launched = true + } else { + t.state.rewind() + } + currentTask = prevTask + if t.state.asyncifysp > t.state.csp { + runtimePanic("stack overflow") + } +} + +//export tinygo_rewind +func (*state) rewind() + +// OnSystemStack returns whether the caller is running on the system stack. +func OnSystemStack() bool { + // If there is not an active goroutine, then this must be running on the system stack. + return Current() == nil +} diff --git a/src/internal/task/task_asyncify_wasm.S b/src/internal/task/task_asyncify_wasm.S new file mode 100644 index 00000000..3d146b4e --- /dev/null +++ b/src/internal/task/task_asyncify_wasm.S @@ -0,0 +1,99 @@ +.globaltype __stack_pointer, i32 + +.global tinygo_unwind +.type tinygo_unwind,@function +tinygo_unwind: // func (state *stackState) unwind() + .functype tinygo_unwind (i32) -> () + // Check if we are rewinding. + i32.const 0 + i32.load8_u tinygo_rewinding + if // if tinygo_rewinding { + // Stop rewinding. + call stop_rewind + i32.const 0 + i32.const 0 + i32.store8 tinygo_rewinding // tinygo_rewinding = false; + else + // Save the C stack pointer (destination structure pointer is in local 0). + local.get 0 + global.get __stack_pointer + i32.store 4 // state.csp = getCurrentStackPointer() + // Ask asyncify to unwind. + // When resuming, asyncify will return this function with tinygo_rewinding set to true. + local.get 0 + call start_unwind // asyncify.start_unwind(state) + end_if + return + end_function + +.global tinygo_launch +.type tinygo_launch,@function +tinygo_launch: // func (state *state) launch() + .functype tinygo_launch (i32) -> () + // Switch to the goroutine's C stack. + global.get __stack_pointer // prev := getCurrentStackPointer() + local.get 0 + i32.load 12 + global.set __stack_pointer // setStackPointer(state.csp) + // Get the argument pack and entry pointer. + local.get 0 + i32.load 4 // args := state.args + local.get 0 + i32.load 0 // fn := state.entry + // Launch the entry function. + call_indirect (i32) -> () // fn(args) + // Stop unwinding. + call stop_unwind + // Restore the C stack. + global.set __stack_pointer // setStackPointer(prev) + return + end_function + +.global tinygo_rewind +.type tinygo_rewind,@function +tinygo_rewind: // func (state *state) rewind() + .functype tinygo_rewind (i32) -> () + // Switch to the goroutine's C stack. + global.get __stack_pointer // prev := getCurrentStackPointer() + local.get 0 + i32.load 12 + global.set __stack_pointer // setStackPointer(state.csp) + // Get the argument pack and entry pointer. + local.get 0 + i32.load 4 // args := state.args + local.get 0 + i32.load 0 // fn := state.entry + // Prepare to rewind. + i32.const 0 + i32.const 1 + i32.store8 tinygo_rewinding // tinygo_rewinding = true; + local.get 0 + i32.const 8 + i32.add + call start_rewind // asyncify.start_rewind(&state.stackState) + // Launch the entry function. + // This will actually rewind the call stack. + call_indirect (i32) -> () // fn(args) + // Stop unwinding. + call stop_unwind + // Restore the C stack. + global.set __stack_pointer // setStackPointer(prev) + return + end_function + +.functype start_unwind (i32) -> () +.import_module start_unwind, asyncify +.functype stop_unwind () -> () +.import_module stop_unwind, asyncify +.functype start_rewind (i32) -> () +.import_module start_rewind, asyncify +.functype stop_rewind () -> () +.import_module stop_rewind, asyncify + + .hidden tinygo_rewinding # @tinygo_rewinding + .type tinygo_rewinding,@object + .section .bss.tinygo_rewinding,"",@ + .globl tinygo_rewinding +tinygo_rewinding: + .int8 0 # 0x0 + .size tinygo_rewinding, 1 diff --git a/src/runtime/gc_conservative.go b/src/runtime/gc_conservative.go index ea142be9..cabd4142 100644 --- a/src/runtime/gc_conservative.go +++ b/src/runtime/gc_conservative.go @@ -1,3 +1,4 @@ +//go:build gc.conservative // +build gc.conservative package runtime diff --git a/src/runtime/runtime_wasm_js.go b/src/runtime/runtime_wasm_js.go index f4335e12..a5f2505d 100644 --- a/src/runtime/runtime_wasm_js.go +++ b/src/runtime/runtime_wasm_js.go @@ -1,3 +1,4 @@ +//go:build wasm && !wasi // +build wasm,!wasi package runtime @@ -6,13 +7,19 @@ import "unsafe" type timeUnit float64 // time in milliseconds, just like Date.now() in JavaScript +// wasmNested is used to detect scheduler nesting (WASM calls into JS calls back into WASM). +// When this happens, we need to use a reduced version of the scheduler. +var wasmNested bool + //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) + wasmNested = true run() + wasmNested = false } var handleEvent func() @@ -27,12 +34,27 @@ func resume() { go func() { handleEvent() }() + + if wasmNested { + minSched() + return + } + + wasmNested = true scheduler() + wasmNested = false } //export go_scheduler func go_scheduler() { + if wasmNested { + minSched() + return + } + + wasmNested = true scheduler() + wasmNested = false } func ticksToNanoseconds(ticks timeUnit) int64 { diff --git a/src/runtime/scheduler.go b/src/runtime/scheduler.go index 44b07f75..618b6638 100644 --- a/src/runtime/scheduler.go +++ b/src/runtime/scheduler.go @@ -172,6 +172,24 @@ func scheduler() { } } +// This horrible hack exists to make WASM work properly. +// When a WASM program calls into JS which calls back into WASM, the event with which we called back in needs to be handled before returning. +// Thus there are two copies of the scheduler running at once. +// This is a reduced version of the scheduler which does not deal with the timer queue (that is a problem for the outer scheduler). +func minSched() { + scheduleLog("start nested scheduler") + for !schedulerDone { + t := runqueue.Pop() + if t == nil { + break + } + + scheduleLogTask(" run:", t) + t.Resume() + } + scheduleLog("stop nested scheduler") +} + func Gosched() { runqueue.Push(task.Current()) task.Pause() diff --git a/targets/wasi.json b/targets/wasi.json index b7852ad4..b056740b 100644 --- a/targets/wasi.json +++ b/targets/wasi.json @@ -6,6 +6,8 @@ "goarch": "arm", "linker": "wasm-ld", "libc": "wasi-libc", + "scheduler": "asyncify", + "default-stack-size": 8192, "ldflags": [ "--allow-undefined", "--stack-first", diff --git a/targets/wasm.json b/targets/wasm.json index 047dd8c3..8a033fa9 100644 --- a/targets/wasm.json +++ b/targets/wasm.json @@ -6,6 +6,8 @@ "goarch": "wasm", "linker": "wasm-ld", "libc": "wasi-libc", + "scheduler": "asyncify", + "default-stack-size": 8192, "ldflags": [ "--allow-undefined", "--stack-first", diff --git a/tests/wasm/chan_test.go b/tests/wasm/chan_test.go index 793bf1de..e44d7ebe 100644 --- a/tests/wasm/chan_test.go +++ b/tests/wasm/chan_test.go @@ -24,13 +24,12 @@ func TestChan(t *testing.T) { err = chromedp.Run(ctx, chromedp.Navigate(server.URL+"/run?file=chan.wasm"), waitLog(`1 -2 4 +2 3 true`), ) if err != nil { t.Fatal(err) } - } diff --git a/transform/optimizer.go b/transform/optimizer.go index bd6d2168..64c3d0b5 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -125,7 +125,7 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i if err != nil { return []error{err} } - case "tasks": + case "tasks", "asyncify": // No transformations necessary. case "none": // Check for any goroutine starts. @@ -219,7 +219,7 @@ func getFunctionsUsedInTransforms(config *compileopts.Config) []string { case "none": case "coroutines": fnused = append(append([]string{}, fnused...), coroFunctionsUsedInTransforms...) - case "tasks": + case "tasks", "asyncify": fnused = append(append([]string{}, fnused...), taskFunctionsUsedInTransforms...) default: panic(fmt.Errorf("invalid scheduler %q", config.Scheduler()))