From 93ffbff6b52ea382b0bc09dde64c1437020f15f2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 13 Jul 2026 21:49:51 +0800 Subject: [PATCH] fix(build): adapt Go runtime sources for wasm32 --- internal/build/plan9asm.go | 3 + internal/build/source_patch.go | 22 +++ internal/build/source_patch_test.go | 132 ++++++++++++++++++ .../_patch/internal/bytealg/bytealg_wasm.go | 76 ++++++++++ .../internal/chacha8rand/chacha8_wasm.go | 11 ++ .../internal/runtime/atomic/atomic_wasm.go | 14 ++ .../_patch/runtime/hash32_wasm_pre_go126.go | 58 ++++++++ runtime/_patch/runtime/malloc_wasm.go | 11 ++ runtime/_patch/runtime/malloc_wasm_go126.go | 11 ++ runtime/build.go | 15 ++ 10 files changed, 353 insertions(+) create mode 100644 runtime/_patch/internal/bytealg/bytealg_wasm.go create mode 100644 runtime/_patch/internal/chacha8rand/chacha8_wasm.go create mode 100644 runtime/_patch/internal/runtime/atomic/atomic_wasm.go create mode 100644 runtime/_patch/runtime/hash32_wasm_pre_go126.go create mode 100644 runtime/_patch/runtime/malloc_wasm.go create mode 100644 runtime/_patch/runtime/malloc_wasm_go126.go diff --git a/internal/build/plan9asm.go b/internal/build/plan9asm.go index 10e8913c16..57938f22c5 100644 --- a/internal/build/plan9asm.go +++ b/internal/build/plan9asm.go @@ -25,6 +25,9 @@ import ( // NOTE: golang.org/x/tools/go/packages.Package does not expose SFiles, so we // query `go list -json` here to get the exact filtered set for GOOS/GOARCH. func compilePkgSFiles(ctx *context, aPkg *aPackage, pkg *packages.Package, verbose bool) ([]string, error) { + if llruntime.SourcePatchReplacesAsmForGOARCH(pkg.PkgPath, ctx.buildConf.Goarch) { + return nil, nil + } sfiles, err := pkgSFiles(ctx, pkg) if err != nil { return nil, err diff --git a/internal/build/source_patch.go b/internal/build/source_patch.go index ca59deeaf5..0ce9686080 100644 --- a/internal/build/source_patch.go +++ b/internal/build/source_patch.go @@ -150,6 +150,28 @@ func applySourcePatchForPkg(base, current map[string][]byte, runtimeDir, goroot, } } + if llruntime.SourcePatchReplacesAsmForGOARCH(pkgPath, ctx.goarch) { + for _, entry := range srcEntries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(name, ".s") || strings.HasSuffix(name, "_test.s") { + continue + } + match, err := buildCtx.MatchFile(srcDir, name) + if err != nil { + return false, nil, fmt.Errorf("match stdlib assembly file %s: %w", filepath.Join(srcDir, name), err) + } + if !match { + continue + } + ensureOverlay() + out[filepath.Join(srcDir, name)] = []byte("// replaced by LLGo source patch\n") + changed = true + } + } + if skipAll { for _, entry := range srcEntries { if entry.IsDir() { diff --git a/internal/build/source_patch_test.go b/internal/build/source_patch_test.go index 319d6b8b2e..4e913720a7 100644 --- a/internal/build/source_patch_test.go +++ b/internal/build/source_patch_test.go @@ -4,6 +4,7 @@ import ( "go/ast" "go/parser" "go/token" + "go/types" "io/fs" "os" "path/filepath" @@ -13,9 +14,140 @@ import ( "testing" "github.com/goplus/llgo/internal/env" + "github.com/goplus/llgo/internal/packages" llruntime "github.com/goplus/llgo/runtime" ) +func TestWasmRuntimeSourcePatchTypeChecks(t *testing.T) { + for _, goos := range []string{"js", "wasip1"} { + t.Run(goos, func(t *testing.T) { + cfgEnv := append(os.Environ(), "GOOS="+goos, "GOARCH=wasm") + goroot, goversion, err := env.GOROOTAndGOVERSIONWithEnv(cfgEnv) + if err != nil { + t.Fatal(err) + } + overlay, err := buildSourcePatchOverlayForGOROOT(nil, env.LLGoRuntimeDir(), goroot, sourcePatchBuildContext{ + goos: goos, + goarch: "wasm", + goversion: goversion, + }) + if err != nil { + t.Fatal(err) + } + + pkgs, err := packages.LoadEx(nil, func(types.Sizes, string, string) types.Sizes { + return &types.StdSizes{WordSize: 4, MaxAlign: 4} + }, &packages.Config{ + Mode: loadSyntax | packages.NeedDeps | packages.NeedModule | packages.NeedExportFile, + Env: cfgEnv, + Fset: token.NewFileSet(), + Overlay: overlay, + }, "runtime") + if err != nil { + t.Fatal(err) + } + if len(pkgs) != 1 { + t.Fatalf("loaded %d runtime packages, want 1", len(pkgs)) + } + if pkgs[0].IllTyped { + logPackageErrors(t, pkgs[0], make(map[string]bool)) + t.Fatal("runtime did not type-check with wasm32 sizes") + } + }) + } +} + +func logPackageErrors(t *testing.T, pkg *packages.Package, seen map[string]bool) { + t.Helper() + if pkg == nil || seen[pkg.ID] { + return + } + seen[pkg.ID] = true + for _, err := range pkg.Errors { + t.Log(err) + } + for _, imported := range pkg.Imports { + if imported.IllTyped { + logPackageErrors(t, imported, seen) + } + } +} + +func TestWasmBytealgSourcePatchReplacesAsm(t *testing.T) { + for _, pkgPath := range []string{"internal/bytealg", "internal/chacha8rand", "internal/runtime/atomic"} { + if !llruntime.HasSourcePatchPkg(pkgPath) { + t.Fatalf("%s should be registered as a source patch package", pkgPath) + } + if !llruntime.SourcePatchReplacesAsmForGOARCH(pkgPath, "wasm") { + t.Fatalf("%s wasm assembly should be replaced by its source patch", pkgPath) + } + if llruntime.SourcePatchReplacesAsmForGOARCH(pkgPath, "arm64") { + t.Fatalf("%s native assembly should remain enabled", pkgPath) + } + } + + overlay, err := buildSourcePatchOverlayForGOROOT(nil, env.LLGoRuntimeDir(), runtime.GOROOT(), sourcePatchBuildContext{ + goos: "js", + goarch: "wasm", + goversion: runtime.Version(), + }) + if err != nil { + t.Fatal(err) + } + for _, file := range []string{ + "internal/bytealg/compare_wasm.s", + "internal/bytealg/equal_wasm.s", + "internal/bytealg/indexbyte_wasm.s", + "internal/chacha8rand/chacha8_stub.s", + "internal/runtime/atomic/atomic_wasm.s", + } { + path := filepath.Join(runtime.GOROOT(), "src", filepath.FromSlash(file)) + if got := string(overlay[path]); got != "// replaced by LLGo source patch\n" { + t.Fatalf("overlay[%q] = %q, want assembly replacement", path, got) + } + } +} + +func TestCompilePkgSFilesSkipsSourcePatchedAssembly(t *testing.T) { + got, err := compilePkgSFiles( + &context{buildConf: &Config{Goarch: "wasm"}}, + nil, + &packages.Package{PkgPath: "internal/bytealg"}, + false, + ) + if err != nil { + t.Fatal(err) + } + if got != nil { + t.Fatalf("compilePkgSFiles returned %v, want no object files", got) + } +} + +func TestSourcePatchAssemblyMatchError(t *testing.T) { + goroot := t.TempDir() + runtimeDir := t.TempDir() + const pkgPath = "internal/bytealg" + srcDir := filepath.Join(goroot, "src", filepath.FromSlash(pkgPath)) + patchDir := filepath.Join(runtimeDir, "_patch", filepath.FromSlash(pkgPath)) + + if err := os.MkdirAll(filepath.Join(srcDir, "adir"), 0755); err != nil { + t.Fatal(err) + } + mustWriteFile(t, filepath.Join(srcDir, "bad_wasm.s"), "//go:build (\n") + mustWriteFile(t, filepath.Join(patchDir, "bytealg_wasm.go"), `//go:build wasm + +package bytealg +`) + + _, _, err := applySourcePatchForPkg(nil, nil, runtimeDir, goroot, pkgPath, sourcePatchBuildContext{ + goos: "js", + goarch: "wasm", + }) + if err == nil || !strings.Contains(err.Error(), "match stdlib assembly file") { + t.Fatalf("applySourcePatchForPkg error = %v, want assembly match error", err) + } +} + func TestBuildSourcePatchOverlayForIter(t *testing.T) { overlay, err := buildSourcePatchOverlayForGOROOT(nil, env.LLGoRuntimeDir(), runtime.GOROOT(), sourcePatchBuildContext{}) if err != nil { diff --git a/runtime/_patch/internal/bytealg/bytealg_wasm.go b/runtime/_patch/internal/bytealg/bytealg_wasm.go new file mode 100644 index 0000000000..da3de44c88 --- /dev/null +++ b/runtime/_patch/internal/bytealg/bytealg_wasm.go @@ -0,0 +1,76 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build wasm + +package bytealg + +import _ "unsafe" // for go:linkname + +func Compare(a, b []byte) int { + l := len(a) + if len(b) < l { + l = len(b) + } + for i := 0; i < l; i++ { + if a[i] < b[i] { + return -1 + } + if a[i] > b[i] { + return 1 + } + } + if len(a) < len(b) { + return -1 + } + if len(a) > len(b) { + return 1 + } + return 0 +} + +func CompareString(a, b string) int { + return runtime_cmpstring(a, b) +} + +//go:linkname runtime_cmpstring runtime.cmpstring +func runtime_cmpstring(a, b string) int { + l := len(a) + if len(b) < l { + l = len(b) + } + for i := 0; i < l; i++ { + if a[i] < b[i] { + return -1 + } + if a[i] > b[i] { + return 1 + } + } + if len(a) < len(b) { + return -1 + } + if len(a) > len(b) { + return 1 + } + return 0 +} + +func IndexByte(b []byte, c byte) int { + for i, value := range b { + if value == c { + return i + } + } + return -1 +} + +func IndexByteString(s string, c byte) int { + for i := 0; i < len(s); i++ { + if s[i] == c { + return i + } + } + return -1 +} diff --git a/runtime/_patch/internal/chacha8rand/chacha8_wasm.go b/runtime/_patch/internal/chacha8rand/chacha8_wasm.go new file mode 100644 index 0000000000..7552843d2a --- /dev/null +++ b/runtime/_patch/internal/chacha8rand/chacha8_wasm.go @@ -0,0 +1,11 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build wasm + +package chacha8rand + +func block(seed *[4]uint64, blocks *[32]uint64, counter uint32) { + block_generic(seed, blocks, counter) +} diff --git a/runtime/_patch/internal/runtime/atomic/atomic_wasm.go b/runtime/_patch/internal/runtime/atomic/atomic_wasm.go new file mode 100644 index 0000000000..a806a3fb67 --- /dev/null +++ b/runtime/_patch/internal/runtime/atomic/atomic_wasm.go @@ -0,0 +1,14 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build wasm + +package atomic + +import "unsafe" + +// StorepNoWB performs *ptr = val without a write barrier. +func StorepNoWB(ptr unsafe.Pointer, val unsafe.Pointer) { + *(*uintptr)(ptr) = uintptr(val) +} diff --git a/runtime/_patch/runtime/hash32_wasm_pre_go126.go b/runtime/_patch/runtime/hash32_wasm_pre_go126.go new file mode 100644 index 0000000000..769ce83649 --- /dev/null +++ b/runtime/_patch/runtime/hash32_wasm_pre_go126.go @@ -0,0 +1,58 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build wasm && !go1.26 + +package runtime + +import "unsafe" + +func memhash32Fallback(p unsafe.Pointer, seed uintptr) uintptr { + a, b := mix32(uint32(seed), uint32(4^hashkey[0])) + t := readUnaligned32(p) + a ^= t + b ^= t + a, b = mix32(a, b) + a, b = mix32(a, b) + return uintptr(a ^ b) +} + +func memhash64Fallback(p unsafe.Pointer, seed uintptr) uintptr { + a, b := mix32(uint32(seed), uint32(8^hashkey[0])) + a ^= readUnaligned32(p) + b ^= readUnaligned32(add(p, 4)) + a, b = mix32(a, b) + a, b = mix32(a, b) + return uintptr(a ^ b) +} + +func memhashFallback(p unsafe.Pointer, seed, s uintptr) uintptr { + a, b := mix32(uint32(seed), uint32(s^hashkey[0])) + if s == 0 { + return uintptr(a ^ b) + } + for ; s > 8; s -= 8 { + a ^= readUnaligned32(p) + b ^= readUnaligned32(add(p, 4)) + a, b = mix32(a, b) + p = add(p, 8) + } + if s >= 4 { + a ^= readUnaligned32(p) + b ^= readUnaligned32(add(p, s-4)) + } else { + t := uint32(*(*byte)(p)) + t |= uint32(*(*byte)(add(p, s>>1))) << 8 + t |= uint32(*(*byte)(add(p, s-1))) << 16 + b ^= t + } + a, b = mix32(a, b) + a, b = mix32(a, b) + return uintptr(a ^ b) +} + +func mix32(a, b uint32) (uint32, uint32) { + c := uint64(a^uint32(hashkey[1])) * uint64(b^uint32(hashkey[2])) + return uint32(c), uint32(c >> 32) +} diff --git a/runtime/_patch/runtime/malloc_wasm.go b/runtime/_patch/runtime/malloc_wasm.go new file mode 100644 index 0000000000..e3e8dd2abb --- /dev/null +++ b/runtime/_patch/runtime/malloc_wasm.go @@ -0,0 +1,11 @@ +//go:build wasm && !go1.26 + +package runtime + +// LLGo uses native wasm32 pointers, while the Go wasm port models uintptr as +// 64 bits. Keep the GC heap geometry in the wasm32 address space. +const ( + heapAddrBits = 32 + maxAlloc = (1 << heapAddrBits) - 1 + logHeapArenaBytes = 22 +) diff --git a/runtime/_patch/runtime/malloc_wasm_go126.go b/runtime/_patch/runtime/malloc_wasm_go126.go new file mode 100644 index 0000000000..f646c50492 --- /dev/null +++ b/runtime/_patch/runtime/malloc_wasm_go126.go @@ -0,0 +1,11 @@ +//go:build wasm && go1.26 + +package runtime + +// Go 1.26 reduced wasm heap arenas to 512 KiB. LLGo still needs the heap +// address and allocation limits adjusted for native wasm32 pointers. +const ( + heapAddrBits = 32 + maxAlloc = (1 << heapAddrBits) - 1 + logHeapArenaBytes = 19 +) diff --git a/runtime/build.go b/runtime/build.go index 0764eadce5..179e2d7323 100644 --- a/runtime/build.go +++ b/runtime/build.go @@ -82,9 +82,24 @@ func SourcePatchPkgPaths() []string { return paths } +func SourcePatchReplacesAsmForGOARCH(path, goarch string) bool { + goarchs, ok := sourcePatchAsmPkgs[path] + return ok && hasGoarch(goarchs, goarch) +} + var sourcePatchPkgs = map[string]struct{}{ "crypto/internal/constanttime": {}, + "internal/bytealg": {}, + "internal/chacha8rand": {}, + "internal/runtime/atomic": {}, "internal/sync": {}, "iter": {}, + "runtime": {}, "runtime/metrics": {}, } + +var sourcePatchAsmPkgs = map[string]map[string]struct{}{ + "internal/bytealg": {"wasm": {}}, + "internal/chacha8rand": {"wasm": {}}, + "internal/runtime/atomic": {"wasm": {}}, +}