diff options
31 files changed, 784 insertions, 62 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml index de75739b7..f3534edde 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 '{}' \; @@ -326,6 +368,20 @@ commands: 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 - run: name: "Build wasi-libc" diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index bdb27dca0..4c1f8dd9d 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 483c789c7..a5603ec7e 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 4df14b5ef..c126bd4b9 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 f1b04d2fc..f6c721229 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 @@ -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 000000000..d6b7ef32c --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/builder/build.go b/builder/build.go index 5c3150c11..252714964 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 0ee4d1d07..2d68b60b2 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 fd0416459..065eb97a3 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 1ff532cc4..646558215 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 f16df32cd..0c8ce1d93 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 f1a0e00c8..cbf89fe20 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 b58481928..a77a9b861 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 2d4f1475b..04bfa4af1 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 index eeefa43cf..eeefa43cf 100644 --- a/compiler/testdata/func.ll +++ b/compiler/testdata/func-coroutines.ll diff --git a/compiler/testdata/gc.ll b/compiler/testdata/gc.ll index 7ac0aa1ba..375763f3c 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 index d34a1eb43..d34a1eb43 100644 --- a/compiler/testdata/goroutine-cortex-m-qemu.ll +++ b/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 000000000..1ecf0c796 --- /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 index 6f3082471..6f3082471 100644 --- a/compiler/testdata/goroutine-wasm.ll +++ b/compiler/testdata/goroutine-wasm-coroutines.ll diff --git a/goenv/goenv.go b/goenv/goenv.go index 3f1352a78..0ef18ba50 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 +Subproject 96f7acf09aae1ec6e8bc573dfa8f309c4f892a4 diff --git a/src/internal/task/task_asyncify.go b/src/internal/task/task_asyncify.go new file mode 100644 index 000000000..d67f0e1ca --- /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 000000000..3d146b4e8 --- /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 ea142be93..cabd41427 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 f4335e121..a5f2505d3 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 44b07f75d..618b6638d 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 b7852ad46..b056740b0 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 047dd8c38..8a033fa95 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 793bf1de3..e44d7ebe4 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 bd6d21687..64c3d0b54 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())) |