diff --git a/.github/actions/setup-binaryen/action.yml b/.github/actions/setup-binaryen/action.yml new file mode 100644 index 0000000000..ed76713576 --- /dev/null +++ b/.github/actions/setup-binaryen/action.yml @@ -0,0 +1,40 @@ +name: "Setup Binaryen" +description: "Install a pinned Binaryen release" +inputs: + version: + description: "Binaryen release version" + required: false + default: "131" + +runs: + using: "composite" + steps: + - name: Install Binaryen + shell: bash + run: | + set -euo pipefail + + version="${{ inputs.version }}" + case "$(uname -s):$(uname -m)" in + Linux:x86_64) platform="x86_64-linux" ;; + Linux:aarch64|Linux:arm64) platform="aarch64-linux" ;; + Darwin:x86_64) platform="x86_64-macos" ;; + Darwin:arm64) platform="arm64-macos" ;; + *) + echo "Unsupported Binaryen host: $(uname -s) $(uname -m)" >&2 + exit 1 + ;; + esac + + archive="binaryen-version_${version}-${platform}.tar.gz" + base_url="https://github.com/WebAssembly/binaryen/releases/download/version_${version}" + cd "$RUNNER_TEMP" + curl --retry 3 --retry-all-errors -fsSLO "${base_url}/${archive}" + curl --retry 3 --retry-all-errors -fsSLO "${base_url}/${archive}.sha256" + if command -v sha256sum >/dev/null; then + sha256sum --check "${archive}.sha256" + else + shasum -a 256 --check "${archive}.sha256" + fi + tar -xzf "$archive" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/binaryen-version_${version}/bin" >> "$GITHUB_PATH" diff --git a/.github/workflows/build-cache.yml b/.github/workflows/build-cache.yml index 62429be59d..570d1e0260 100644 --- a/.github/workflows/build-cache.yml +++ b/.github/workflows/build-cache.yml @@ -33,6 +33,9 @@ jobs: - name: Set up Go uses: ./.github/actions/setup-go + - name: Set up Binaryen + uses: ./.github/actions/setup-binaryen + - name: Install wamr (for wasm tests) if: startsWith(matrix.os, 'macos') run: | diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 8a05f42bed..dadff854d0 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -368,6 +368,9 @@ jobs: - name: Set up Go for building llgo uses: ./.github/actions/setup-go + - name: Set up Binaryen + uses: ./.github/actions/setup-binaryen + - name: Install wamr run: | git clone --branch WAMR-2.4.4 --depth 1 https://github.com/bytecodealliance/wasm-micro-runtime.git @@ -421,6 +424,19 @@ jobs: with: node-version: "25" + - name: Set up Binaryen + uses: ./.github/actions/setup-binaryen + + - name: Set up Wasmtime + uses: bytecodealliance/actions/wasmtime/setup@v1 + with: + version: "39.0.1" + + - name: Set up wasm-tools + uses: bytecodealliance/actions/wasm-tools/setup@v1 + with: + version: "1.243.0" + - name: Set up Go for building llgo uses: ./.github/actions/setup-go @@ -448,10 +464,31 @@ jobs: grep -Fq "fatal error: all goroutines are asleep - deadlock!" <<<"$output" } + run_wasi_scheduler() { + local module="$1" + local output + wasm-tools validate --features all "$module" + output=$(wasmtime run -W exceptions=y "$module" 2>&1) + grep -Fq "wasm scheduler ok" <<<"$output" + if output=$(wasmtime run -W exceptions=y \ + --env LLGO_WASM_SCHEDULER_DEADLOCK=1 "$module" 2>&1); then + echo "deadlock scheduler fixture unexpectedly succeeded" + return 1 + fi + grep -Fq "fatal error: all goroutines are asleep - deadlock!" <<<"$output" + } + GOOS=js GOARCH=wasm llgo build -o "$RUNNER_TEMP/runtime-js.wasm" ./internal/build/testdata/wasm-runtime GOOS=wasip1 GOARCH=wasm llgo build -o "$RUNNER_TEMP/runtime-wasip1.wasm" ./internal/build/testdata/wasm-runtime + LLGO_WASI_THREADS=1 GOOS=wasip1 GOARCH=wasm llgo build \ + -o "$RUNNER_TEMP/runtime-wasip1-threads.wasm" ./internal/build/testdata/wasm-runtime + test "$(wasmtime run -W exceptions=y "$RUNNER_TEMP/runtime-wasip1.wasm" 2>&1)" = "wasip1" GOOS=js GOARCH=wasm llgo build -o "$RUNNER_TEMP/wasm-scheduler-go.mjs" ./internal/build/testdata/wasm-scheduler run_wasm_scheduler "$RUNNER_TEMP/wasm-scheduler-go.mjs" llgo build -target wasm -o "$RUNNER_TEMP/wasm-scheduler.mjs" ./internal/build/testdata/wasm-scheduler run_wasm_scheduler "$RUNNER_TEMP/wasm-scheduler.mjs" - file "$RUNNER_TEMP/runtime-js.wasm" "$RUNNER_TEMP/runtime-wasip1.wasm" + GOOS=wasip1 GOARCH=wasm llgo build -o "$RUNNER_TEMP/wasm-scheduler-wasip1.wasm" ./internal/build/testdata/wasm-scheduler + run_wasi_scheduler "$RUNNER_TEMP/wasm-scheduler-wasip1.wasm" + file "$RUNNER_TEMP/runtime-js.wasm" \ + "$RUNNER_TEMP/runtime-wasip1.wasm" \ + "$RUNNER_TEMP/runtime-wasip1-threads.wasm" diff --git a/internal/build/build.go b/internal/build/build.go index 19ca8eaecb..f0b08fd43d 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -782,7 +782,8 @@ func effectiveTypeSizes(sizes types.Sizes, goos, goarch, target string) types.Si // Named wasm targets use the native wasm32 data model. The raw js/wasm // entry point keeps Go's 64-bit word model and is emitted as Memory64. if goarch == "wasm" && (target != "" || goos != "js") { - return &types.StdSizes{WordSize: 4, MaxAlign: 4} + // LLVM's wasm32 data layout gives 64-bit scalars 8-byte alignment. + return &types.StdSizes{WordSize: 4, MaxAlign: 8} } return sizes } @@ -1413,12 +1414,15 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa } linkArgs = append(linkArgs, cSharedExportArgs(ctx, linkedOrder)...) - err = linkObjFiles(ctx, outputPath, linkInputs, linkArgs, verbose) + linkOutput, err := prepareWasmLinkOutput(ctx.buildConf, &ctx.crossCompile, outputPath) if err != nil { return err } - - return nil + defer cleanupWasmLinkOutput(linkOutput, outputPath) + if err := linkObjFiles(ctx, linkOutput, linkInputs, linkArgs, verbose); err != nil { + return err + } + return publishWasmLinkOutput(ctx, linkOutput, outputPath, verbose) } func linkedModuleGlobals(pkgs []Package) map[string]none { @@ -2439,7 +2443,7 @@ func llvmPassPipeline(level optlevel.Level, ltoMode lto.Mode) string { } func IsWasiThreadsEnabled() bool { - return isEnvOn(llgoWasiThreads, true) + return isEnvOn(llgoWasiThreads, false) } func IsFullRpathEnabled() bool { diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 662eb6a51a..b33332928b 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -301,16 +301,20 @@ func TestEffectiveWasmTypeSizes(t *testing.T) { goos string target string want int64 + align int64 }{ - {name: "Go js wasm", goos: "js", want: 8}, - {name: "configured wasm", goos: "js", target: "wasm", want: 4}, - {name: "WASI compatibility", goos: "wasip1", want: 4}, + {name: "Go js wasm", goos: "js", want: 8, align: 8}, + {name: "configured wasm", goos: "js", target: "wasm", want: 4, align: 8}, + {name: "WASI compatibility", goos: "wasip1", want: 4, align: 8}, } { t.Run(test.name, func(t *testing.T) { got := effectiveTypeSizes(goSizes, test.goos, "wasm", test.target) if size := got.Sizeof(types.Typ[types.Uintptr]); size != test.want { t.Fatalf("uintptr size = %d, want %d", size, test.want) } + if align := got.Alignof(types.Typ[types.Uint64]); align != test.align { + t.Fatalf("uint64 alignment = %d, want %d", align, test.align) + } }) } if got := effectiveTypeSizes(goSizes, "linux", "amd64", ""); got != goSizes { @@ -1158,6 +1162,17 @@ func TestApplyBuildModeCompileFlags(t *testing.T) { applyBuildModeCompileFlags(BuildModeCShared, nil) } +func TestWASIThreadsAreOptIn(t *testing.T) { + t.Setenv(llgoWasiThreads, "") + if IsWasiThreadsEnabled() { + t.Fatal("WASI threads are enabled by default") + } + t.Setenv(llgoWasiThreads, "1") + if !IsWasiThreadsEnabled() { + t.Fatal("WASI threads opt-in was ignored") + } +} + func TestCHeaderPackagesExcludesStandardRuntime(t *testing.T) { prog := llssa.NewProgram(nil) defer prog.Dispose() diff --git a/internal/build/main_module.go b/internal/build/main_module.go index eb57ee8b49..02d8ee9e84 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -88,7 +88,7 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g } var rtInit llssa.Function - if cfg.rtInit { + if cfg.rtInit || ctx.crossCompile.WasmPostLink.Asyncify { rtInit = declareNoArgFunc(mainPkg, rtPkgPath+".init") } @@ -127,10 +127,16 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g return mainAPkg } + var wasmRunMain llssa.Function + if ctx.crossCompile.WasmPostLink.Asyncify { + defineWasmMainTask(mainPkg, mainInit, mainMain) + wasmRunMain = declareNoArgFunc(mainPkg, rtPkgPath+".RunWasmMain") + } entryFn := defineEntryFunction(ctx, mainPkg, argcVar, argvVar, argvValueType, entryFunctions{ runtimeStub: runtimeStub, mainInit: mainInit, mainMain: mainMain, + wasmRunMain: wasmRunMain, pyInit: pyInit, pyFinalize: pyFinalize, rtInit: rtInit, @@ -225,6 +231,7 @@ type entryFunctions struct { runtimeStub llssa.Function mainInit llssa.Function mainMain llssa.Function + wasmRunMain llssa.Function pyInit llssa.Function pyFinalize llssa.Function rtInit llssa.Function @@ -272,8 +279,12 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa b.Call(fns.abiInit.Expr) } b.Call(fns.runtimeStub.Expr) - b.Call(fns.mainInit.Expr) - b.Call(fns.mainMain.Expr) + if fns.wasmRunMain != nil { + b.Call(fns.wasmRunMain.Expr) + } else { + b.Call(fns.mainInit.Expr) + b.Call(fns.mainMain.Expr) + } if fns.pyFinalize != nil { b.Call(fns.pyFinalize.Expr) } @@ -284,6 +295,21 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa return fn } +func defineWasmMainTask(pkg llssa.Package, mainInit, mainMain llssa.Function) { + prog := pkg.Prog + sig := newSignature( + []types.Type{types.Typ[types.UnsafePointer]}, + []types.Type{types.Typ[types.UnsafePointer]}, + ) + fn := pkg.NewFunc("__llgo_wasm_main", sig, llssa.InC) + fnVal := pkg.Module().NamedFunction("__llgo_wasm_main") + fnVal.SetVisibility(llvm.HiddenVisibility) + b := fn.MakeBody(1) + b.Call(mainInit.Expr) + b.Call(mainMain.Expr) + b.Return(prog.Nil(prog.VoidPtr())) +} + func defineStart(pkg llssa.Package, entry llssa.Function, argvType llssa.Type) { fn := pkg.NewFunc("_start", llssa.NoArgsNoRet, llssa.InC) pkg.Module().NamedFunction("_start").SetLinkage(llvm.WeakAnyLinkage) diff --git a/internal/build/main_module_test.go b/internal/build/main_module_test.go index 4e0b16c907..b577b12cdd 100644 --- a/internal/build/main_module_test.go +++ b/internal/build/main_module_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/goplus/llgo/internal/crosscompile" "github.com/xgo-dev/llvm" "github.com/goplus/llgo/internal/packages" @@ -57,6 +58,47 @@ func TestGenMainModuleExecutable(t *testing.T) { ) } +func TestGenMainModuleWASIAsyncifyEntry(t *testing.T) { + llvm.InitializeAllTargets() + t.Setenv(llgoStdioNobuf, "") + ctx := &context{ + prog: llssa.NewProgram(nil), + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "wasip1", + Goarch: "wasm", + }, + crossCompile: crosscompile.Export{ + WasmPostLink: crosscompile.WasmPostLink{Asyncify: true}, + }, + } + pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"} + mod := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{}) + ir := mod.LPkg.String() + checks := []string{ + `define hidden ptr @__llgo_wasm_main(ptr %0)`, + `call void @"github.com/goplus/llgo/runtime/internal/runtime.init"()`, + `call void @"example.com/foo.init"()`, + `call void @"example.com/foo.main"()`, + `call void @"github.com/goplus/llgo/runtime/internal/runtime.RunWasmMain"()`, + } + for _, want := range checks { + if !strings.Contains(ir, want) { + t.Fatalf("WASI main module IR missing %q:\n%s", want, ir) + } + } + entryStart := strings.Index(ir, "define hidden i32 @__main_argc_argv(") + if entryStart < 0 { + t.Fatalf("WASI main module missing host entry:\n%s", ir) + } + entry := ir[entryStart:] + entry = entry[:strings.Index(entry, "}\n")+2] + if strings.Contains(entry, `call void @"example.com/foo.init"()`) || + strings.Contains(entry, `call void @"example.com/foo.main"()`) { + t.Fatalf("WASI system-stack entry calls package main directly:\n%s", entry) + } +} + func TestGenMainModuleLibrary(t *testing.T) { llvm.InitializeAllTargets() t.Setenv(llgoStdioNobuf, "") diff --git a/internal/build/testdata/wasm-scheduler/layout.go b/internal/build/testdata/wasm-scheduler/layout.go new file mode 100644 index 0000000000..7363bb1002 --- /dev/null +++ b/internal/build/testdata/wasm-scheduler/layout.go @@ -0,0 +1,26 @@ +package main + +import "unsafe" + +type wasmStructLayoutProbe struct { + prefix byte + wide uint64 + ptr unsafe.Pointer +} + +func checkWasmStructLayout() { + var value wasmStructLayoutProbe + base := uintptr(unsafe.Pointer(&value)) + if got, want := uintptr(unsafe.Pointer(&value.wide))-base, unsafe.Offsetof(value.wide); got != want { + panic("uint64 field layout mismatch") + } + if got, want := uintptr(unsafe.Pointer(&value.ptr))-base, unsafe.Offsetof(value.ptr); got != want { + panic("pointer field layout mismatch") + } + + var values [2]wasmStructLayoutProbe + stride := uintptr(unsafe.Pointer(&values[1])) - uintptr(unsafe.Pointer(&values[0])) + if stride != unsafe.Sizeof(value) { + panic("struct size layout mismatch") + } +} diff --git a/internal/build/testdata/wasm-scheduler/main.go b/internal/build/testdata/wasm-scheduler/main.go index 12fd8c81ae..1fbc440c45 100644 --- a/internal/build/testdata/wasm-scheduler/main.go +++ b/internal/build/testdata/wasm-scheduler/main.go @@ -30,6 +30,7 @@ var ( eventLog [8]int eventCount int done int + lifecycle int ) func event(value int) { @@ -58,6 +59,7 @@ func checkCurrentG() { } func main() { + checkWasmStructLayout() checkWasmModel() if schedulerDeadlockMode() != 0 { testParkedMainDeadlock() @@ -134,9 +136,23 @@ func main() { if seenGCount != len(seenG) { panic("not all goroutines ran") } + testGoroutineLifecycle() println("wasm scheduler ok") } +func testGoroutineLifecycle() { + const count = 5000 + for i := 1; i <= count; i++ { + want := i + go func() { + lifecycle = want + }() + for lifecycle != want { + runtime.Gosched() + } + } +} + func testParkedMainDeadlock() { go func() {}() parkForTesting() diff --git a/internal/build/testdata/wasm-scheduler/model_go.go b/internal/build/testdata/wasm-scheduler/model_go.go index 73781131c4..9cde6cc3f5 100644 --- a/internal/build/testdata/wasm-scheduler/model_go.go +++ b/internal/build/testdata/wasm-scheduler/model_go.go @@ -2,9 +2,21 @@ package main -import "unsafe" +import ( + "runtime" + "unsafe" +) func checkWasmModel() { + if runtime.GOOS == "wasip1" { + if unsafe.Sizeof(uintptr(0)) != 4 { + panic("GOOS=wasip1 GOARCH=wasm must use 32-bit words") + } + if cLongSize() != 4 { + panic("GOOS=wasip1 GOARCH=wasm must use the wasm32 C data model") + } + return + } if unsafe.Sizeof(uintptr(0)) != 8 { panic("GOOS/GOARCH wasm must use 64-bit words") } diff --git a/internal/build/wasm_postlink.go b/internal/build/wasm_postlink.go new file mode 100644 index 0000000000..b460f153b1 --- /dev/null +++ b/internal/build/wasm_postlink.go @@ -0,0 +1,123 @@ +//go:build !llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package build + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/goplus/llgo/internal/crosscompile" +) + +func needsWasmPostLink(conf *Config, target *crosscompile.Export) bool { + return conf != nil && conf.BuildMode == BuildModeExe && + target != nil && target.WasmPostLink.Asyncify +} + +func wasmPostLinkArgs(target *crosscompile.Export, input, output string, debug bool) []string { + if target == nil || !target.WasmPostLink.Asyncify { + return nil + } + // LLVM 19 lowers Wasm SjLj through the legacy EH encoding. Asyncify + // understands that form; translate it only after instrumentation so the + // final module uses the standardized exnref-based EH instructions. + args := []string{"--asyncify", "--translate-to-exnref"} + if debug { + args = append(args, "-g") + } + return append(args, input, "-o", output) +} + +func prepareWasmLinkOutput(conf *Config, target *crosscompile.Export, output string) (string, error) { + if !needsWasmPostLink(conf, target) { + return output, nil + } + return createClosedTemp( + filepath.Dir(output), + "."+filepath.Base(output)+".linked-*", + ) +} + +func cleanupWasmLinkOutput(input, output string) { + if input != output { + os.Remove(input) + } +} + +func publishWasmLinkOutput(ctx *context, input, output string, verbose bool) error { + if input == output { + return nil + } + return postLinkWasm(ctx, input, output, verbose) +} + +func createClosedTemp(dir, pattern string) (string, error) { + tmp, err := os.CreateTemp(dir, pattern) + if err != nil { + return "", err + } + name := tmp.Name() + if err := tmp.Close(); err != nil { + os.Remove(name) + return "", err + } + return name, nil +} + +func postLinkWasm(ctx *context, input, output string, verbose bool) error { + wasmOpt := os.Getenv("WASMOPT") + if wasmOpt == "" { + wasmOpt = "wasm-opt" + } + resolved, err := exec.LookPath(wasmOpt) + if err != nil { + return fmt.Errorf("WebAssembly Asyncify requires wasm-opt; install Binaryen or set WASMOPT: %w", err) + } + + tmpName, err := createClosedTemp( + filepath.Dir(output), + "."+filepath.Base(output)+".wasm-opt-*", + ) + if err != nil { + return err + } + defer os.Remove(tmpName) + + args := wasmPostLinkArgs( + &ctx.crossCompile, + input, + tmpName, + shouldEmitDebugInfo(ctx.buildConf, &ctx.crossCompile), + ) + if ctx.shouldPrintCommands(verbose) { + fmt.Fprintln(os.Stderr, resolved, args) + } + cmd := exec.Command(resolved, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("wasm-opt Asyncify failed: %w", err) + } + if err := os.Rename(tmpName, output); err != nil { + return err + } + return nil +} diff --git a/internal/build/wasm_postlink_test.go b/internal/build/wasm_postlink_test.go new file mode 100644 index 0000000000..0a00327425 --- /dev/null +++ b/internal/build/wasm_postlink_test.go @@ -0,0 +1,249 @@ +//go:build !llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package build + +import ( + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + "github.com/goplus/llgo/internal/crosscompile" +) + +func wasmPostLinkTestContext() *context { + return &context{ + buildConf: &Config{LinkOptions: LinkOptions{DWARF: DWARFOmit}}, + crossCompile: crosscompile.Export{ + WasmPostLink: crosscompile.WasmPostLink{Asyncify: true}, + }, + } +} + +func writeWasmOptTestTool(t *testing.T, dir, script string) string { + t.Helper() + if runtime.GOOS == "windows" { + t.Skip("test helper uses a POSIX shell") + } + tool := filepath.Join(dir, "wasm-opt") + if err := os.WriteFile(tool, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + return tool +} + +func TestWasmPostLinkArgs(t *testing.T) { + target := &crosscompile.Export{WasmPostLink: crosscompile.WasmPostLink{Asyncify: true}} + if got, want := wasmPostLinkArgs(target, "in.wasm", "out.wasm", false), + []string{"--asyncify", "--translate-to-exnref", "in.wasm", "-o", "out.wasm"}; !reflect.DeepEqual(got, want) { + t.Fatalf("wasmPostLinkArgs() = %v, want %v", got, want) + } + if got, want := wasmPostLinkArgs(target, "in.wasm", "out.wasm", true), + []string{"--asyncify", "--translate-to-exnref", "-g", "in.wasm", "-o", "out.wasm"}; !reflect.DeepEqual(got, want) { + t.Fatalf("wasmPostLinkArgs(debug) = %v, want %v", got, want) + } + if got := wasmPostLinkArgs(&crosscompile.Export{}, "in", "out", false); got != nil { + t.Fatalf("wasmPostLinkArgs(disabled) = %v, want nil", got) + } +} + +func TestNeedsWasmPostLink(t *testing.T) { + target := &crosscompile.Export{WasmPostLink: crosscompile.WasmPostLink{Asyncify: true}} + tests := []struct { + name string + conf *Config + want bool + }{ + {name: "executable", conf: &Config{BuildMode: BuildModeExe}, want: true}, + {name: "archive", conf: &Config{BuildMode: BuildModeCArchive}}, + {name: "shared", conf: &Config{BuildMode: BuildModeCShared}}, + {name: "nil config"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := needsWasmPostLink(test.conf, target); got != test.want { + t.Fatalf("needsWasmPostLink() = %v, want %v", got, test.want) + } + }) + } + if needsWasmPostLink(&Config{BuildMode: BuildModeExe}, nil) { + t.Fatal("needsWasmPostLink() enabled for a nil target") + } +} + +func TestPrepareWasmLinkOutput(t *testing.T) { + dir := t.TempDir() + output := filepath.Join(dir, "app.wasm") + target := &crosscompile.Export{WasmPostLink: crosscompile.WasmPostLink{Asyncify: true}} + + input, err := prepareWasmLinkOutput(&Config{BuildMode: BuildModeExe}, target, output) + if err != nil { + t.Fatal(err) + } + if input == output || filepath.Dir(input) != dir { + t.Fatalf("temporary link output = %q, want a distinct file in %q", input, dir) + } + if _, err := os.Stat(input); err != nil { + t.Fatalf("temporary link output was not created: %v", err) + } + cleanupWasmLinkOutput(input, output) + if _, err := os.Stat(input); !os.IsNotExist(err) { + t.Fatalf("temporary link output remains after cleanup: %v", err) + } + + if err := os.WriteFile(output, []byte("final"), 0o644); err != nil { + t.Fatal(err) + } + input, err = prepareWasmLinkOutput(&Config{BuildMode: BuildModeCArchive}, target, output) + if err != nil || input != output { + t.Fatalf("disabled post-link output = %q, %v; want %q, nil", input, err, output) + } + cleanupWasmLinkOutput(input, output) + if data, err := os.ReadFile(output); err != nil || string(data) != "final" { + t.Fatalf("cleanup removed final output: %q, %v", data, err) + } + if err := publishWasmLinkOutput(nil, output, output, false); err != nil { + t.Fatalf("disabled publish failed: %v", err) + } + + missingOutput := filepath.Join(dir, "missing", "app.wasm") + if _, err := prepareWasmLinkOutput(&Config{BuildMode: BuildModeExe}, target, missingOutput); err == nil { + t.Fatal("prepareWasmLinkOutput succeeded with a missing output directory") + } +} + +func TestPostLinkWasmPublishesOutput(t *testing.T) { + dir := t.TempDir() + input := filepath.Join(dir, "linked.wasm") + output := filepath.Join(dir, "app.wasm") + argsFile := filepath.Join(dir, "args") + if err := os.WriteFile(input, []byte("core module"), 0o644); err != nil { + t.Fatal(err) + } + + script := `#!/bin/sh +printf '%s\n' "$@" > "$ARGS_FILE" +cp "$3" "$5" +` + tool := writeWasmOptTestTool(t, dir, script) + t.Setenv("WASMOPT", "") + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("ARGS_FILE", argsFile) + + ctx := wasmPostLinkTestContext() + stderr, err := os.CreateTemp(dir, "stderr") + if err != nil { + t.Fatal(err) + } + oldStderr := os.Stderr + os.Stderr = stderr + t.Cleanup(func() { os.Stderr = oldStderr }) + + if err := publishWasmLinkOutput(ctx, input, output, true); err != nil { + t.Fatal(err) + } + if err := stderr.Close(); err != nil { + t.Fatal(err) + } + if got, err := os.ReadFile(stderr.Name()); err != nil || + !strings.Contains(string(got), tool) || + !strings.Contains(string(got), "--asyncify") { + t.Fatalf("verbose command = %q, %v", got, err) + } + if data, err := os.ReadFile(output); err != nil || string(data) != "core module" { + t.Fatalf("published output = %q, %v", data, err) + } + args, err := os.ReadFile(argsFile) + if err != nil { + t.Fatal(err) + } + if got := string(args); !strings.Contains(got, "--asyncify\n--translate-to-exnref\n") || + !strings.Contains(got, input+"\n-o\n") { + t.Fatalf("wasm-opt args = %q", got) + } +} + +func TestPostLinkWasmReportsToolFailure(t *testing.T) { + dir := t.TempDir() + input := filepath.Join(dir, "linked.wasm") + output := filepath.Join(dir, "app.wasm") + if err := os.WriteFile(input, []byte("new"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(output, []byte("old"), 0o644); err != nil { + t.Fatal(err) + } + tool := writeWasmOptTestTool(t, dir, "#!/bin/sh\nexit 7\n") + t.Setenv("WASMOPT", tool) + + ctx := wasmPostLinkTestContext() + err := postLinkWasm(ctx, input, output, false) + if err == nil || !strings.Contains(err.Error(), "wasm-opt Asyncify failed") { + t.Fatalf("postLinkWasm() error = %v", err) + } + if data, err := os.ReadFile(output); err != nil || string(data) != "old" { + t.Fatalf("failed post-link changed final output: %q, %v", data, err) + } +} + +func TestPostLinkWasmReportsPublishFailure(t *testing.T) { + dir := t.TempDir() + input := filepath.Join(dir, "linked.wasm") + output := filepath.Join(dir, "existing-directory") + if err := os.WriteFile(input, []byte("core module"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(output, 0o755); err != nil { + t.Fatal(err) + } + script := "#!/bin/sh\ncp \"$3\" \"$5\"\n" + tool := writeWasmOptTestTool(t, dir, script) + t.Setenv("WASMOPT", tool) + + ctx := wasmPostLinkTestContext() + err := postLinkWasm(ctx, input, output, false) + if err == nil { + t.Fatal("postLinkWasm succeeded when the final output was a directory") + } + if strings.Contains(err.Error(), "wasm-opt Asyncify failed") { + t.Fatalf("postLinkWasm failed before publishing output: %v", err) + } +} + +func TestPostLinkWasmReportsMissingTool(t *testing.T) { + t.Setenv("WASMOPT", filepath.Join(t.TempDir(), "missing-wasm-opt")) + ctx := wasmPostLinkTestContext() + err := postLinkWasm(ctx, "input", filepath.Join(t.TempDir(), "output"), false) + if err == nil || !strings.Contains(err.Error(), "install Binaryen or set WASMOPT") { + t.Fatalf("postLinkWasm() error = %v", err) + } +} + +func TestPostLinkWasmReportsInvalidOutputDirectory(t *testing.T) { + dir := t.TempDir() + tool := writeWasmOptTestTool(t, dir, "") + t.Setenv("WASMOPT", tool) + ctx := wasmPostLinkTestContext() + err := postLinkWasm(ctx, "input", filepath.Join(dir, "missing", "output"), false) + if err == nil { + t.Fatal("postLinkWasm succeeded with a missing output directory") + } +} diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index 8e2c6943c0..69a42b93c6 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -42,11 +42,18 @@ type Export struct { FormatDetail string // For uf2, it's uf2FamilyID Emulator string // Emulator command template (e.g., "qemu-system-arm -M {} -kernel {}") DebugInfo DebugInfoPolicy + WasmPostLink WasmPostLink // Flashing/Debugging configuration Device flash.Device // Device configuration for flashing/debugging } +// WasmPostLink describes transformations required after the core module is +// linked. Build orchestration owns tool discovery and atomic output handling. +type WasmPostLink struct { + Asyncify bool +} + // DebugInfoPolicy describes how a selected linker handles debug information. // Build orchestration consumes this typed capability instead of inferring it // from a target name or linker executable. @@ -374,6 +381,9 @@ func useWithJSWasm32(goos, goarch string, wasiThreads, forceEspClang bool, level "-matomics", "-mbulk-memory", } + if wasiThreads { + export.CCFLAGS = append(export.CCFLAGS, "-pthread") + } export.CFLAGS = []string{ "-I" + includeDir, "-Qunused-arguments", @@ -381,12 +391,20 @@ func useWithJSWasm32(goos, goarch string, wasiThreads, forceEspClang bool, level } // Add WebAssembly linker flags export.LDFLAGS = append(export.LDFLAGS, export.CCFLAGS...) + export.LDFLAGS = append(export.LDFLAGS, "-fwasm-exceptions") + if ltoMode.Enabled() { + export.LDFLAGS = append(export.LDFLAGS, "-Wl,--mllvm=-wasm-enable-sjlj") + } + export.CCFLAGS = append( + export.CCFLAGS, + "-fwasm-exceptions", + "-mllvm", "-wasm-enable-sjlj", + ) export.LDFLAGS = append(export.LDFLAGS, []string{ "-Wno-override-module", "-Wl,--error-limit=0", "-L" + libDir, "-Wl,--allow-undefined", - "-Wl,--import-memory,", // unknown import: `env::memory` has not been defined "-Wl,--export-memory", "-Wl,--initial-memory=67108864", // 64MB "-mbulk-memory", @@ -403,22 +421,19 @@ func useWithJSWasm32(goos, goarch string, wasiThreads, forceEspClang bool, level "-lwasi-emulated-getpid", "-lwasi-emulated-process-clocks", "-lwasi-emulated-signal", - "-fwasm-exceptions", - "-mllvm", "-wasm-enable-sjlj", }...) export.LLVMTarget = "wasm32-unknown-wasip1" // Add thread support if enabled if wasiThreads { - export.CCFLAGS = append( - export.CCFLAGS, - "-pthread", - ) - export.LDFLAGS = append(export.LDFLAGS, export.CCFLAGS...) + export.BuildTags = append(export.BuildTags, "llgo.wasi_threads") export.LDFLAGS = append( export.LDFLAGS, + "-Wl,--import-memory", "-lwasi-emulated-pthread", "-lpthread", ) + } else { + export.WasmPostLink.Asyncify = true } case "js": diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index 66be579201..f811bf3e9b 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -125,6 +125,16 @@ func TestUseCrossCompileSDK(t *testing.T) { if !hasResourceDir { t.Error("Missing -resource-dir flag in CCFLAGS") } + if !slices.Contains(export.CCFLAGS, "-fwasm-exceptions") || + !hasMllvmOption(export.CCFLAGS, "-wasm-enable-sjlj") { + t.Errorf("CCFLAGS do not enable WebAssembly SjLj lowering: %v", export.CCFLAGS) + } + if !export.WasmPostLink.Asyncify { + t.Error("WASI target does not request Asyncify post-link processing") + } + if slices.Contains(export.LDFLAGS, "-Wl,--import-memory") { + t.Errorf("single-worker WASI imports host memory: %v", export.LDFLAGS) + } } else if tc.name == "Same Platform" { // For same platform, we expect sysroot only on macOS if runtime.GOOS == "darwin" && !hasSysroot { @@ -173,6 +183,41 @@ func TestUseCrossCompileSDK(t *testing.T) { } } +func TestUseWASIThreadsImportsMemory(t *testing.T) { + if testing.Short() { + t.Skip("requires WASI SDK") + } + export, err := use("wasip1", "wasm", true, false, optlevel.O2, lto.Off, false) + if err != nil { + t.Fatal(err) + } + if !slices.Contains(export.CCFLAGS, "-pthread") { + t.Fatalf("CCFLAGS do not enable WASI threads: %v", export.CCFLAGS) + } + if !slices.Contains(export.BuildTags, "llgo.wasi_threads") { + t.Fatalf("BuildTags do not select the WASI pthread backend: %v", export.BuildTags) + } + if !slices.Contains(export.LDFLAGS, "-Wl,--import-memory") { + t.Fatalf("LDFLAGS do not import shared host memory: %v", export.LDFLAGS) + } + if export.WasmPostLink.Asyncify { + t.Fatal("WASI pthread mode requests single-worker Asyncify processing") + } +} + +func TestUseWASILTOEnablesSjLjAtLink(t *testing.T) { + if testing.Short() { + t.Skip("requires WASI SDK") + } + export, err := use("wasip1", "wasm", false, false, optlevel.O2, lto.Thin, false) + if err != nil { + t.Fatal(err) + } + if !slices.Contains(export.LDFLAGS, "-Wl,--mllvm=-wasm-enable-sjlj") { + t.Fatalf("LDFLAGS do not enable Wasm SjLj for LTO: %v", export.LDFLAGS) + } +} + func TestUseJSSupportsNode(t *testing.T) { export, err := use("js", "wasm", false, false, optlevel.Oz, lto.Off, false) if err != nil { diff --git a/runtime/internal/clite/emscripten/fiber.go b/runtime/internal/clite/emscripten/fiber.go index 00a4fe87f7..95e6ae2410 100644 --- a/runtime/internal/clite/emscripten/fiber.go +++ b/runtime/internal/clite/emscripten/fiber.go @@ -29,14 +29,14 @@ type Fiber struct { //llgo:type C type FiberEntry func(c.Pointer) -// llgo:link (*Fiber).Init C.emscripten_fiber_init -func (fiber *Fiber) Init(entry FiberEntry, arg, stack c.Pointer, stackSize uintptr, asyncifyStack c.Pointer, asyncifyStackSize uintptr) { +// llgo:link FiberInit C.emscripten_fiber_init +func FiberInit(fiber *Fiber, entry FiberEntry, arg, stack c.Pointer, stackSize uintptr, asyncifyStack c.Pointer, asyncifyStackSize uintptr) { } -// llgo:link (*Fiber).InitCurrent C.emscripten_fiber_init_from_current_context -func (fiber *Fiber) InitCurrent(asyncifyStack c.Pointer, asyncifyStackSize uintptr) { +// llgo:link FiberInitCurrent C.emscripten_fiber_init_from_current_context +func FiberInitCurrent(fiber *Fiber, asyncifyStack c.Pointer, asyncifyStackSize uintptr) { } -// llgo:link (*Fiber).Swap C.emscripten_fiber_swap -func (fiber *Fiber) Swap(next *Fiber) { +// llgo:link FiberSwap C.emscripten_fiber_swap +func FiberSwap(fiber, next *Fiber) { } diff --git a/runtime/internal/clite/emscripten/fiber_test.go b/runtime/internal/clite/emscripten/fiber_test.go index da2456dbc1..42c41eb5bc 100644 --- a/runtime/internal/clite/emscripten/fiber_test.go +++ b/runtime/internal/clite/emscripten/fiber_test.go @@ -1,6 +1,7 @@ package emscripten import ( + "reflect" "testing" "unsafe" ) @@ -10,3 +11,9 @@ func TestFiberStorageUsesEightWords(t *testing.T) { t.Fatalf("Fiber size = %d, want %d", got, want) } } + +func TestFiberHasNoReflectableHostMethods(t *testing.T) { + if got := reflect.TypeOf(Fiber{}).NumMethod(); got != 0 { + t.Fatalf("Fiber has %d reflectable methods, want 0", got) + } +} diff --git a/runtime/internal/runtime/fatal_default.go b/runtime/internal/runtime/fatal_default.go index 0046a9ec36..32b9c9dc96 100644 --- a/runtime/internal/runtime/fatal_default.go +++ b/runtime/internal/runtime/fatal_default.go @@ -1,4 +1,4 @@ -//go:build !llgo || !js || !wasm +//go:build !llgo || !wasm || (!js && !wasip1) || (wasip1 && llgo.wasi_threads) package runtime diff --git a/runtime/internal/runtime/fatal_wasm.go b/runtime/internal/runtime/fatal_wasm.go index 3482947aab..1455d0ce08 100644 --- a/runtime/internal/runtime/fatal_wasm.go +++ b/runtime/internal/runtime/fatal_wasm.go @@ -1,4 +1,4 @@ -//go:build llgo && js && wasm +//go:build llgo && wasm && (js || (wasip1 && !llgo.wasi_threads)) package runtime diff --git a/runtime/internal/runtime/g_tls.go b/runtime/internal/runtime/g_tls.go index e02d8ed237..f5566f4cc3 100644 --- a/runtime/internal/runtime/g_tls.go +++ b/runtime/internal/runtime/g_tls.go @@ -1,4 +1,4 @@ -//go:build llgo && !baremetal && (!js || !wasm) +//go:build llgo && !baremetal && (!wasm || (wasip1 && llgo.wasi_threads)) /* * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/runtime/g_wasm.go b/runtime/internal/runtime/g_wasm.go index 9ec5a4f221..78746da0e5 100644 --- a/runtime/internal/runtime/g_wasm.go +++ b/runtime/internal/runtime/g_wasm.go @@ -1,4 +1,4 @@ -//go:build llgo && js && wasm +//go:build llgo && wasm && !(wasip1 && llgo.wasi_threads) /* * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/runtime/os_pthread.go b/runtime/internal/runtime/os_pthread.go index ee29af0633..3f99bf0f45 100644 --- a/runtime/internal/runtime/os_pthread.go +++ b/runtime/internal/runtime/os_pthread.go @@ -1,4 +1,4 @@ -//go:build !llgo || !js || !wasm +//go:build !llgo || !wasm || (wasip1 && llgo.wasi_threads) /* * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/runtime/os_wasm.go b/runtime/internal/runtime/os_wasm.go index 956031b417..02af93043b 100644 --- a/runtime/internal/runtime/os_wasm.go +++ b/runtime/internal/runtime/os_wasm.go @@ -1,4 +1,4 @@ -//go:build llgo && js && wasm +//go:build llgo && wasm && !(wasip1 && llgo.wasi_threads) /* * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/runtime/proc_pthread.go b/runtime/internal/runtime/proc_pthread.go index f3f96fccf7..bebc2f7740 100644 --- a/runtime/internal/runtime/proc_pthread.go +++ b/runtime/internal/runtime/proc_pthread.go @@ -1,4 +1,4 @@ -//go:build !llgo || !js || !wasm +//go:build !llgo || !wasm || (wasip1 && llgo.wasi_threads) /* * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/runtime/proc_wasip1.go b/runtime/internal/runtime/proc_wasip1.go new file mode 100644 index 0000000000..cfa88861e4 --- /dev/null +++ b/runtime/internal/runtime/proc_wasip1.go @@ -0,0 +1,235 @@ +//go:build llgo && wasip1 && wasm && !llgo.wasi_threads + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import ( + "unsafe" + + "github.com/goplus/llgo/runtime/internal/runqueue" + "github.com/goplus/llgo/runtime/internal/wasmcontext" +) + +type runtimeContextPlatform struct { + context wasmcontext.Context + runqNext *g + runqQueued bool +} + +var wasmSched struct { + m m + p p + runq runqueue.Queue[*g] + started bool + mainExited bool +} + +func initRuntimeContext(ctx *runtimeContext, callergp *g, status uint32) *g { + gp := initG(ctx, callergp, status) + if status == _Grunning { + initWasmScheduler(gp) + } + return gp +} + +func initWasmScheduler(gp *g) { + if wasmSched.started { + fatal("runtime: WebAssembly scheduler initialized twice") + return + } + wasmSched.started = true + mp := &wasmSched.m + pp := &wasmSched.p + mp.curg = gp + mp.p = pp + mp.id = nextMid(mp) + pp.id = nextPid(pp) + setpstatus(pp, _Prunning) + pp.m = mp + gp.m = mp +} + +//go:linkname wasmMainTask __llgo_wasm_main +func wasmMainTask(unsafe.Pointer) unsafe.Pointer + +// RunWasmMain runs package initialization and main.main as the first +// Asyncify task. It remains on the system stack and dispatches one G at a time. +func RunWasmMain() { + gp := getg() + if gp == nil || !gp.isMain { + fatal("runtime: invalid WebAssembly main goroutine") + return + } + initWasmContext(gp, wasmcontext.Entry(wasmMainTask), nil, 0) + + for { + runWasmContext(gp) + status := readgstatus(gp) + if gp.isMain && status == _Grunning { + casgstatus(gp, _Grunning, _Gdead) + releaseWasmContext(gp) + return + } + releaseWasmOwnership(gp) + if status == _Gdead { + releaseWasmContext(gp) + } + + gp = wasmSched.runq.Pop() + if gp == nil { + if wasmSched.mainExited { + fatal("no goroutines (main called runtime.Goexit) - deadlock!") + } else { + fatal("all goroutines are asleep - deadlock!") + } + return + } + } +} + +func runWasmContext(gp *g) { + if readgstatus(gp) == _Grunnable { + casgstatus(gp, _Grunnable, _Grunning) + } + mp := &wasmSched.m + pp := &wasmSched.p + mp.curg = gp + pp.m = mp + gp.m = mp + setg(gp) + gp.context.platform.context.Resume() +} + +func releaseWasmOwnership(gp *g) { + if gp != nil { + gp.m = nil + } + wasmSched.m.curg = nil +} + +func newprocBackend(fn goroutineFunc, arg unsafe.Pointer, stackSize uintptr, callergp *g) { + gp := newproc1(fn, arg, callergp) + initWasmContext(gp, wasmcontext.Entry(wasmGStart), unsafe.Pointer(gp), stackSize) + if !wasmSched.runq.Push(gp) { + fatal("runtime: invalid run queue insertion") + } +} + +func initWasmContext(gp *g, entry wasmcontext.Entry, arg unsafe.Pointer, stackSize uintptr) { + if !gp.context.platform.context.Init( + entry, + arg, + stackSize, + AllocRoot, + FreeRoot, + ) { + panic("runtime: failed to allocate WebAssembly goroutine stack") + } +} + +func releaseWasmContext(gp *g) { + if gp == nil || gp.context == nil { + return + } + ctx := gp.context + ctx.platform.context.Close(FreeRoot) + freeRuntimeContext(ctx) +} + +func wasmGStart(arg unsafe.Pointer) unsafe.Pointer { + gp := (*g)(arg) + if gp == nil || getg() != gp { + fatal("runtime: invalid WebAssembly goroutine entry") + return nil + } + fn, fnarg := gp.startfn, gp.startarg + gp.startfn = nil + gp.startarg = nil + ret := fn(fnarg) + goexitBackend(gp) + return ret +} + +func goschedBackend() { + gp := getg() + casgstatus(gp, _Grunning, _Grunnable) + if !wasmSched.runq.Push(gp) { + fatal("runtime: invalid run queue insertion") + return + } + gp.context.platform.context.Suspend() +} + +func gopark() { + gp := getg() + casgstatus(gp, _Grunning, _Gwaiting) + gp.context.platform.context.Suspend() +} + +func goready(gp *g) { + if gp == nil { + fatal("runtime: ready of nil goroutine") + return + } + casgstatus(gp, _Gwaiting, _Grunnable) + if !wasmSched.runq.Push(gp) { + fatal("runtime: invalid run queue insertion") + } +} + +func goexitBackend(gp *g) { + casgstatus(gp, _Grunning, _Gdead) + if gp.isMain { + wasmSched.mainExited = true + } + gp.context.platform.context.Suspend() + fatal("runtime: resumed dead WebAssembly goroutine") +} + +// CurrentGForTesting returns an opaque handle suitable for ReadyForTesting. +func CurrentGForTesting() unsafe.Pointer { + return unsafe.Pointer(getg()) +} + +// ParkForTesting parks the current G until another G marks it runnable. +func ParkForTesting() { + gopark() +} + +// ReadyForTesting makes a G previously parked by ParkForTesting runnable. +func ReadyForTesting(handle unsafe.Pointer) { + goready((*g)(handle)) +} + +// SchedulerStateForTesting reports single-worker queue and ownership state. +func SchedulerStateForTesting() (runq uintptr, mid int64, pid int32) { + return wasmSched.runq.Len(), wasmSched.m.id, wasmSched.p.id +} + +func GMPForTesting() (goid, parentGoid uint64, mid int64, pid int32, gstatus, pstatus uint32, linked bool) { + gp := getg() + if gp == nil || gp.m == nil || gp.m.p == nil { + return + } + mp := gp.m + pp := mp.p + ctx := gp.context + return gp.goid, gp.parentGoid, mp.id, pp.id, readgstatus(gp), readpstatus(pp), + mp == &wasmSched.m && pp == &wasmSched.p && + mp.curg == gp && pp.m == mp && ctx != nil && &ctx.g == gp +} diff --git a/runtime/internal/runtime/proc_wasm.go b/runtime/internal/runtime/proc_wasm.go index ec6a0572a7..d0f07791d3 100644 --- a/runtime/internal/runtime/proc_wasm.go +++ b/runtime/internal/runtime/proc_wasm.go @@ -21,21 +21,14 @@ package runtime import ( "unsafe" - "github.com/goplus/llgo/runtime/internal/clite/emscripten" "github.com/goplus/llgo/runtime/internal/runqueue" -) - -const ( - defaultWasmGStackSize = 64 << 10 - defaultWasmAsyncifyStackSize = 64 << 10 + "github.com/goplus/llgo/runtime/internal/wasmcontext" ) type runtimeContextPlatform struct { - fiber emscripten.Fiber - stack unsafe.Pointer - asyncifyStack unsafe.Pointer - runqNext *g - runqQueued bool + context wasmcontext.Context + runqNext *g + runqQueued bool } var wasmSched struct { @@ -46,22 +39,6 @@ var wasmSched struct { started bool } -func (gp *g) RunqueueNext() *g { - return gp.context.platform.runqNext -} - -func (gp *g) SetRunqueueNext(next *g) { - gp.context.platform.runqNext = next -} - -func (gp *g) RunqueueQueued() bool { - return gp.context.platform.runqQueued -} - -func (gp *g) SetRunqueueQueued(queued bool) { - gp.context.platform.runqQueued = queued -} - func initRuntimeContext(ctx *runtimeContext, callergp *g, status uint32) *g { gp := initG(ctx, callergp, status) if status == _Grunning { @@ -96,48 +73,26 @@ func newprocBackend(fn goroutineFunc, arg unsafe.Pointer, stackSize uintptr, cal } func initWasmFiber(gp *g, stackSize uintptr) { - if stackSize == 0 { - stackSize = defaultWasmGStackSize - } - stackSize = alignWasmStackSize(stackSize) - asyncifySize := uintptr(defaultWasmAsyncifyStackSize) - if stackSize > asyncifySize { - asyncifySize = stackSize - } - platform := &gp.context.platform - platform.stack = allocWasmStack(stackSize) - platform.asyncifyStack = allocWasmStack(asyncifySize) - platform.fiber.Init( - emscripten.FiberEntry(wasmGStart), + if !platform.context.Init( + wasmcontext.Entry(wasmGStart), unsafe.Pointer(gp), - platform.stack, stackSize, - platform.asyncifyStack, - asyncifySize, - ) -} - -func alignWasmStackSize(size uintptr) uintptr { - const alignment = uintptr(16) - return (size + alignment - 1) &^ (alignment - 1) -} - -func allocWasmStack(size uintptr) unsafe.Pointer { - stack := AllocRoot(size) - if stack == nil { + AllocRoot, + FreeRoot, + ) { panic("runtime: failed to allocate WebAssembly goroutine stack") } - return stack } func ensureCurrentWasmFiber(gp *g) { - platform := &gp.context.platform - if platform.asyncifyStack != nil { + context := &gp.context.platform.context + if context.Ready() { return } - platform.asyncifyStack = allocWasmStack(defaultWasmAsyncifyStackSize) - platform.fiber.InitCurrent(platform.asyncifyStack, defaultWasmAsyncifyStackSize) + if !context.InitCurrent(AllocRoot) { + panic("runtime: failed to allocate WebAssembly goroutine stack") + } } func wasmGStart(arg unsafe.Pointer) { @@ -197,7 +152,7 @@ func resumeWasmG(old, next *g) { return } ensureCurrentWasmFiber(old) - if next.context.platform.asyncifyStack == nil { + if !next.context.platform.context.Ready() { fatal("runtime: uninitialized WebAssembly goroutine context") return } @@ -208,7 +163,7 @@ func resumeWasmG(old, next *g) { next.m = mp mp.curg = next setg(next) - old.context.platform.fiber.Swap(&next.context.platform.fiber) + old.context.platform.context.Swap(&next.context.platform.context) reapRetiredWasmG() } @@ -241,7 +196,7 @@ func resumeDeadWasmG(old, next *g) { next.m = mp mp.curg = next setg(next) - old.context.platform.fiber.Swap(&next.context.platform.fiber) + old.context.platform.context.Swap(&next.context.platform.context) fatal("runtime: resumed dead WebAssembly goroutine") } @@ -251,15 +206,7 @@ func reapRetiredWasmG() { return } wasmSched.retired = nil - platform := &ctx.platform - if platform.stack != nil { - FreeRoot(platform.stack) - platform.stack = nil - } - if platform.asyncifyStack != nil { - FreeRoot(platform.asyncifyStack) - platform.asyncifyStack = nil - } + ctx.platform.context.Close(FreeRoot) freeRuntimeContext(ctx) } diff --git a/runtime/internal/runtime/runqueue_wasm.go b/runtime/internal/runtime/runqueue_wasm.go new file mode 100644 index 0000000000..c5e5e0e7f4 --- /dev/null +++ b/runtime/internal/runtime/runqueue_wasm.go @@ -0,0 +1,19 @@ +//go:build llgo && wasm && (js || (wasip1 && !llgo.wasi_threads)) + +package runtime + +func (gp *g) RunqueueNext() *g { + return gp.context.platform.runqNext +} + +func (gp *g) SetRunqueueNext(next *g) { + gp.context.platform.runqNext = next +} + +func (gp *g) RunqueueQueued() bool { + return gp.context.platform.runqQueued +} + +func (gp *g) SetRunqueueQueued(queued bool) { + gp.context.platform.runqQueued = queued +} diff --git a/runtime/internal/wasmcontext/_asm/context_wasm.S b/runtime/internal/wasmcontext/_asm/context_wasm.S new file mode 100644 index 0000000000..bc3b33034c --- /dev/null +++ b/runtime/internal/wasmcontext/_asm/context_wasm.S @@ -0,0 +1,121 @@ +// Copyright (c) 2018-2026 The TinyGo Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of the copyright holder nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// This file was adapted for LLGo's wasmcontext ABI and wasm32 WASI scheduler. + +.globaltype __stack_pointer, i32 + +.functype start_unwind (i32) -> () +.import_module start_unwind, asyncify +.import_name start_unwind, start_unwind +.functype stop_unwind () -> () +.import_module stop_unwind, asyncify +.import_name stop_unwind, stop_unwind +.functype start_rewind (i32) -> () +.import_module start_rewind, asyncify +.import_name start_rewind, start_rewind +.functype stop_rewind () -> () +.import_module stop_rewind, asyncify +.import_name stop_rewind, stop_rewind + +.global __llgo_wasm_context_unwind +.hidden __llgo_wasm_context_unwind +.type __llgo_wasm_context_unwind,@function +__llgo_wasm_context_unwind: + .functype __llgo_wasm_context_unwind (i32) -> () + i32.const 0 + i32.load8_u __llgo_wasm_context_rewinding + if + call stop_rewind + i32.const 0 + i32.const 0 + i32.store8 __llgo_wasm_context_rewinding + else + local.get 0 + global.get __stack_pointer + i32.store 16 + local.get 0 + i32.const 8 + i32.add + call start_unwind + end_if + return + end_function + +.global __llgo_wasm_context_launch +.hidden __llgo_wasm_context_launch +.type __llgo_wasm_context_launch,@function +__llgo_wasm_context_launch: + .functype __llgo_wasm_context_launch (i32) -> () + global.get __stack_pointer + local.get 0 + i32.load 16 + global.set __stack_pointer + local.get 0 + i32.load 4 + local.get 0 + i32.load 0 + call_indirect (i32) -> (i32) + drop + call stop_unwind + global.set __stack_pointer + return + end_function + +.global __llgo_wasm_context_rewind +.hidden __llgo_wasm_context_rewind +.type __llgo_wasm_context_rewind,@function +__llgo_wasm_context_rewind: + .functype __llgo_wasm_context_rewind (i32) -> () + global.get __stack_pointer + local.get 0 + i32.load 16 + global.set __stack_pointer + local.get 0 + i32.load 4 + local.get 0 + i32.load 0 + i32.const 0 + i32.const 1 + i32.store8 __llgo_wasm_context_rewinding + local.get 0 + i32.const 8 + i32.add + call start_rewind + call_indirect (i32) -> (i32) + drop + call stop_unwind + global.set __stack_pointer + return + end_function + +.hidden __llgo_wasm_context_rewinding +.type __llgo_wasm_context_rewinding,@object +.section .bss.__llgo_wasm_context_rewinding,"",@ +.globl __llgo_wasm_context_rewinding +__llgo_wasm_context_rewinding: + .int8 0 + .size __llgo_wasm_context_rewinding, 1 diff --git a/runtime/internal/wasmcontext/context_js.go b/runtime/internal/wasmcontext/context_js.go new file mode 100644 index 0000000000..56e267c420 --- /dev/null +++ b/runtime/internal/wasmcontext/context_js.go @@ -0,0 +1,76 @@ +//go:build llgo && js && wasm + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package wasmcontext + +import ( + "unsafe" + + "github.com/goplus/llgo/runtime/internal/clite/emscripten" +) + +type Entry = emscripten.FiberEntry + +// Context wraps the Emscripten Fiber ABI used by JavaScript hosts. +type Context struct { + fiber emscripten.Fiber + stack unsafe.Pointer + asyncifyStack unsafe.Pointer +} + +func (ctx *Context) Init(entry Entry, arg unsafe.Pointer, stackSize uintptr, alloc func(uintptr) unsafe.Pointer, free func(unsafe.Pointer)) bool { + stack, stackSize, asyncifyStack, asyncifySize, ok := allocStorage(stackSize, alloc, free) + if !ok { + return false + } + ctx.stack = stack + ctx.asyncifyStack = asyncifyStack + emscripten.FiberInit( + &ctx.fiber, + entry, + arg, + stack, + stackSize, + asyncifyStack, + asyncifySize, + ) + return true +} + +func (ctx *Context) InitCurrent(alloc func(uintptr) unsafe.Pointer) bool { + asyncifyStack := alloc(defaultAsyncifyStackSize) + if asyncifyStack == nil { + return false + } + ctx.asyncifyStack = asyncifyStack + emscripten.FiberInitCurrent(&ctx.fiber, asyncifyStack, defaultAsyncifyStackSize) + return true +} + +func (ctx *Context) Ready() bool { + return ctx.asyncifyStack != nil +} + +func (ctx *Context) Close(free func(unsafe.Pointer)) { + freeStorage(ctx.stack, ctx.asyncifyStack, free) + *ctx = Context{} +} + +func (ctx *Context) Swap(next *Context) { + emscripten.FiberSwap(&ctx.fiber, &next.fiber) +} diff --git a/runtime/internal/wasmcontext/context_wasip1.go b/runtime/internal/wasmcontext/context_wasip1.go new file mode 100644 index 0000000000..837b42981b --- /dev/null +++ b/runtime/internal/wasmcontext/context_wasip1.go @@ -0,0 +1,88 @@ +//go:build llgo && wasip1 && wasm && !llgo.wasi_threads + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package wasmcontext + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" +) + +//llgo:type C +type Entry func(unsafe.Pointer) unsafe.Pointer + +// Context is the state consumed by Binaryen Asyncify. The first five fields +// have fixed wasm32 offsets shared with context_wasm.S. +type Context struct { + entry unsafe.Pointer + arg unsafe.Pointer + asyncifyStack unsafe.Pointer + asyncifyEnd unsafe.Pointer + stackPointer unsafe.Pointer + launched bool + stack unsafe.Pointer +} + +func (ctx *Context) Init(entry Entry, arg unsafe.Pointer, stackSize uintptr, alloc func(uintptr) unsafe.Pointer, free func(unsafe.Pointer)) bool { + stack, stackSize, asyncifyStack, asyncifySize, ok := allocStorage(stackSize, alloc, free) + if !ok { + return false + } + ctx.entry = c.Func(entry) + ctx.arg = arg + ctx.asyncifyStack = asyncifyStack + ctx.asyncifyEnd = unsafe.Add(asyncifyStack, asyncifySize) + ctx.stackPointer = unsafe.Add(stack, stackSize) + ctx.launched = false + ctx.stack = stack + return true +} + +func (ctx *Context) Ready() bool { + return ctx.asyncifyStack != nil +} + +func (ctx *Context) Close(free func(unsafe.Pointer)) { + freeStorage(ctx.stack, ctx.asyncifyStack, free) + *ctx = Context{} +} + +func (ctx *Context) Resume() { + if !ctx.launched { + contextLaunch(ctx) + ctx.launched = true + return + } + contextRewind(ctx) +} + +func (ctx *Context) Suspend() { + contextUnwind(ctx) +} + +//go:linkname contextLaunch C.__llgo_wasm_context_launch +func contextLaunch(*Context) + +//go:linkname contextRewind C.__llgo_wasm_context_rewind +func contextRewind(*Context) + +//go:linkname contextUnwind C.__llgo_wasm_context_unwind +func contextUnwind(*Context) + +const LLGoFiles = "_asm/context_wasm.S" diff --git a/runtime/internal/wasmcontext/doc.go b/runtime/internal/wasmcontext/doc.go new file mode 100644 index 0000000000..1f0446f7b0 --- /dev/null +++ b/runtime/internal/wasmcontext/doc.go @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package wasmcontext owns suspended WebAssembly execution contexts and their +// backend-specific storage. Runtime schedulers provide root-aware allocation +// callbacks during context creation and do not inspect the resulting buffers. +package wasmcontext diff --git a/runtime/internal/wasmcontext/storage.go b/runtime/internal/wasmcontext/storage.go new file mode 100644 index 0000000000..6b5010dbf0 --- /dev/null +++ b/runtime/internal/wasmcontext/storage.go @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package wasmcontext + +import "unsafe" + +const ( + defaultStackSize = uintptr(64 << 10) + defaultAsyncifyStackSize = uintptr(64 << 10) + stackAlignment = uintptr(16) +) + +func allocStorage(stackSize uintptr, alloc func(uintptr) unsafe.Pointer, free func(unsafe.Pointer)) (stack unsafe.Pointer, normalizedStackSize uintptr, asyncifyStack unsafe.Pointer, asyncifySize uintptr, ok bool) { + if stackSize == 0 { + stackSize = defaultStackSize + } + stackSize = alignStackSize(stackSize) + asyncifySize = defaultAsyncifyStackSize + if stackSize > asyncifySize { + asyncifySize = stackSize + } + + stack = alloc(stackSize) + if stack == nil { + return + } + asyncifyStack = alloc(asyncifySize) + if asyncifyStack == nil { + free(stack) + stack = nil + return + } + return stack, stackSize, asyncifyStack, asyncifySize, true +} + +func freeStorage(stack, asyncifyStack unsafe.Pointer, free func(unsafe.Pointer)) { + if stack != nil { + free(stack) + } + if asyncifyStack != nil { + free(asyncifyStack) + } +} + +func alignStackSize(size uintptr) uintptr { + return (size + stackAlignment - 1) &^ (stackAlignment - 1) +} diff --git a/runtime/internal/wasmcontext/storage_test.go b/runtime/internal/wasmcontext/storage_test.go new file mode 100644 index 0000000000..f938bb0574 --- /dev/null +++ b/runtime/internal/wasmcontext/storage_test.go @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package wasmcontext + +import ( + "testing" + "unsafe" +) + +func TestStorageLifecycle(t *testing.T) { + var allocated []uintptr + var freed []unsafe.Pointer + buffers := make([][]byte, 0, 2) + alloc := func(size uintptr) unsafe.Pointer { + allocated = append(allocated, size) + buf := make([]byte, size) + buffers = append(buffers, buf) + return unsafe.Pointer(&buf[0]) + } + free := func(ptr unsafe.Pointer) { + freed = append(freed, ptr) + } + + stack, stackSize, asyncify, asyncifySize, ok := allocStorage(defaultStackSize+1, alloc, free) + if !ok { + t.Fatal("init failed") + } + wantSize := defaultStackSize + stackAlignment + if len(allocated) != 2 || allocated[0] != wantSize || allocated[1] != wantSize { + t.Fatalf("allocated sizes = %v, want [%d %d]", allocated, wantSize, wantSize) + } + if stackSize != wantSize || asyncifySize != wantSize { + t.Fatalf("returned sizes = %d/%d, want %d/%d", stackSize, asyncifySize, wantSize, wantSize) + } + + freeStorage(stack, asyncify, free) + if len(freed) != 2 || freed[0] != stack || freed[1] != asyncify { + t.Fatalf("freed pointers = %v, want [%p %p]", freed, stack, asyncify) + } +} + +func TestStorageInitFailure(t *testing.T) { + buf := make([]byte, defaultStackSize) + stack := unsafe.Pointer(&buf[0]) + for _, failAt := range []int{1, 2} { + allocations := 0 + alloc := func(uintptr) unsafe.Pointer { + allocations++ + if allocations == failAt { + return nil + } + return stack + } + var freed unsafe.Pointer + + stackResult, _, asyncifyResult, _, ok := allocStorage(0, alloc, func(ptr unsafe.Pointer) { freed = ptr }) + if ok { + t.Fatalf("allocation %d failure succeeded", failAt) + } + wantFreed := unsafe.Pointer(nil) + if failAt == 2 { + wantFreed = stack + } + if freed != wantFreed { + t.Fatalf("allocation %d freed pointer = %p, want %p", failAt, freed, wantFreed) + } + if stackResult != nil || asyncifyResult != nil { + t.Fatalf("allocation %d failure returned storage", failAt) + } + } +} + +func TestStorageDefaultSize(t *testing.T) { + var sizes []uintptr + buffers := make([][]byte, 0, 2) + stack, stackSize, asyncify, asyncifySize, ok := allocStorage(0, func(size uintptr) unsafe.Pointer { + sizes = append(sizes, size) + buf := make([]byte, size) + buffers = append(buffers, buf) + return unsafe.Pointer(&buf[0]) + }, func(unsafe.Pointer) {}) + if !ok { + t.Fatal("init failed") + } + if stackSize != defaultStackSize || asyncifySize != defaultAsyncifyStackSize { + t.Fatalf("default sizes = %d/%d", stackSize, asyncifySize) + } + if len(sizes) != 2 || sizes[0] != defaultStackSize || sizes[1] != defaultAsyncifyStackSize { + t.Fatalf("requested sizes = %v", sizes) + } + freeStorage(stack, asyncify, func(unsafe.Pointer) {}) +} diff --git a/ssa/eh.go b/ssa/eh.go index b8ead4eb64..989371c75c 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -548,15 +548,41 @@ func (b Builder) RunDefers() { return } blk := b.Func.MakeBlock() + next := len(self.rundsNext) self.rundsNext = append(self.rundsNext, blk) - b.Store(self.rundPtr, blk.Addr()) + b.storeRunDefersTarget(self.rundPtr, next, blk) b.Jump(self.procBlk) b.SetBlockEx(blk, AtEnd, false) b.blk.last = blk.last } +func (b Builder) storeRunDefersTarget(ptr Expr, index int, target BasicBlock) { + value := target.Addr() + if b.Prog.target.GOARCH == "wasm" { + value = b.PtrCast(b.Prog.VoidPtr(), b.Prog.Val(uintptr(index))) + } + b.Store(ptr, value) +} + +func (b Builder) jumpRunDefersTarget(ptr Expr, targets []BasicBlock) { + target := b.Load(ptr) + if b.Prog.target.GOARCH != "wasm" { + b.IndirectJump(target, targets) + return + } + + index := b.Convert(b.Prog.Uintptr(), target) + invalid := b.Func.MakeBlock() + sw := b.impl.CreateSwitch(index.impl, invalid.first, len(targets)) + for i, target := range targets { + sw.AddCase(b.Prog.Val(uintptr(i)).impl, target.first) + } + b.SetBlockEx(invalid, AtEnd, false) + b.Unreachable() +} + func (p Function) endDefer(b Builder) { self := p.defer_ if self == nil { @@ -593,10 +619,10 @@ func (p Function) endDefer(b Builder) { } link := b.getField(b.Load(self.data), deferLink) b.Call(b.Pkg.rtFunc("SetThreadDefer"), link) - b.IndirectJump(b.Load(rundPtr), nexts) + b.jumpRunDefersTarget(rundPtr, nexts) b.SetBlockEx(panicBlk, AtEnd, false) // panicBlk: exec runDefers and rethrow - b.Store(rundPtr, rethrowBlk.Addr()) + b.storeRunDefersTarget(rundPtr, 0, rethrowBlk) b.IndirectJump(b.Load(rethPtr), rethsNext) } diff --git a/ssa/eh_defer_test.go b/ssa/eh_defer_test.go index 5f99729b1e..764134695c 100644 --- a/ssa/eh_defer_test.go +++ b/ssa/eh_defer_test.go @@ -156,3 +156,31 @@ func TestConditionalDeferIR(t *testing.T) { t.Fatalf("expected conditional defer bitmask operations in IR, got:\n%s", ir) } } + +func TestWasmRunDefersUsesStaticDispatch(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + prog.Target().GOOS = "js" + prog.Target().GOARCH = "wasm" + pkg := prog.NewPackage("foo", "foo") + + callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo) + cb := callee.MakeBody(1) + cb.Return() + cb.EndBuild() + + fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) + b := fn.MakeBody(1) + fn.SetRecover(fn.MakeBlock()) + b.Defer(ssa.DeferAlways, callee.Expr, ssa.Builder.Call) + b.RunDefers() + b.Return() + b.EndBuild() + + ir := pkg.Module().String() + if !strings.Contains(ir, "switch i64") { + t.Fatalf("expected wasm RunDefers selector dispatch in IR, got:\n%s", ir) + } + if got := strings.Count(ir, "indirectbr"); got != 1 { + t.Fatalf("got %d indirect branches, want only the rethrow dispatch:\n%s", got, ir) + } +}