From 0d684daf751bea8bbd6eb147b43245cc5a16b0f3 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 28 Jul 2026 21:23:24 +0800 Subject: [PATCH 1/8] runtime: isolate non-moving GC platform hooks --- ...c_baremetal.go => runtime_gc_nonmoving.go} | 5 +- runtime/internal/runtime/tinygogc/gc.go | 10 ++-- runtime/internal/runtime/tinygogc/gc_link.go | 40 ++++++++++++-- .../internal/runtime/tinygogc/gc_tinygo.go | 54 +++++++++++-------- ...efer_baremetal.go => z_defer_nonmoving.go} | 5 +- .../{z_gc_baremetal.go => z_gc_nonmoving.go} | 12 ++--- 6 files changed, 81 insertions(+), 45 deletions(-) rename runtime/internal/lib/runtime/{runtime_gc_baremetal.go => runtime_gc_nonmoving.go} (90%) rename runtime/internal/runtime/{z_defer_baremetal.go => z_defer_nonmoving.go} (77%) rename runtime/internal/runtime/{z_gc_baremetal.go => z_gc_nonmoving.go} (74%) diff --git a/runtime/internal/lib/runtime/runtime_gc_baremetal.go b/runtime/internal/lib/runtime/runtime_gc_nonmoving.go similarity index 90% rename from runtime/internal/lib/runtime/runtime_gc_baremetal.go rename to runtime/internal/lib/runtime/runtime_gc_nonmoving.go index f384b6d0f2..b12c146609 100644 --- a/runtime/internal/lib/runtime/runtime_gc_baremetal.go +++ b/runtime/internal/lib/runtime/runtime_gc_nonmoving.go @@ -1,4 +1,4 @@ -//go:build !nogc && baremetal +//go:build baremetal && !nogc package runtime @@ -9,6 +9,9 @@ import ( ) func ReadMemStats(m *runtime.MemStats) { + if m == nil { + return + } stats := tinygogc.ReadGCStats() m.Alloc = stats.Alloc m.TotalAlloc = stats.TotalAlloc diff --git a/runtime/internal/runtime/tinygogc/gc.go b/runtime/internal/runtime/tinygogc/gc.go index b83b8569d6..2af66f6b7b 100644 --- a/runtime/internal/runtime/tinygogc/gc.go +++ b/runtime/internal/runtime/tinygogc/gc.go @@ -1,9 +1,7 @@ -//go:build baremetal +//go:build baremetal && !nogc package tinygogc -import "unsafe" - type GCStats struct { // General statistics. @@ -150,6 +148,7 @@ func ReadGCStats() GCStats { var heapInuse, heapIdle uint64 lock(&gcMutex) + lazyInit() for block := uintptr(0); block < endBlock; block++ { bstate := gcStateOf(block) @@ -160,8 +159,7 @@ func ReadGCStats() GCStats { } } - stackEnd := uintptr(unsafe.Pointer(&_stackEnd)) - stackSys := stackTop - stackEnd + stackInuse, stackSys := gcStackStats() stats := GCStats{ Alloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock), @@ -173,7 +171,7 @@ func ReadGCStats() GCStats { HeapSys: heapInuse + heapIdle, HeapIdle: heapIdle, HeapInuse: heapInuse, - StackInuse: uint64(stackTop - uintptr(getsp())), + StackInuse: uint64(stackInuse), StackSys: uint64(stackSys), GCSys: uint64(heapEnd - uintptr(metadataStart)), } diff --git a/runtime/internal/runtime/tinygogc/gc_link.go b/runtime/internal/runtime/tinygogc/gc_link.go index 9921cd5c7b..550341e897 100644 --- a/runtime/internal/runtime/tinygogc/gc_link.go +++ b/runtime/internal/runtime/tinygogc/gc_link.go @@ -1,4 +1,4 @@ -//go:build baremetal +//go:build baremetal && !nogc package tinygogc @@ -32,9 +32,6 @@ func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { return Realloc(ptr, size) } -//go:linkname getsp llgo.stackSave -func getsp() unsafe.Pointer - //go:linkname _heapStart _heapStart var _heapStart [0]byte @@ -52,3 +49,38 @@ var _globals_start [0]byte //go:linkname _globals_end _globals_end var _globals_end [0]byte + +func gcMemoryLayout() (heapStart, heapEnd, globalsStart, globalsEnd, stackTop uintptr) { + // Reserve 2 KiB for libc internal allocation that cannot be wrapped. + return uintptr(unsafe.Pointer(&_heapStart)) + 2048, + uintptr(unsafe.Pointer(&_heapEnd)), + uintptr(unsafe.Pointer(&_globals_start)), + uintptr(unsafe.Pointer(&_globals_end)), + uintptr(unsafe.Pointer(&_stackStart)) +} + +func gcGrowMemory(oldHeapEnd uintptr) uintptr { + return oldHeapEnd +} + +func gcMarkReachable() { + sp := uintptr(getsp()) + if sp < stackTop { + markRoots(sp, stackTop) + } + if globalsStart < globalsEnd { + markRoots(globalsStart, globalsEnd) + } +} + +func gcStackStats() (inuse, sys uintptr) { + sp := uintptr(getsp()) + if sp < stackTop { + inuse = stackTop - sp + } + stackEnd := uintptr(unsafe.Pointer(&_stackEnd)) + if stackEnd < stackTop { + sys = stackTop - stackEnd + } + return +} diff --git a/runtime/internal/runtime/tinygogc/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go index cca6f9fccd..fef3866151 100644 --- a/runtime/internal/runtime/tinygogc/gc_tinygo.go +++ b/runtime/internal/runtime/tinygogc/gc_tinygo.go @@ -1,4 +1,4 @@ -//go:build baremetal +//go:build baremetal && !nogc /* * Copyright (c) 2018-2025 The TinyGo Authors. All rights reserved. @@ -18,14 +18,11 @@ */ // Package tinygogc implements a conservative mark-and-sweep garbage collector -// for baremetal environments where the standard Go runtime and bdwgc are unavailable. +// for targets where the standard Go runtime and bdwgc are unavailable. // // This implementation is based on TinyGo's GC and is designed for resource-constrained // embedded systems. It uses a block-based allocator with conservative pointer scanning. // -// Build tags: -// - baremetal: Enables this GC for baremetal targets -// // Memory Layout: // The heap is divided into fixed-size blocks (32 bytes on 64-bit). Metadata is stored // at the end of the heap, using 2 bits per block to track state (free/head/tail/mark). @@ -95,18 +92,20 @@ const ( // this function MUST be initalized first, which means it's required to be initalized before runtime func initGC() { - // reserve 2K blocks for libc internal malloc, we cannot wrap those internal functions - heapStart = uintptr(unsafe.Pointer(&_heapStart)) + 2048 - heapEnd = uintptr(unsafe.Pointer(&_heapEnd)) - globalsStart = uintptr(unsafe.Pointer(&_globals_start)) - globalsEnd = uintptr(unsafe.Pointer(&_globals_end)) + heapStart, heapEnd, globalsStart, globalsEnd, stackTop = gcMemoryLayout() + if heapStart >= heapEnd { + gcPanic(c.Str("gc: invalid heap range")) + } + configureHeap() + metadataSize := heapEnd - uintptr(metadataStart) + c.Memset(metadataStart, 0, metadataSize) +} + +func configureHeap() { totalSize := heapEnd - heapStart metadataSize := (totalSize + blocksPerStateByte*bytesPerBlock) / (1 + blocksPerStateByte*bytesPerBlock) metadataStart = unsafe.Pointer(heapEnd - metadataSize) endBlock = (uintptr(metadataStart) - heapStart) / bytesPerBlock - stackTop = uintptr(unsafe.Pointer(&_stackStart)) - - c.Memset(metadataStart, 0, metadataSize) } func lazyInit() { @@ -362,12 +361,12 @@ func Realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { newAlloc := Alloc(size) c.Memcpy(newAlloc, ptr, oldSize) - free(ptr) + freeObject(ptr) return newAlloc } -func free(ptr unsafe.Pointer) { +func freeObject(ptr unsafe.Pointer) { // TODO: free blocks on request, when the compiler knows they're unused. } @@ -555,15 +554,28 @@ func sweep() (freeBytes uintptr) { // growHeap tries to grow the heap size. It returns true if it succeeds, false // otherwise. func growHeap() bool { - // On baremetal, there is no way the heap can be grown. - return false -} + oldHeapEnd := heapEnd + oldMetadataStart := metadataStart + oldMetadataSize := oldHeapEnd - uintptr(oldMetadataStart) + newHeapEnd := gcGrowMemory(oldHeapEnd) + if newHeapEnd <= oldHeapEnd { + return false + } -func gcMarkReachable() { - markRoots(uintptr(getsp()), stackTop) - markRoots(globalsStart, globalsEnd) + heapEnd = newHeapEnd + configureHeap() + newMetadataSize := heapEnd - uintptr(metadataStart) + if newMetadataSize < oldMetadataSize { + gcPanic(c.Str("gc: metadata shrank while growing heap")) + } + c.Memmove(metadataStart, oldMetadataStart, oldMetadataSize) + c.Memset(unsafe.Add(metadataStart, oldMetadataSize), 0, newMetadataSize-oldMetadataSize) + return true } func gcResumeWorld() { // Nothing to do here (single threaded). } + +//go:linkname getsp llgo.stackSave +func getsp() unsafe.Pointer diff --git a/runtime/internal/runtime/z_defer_baremetal.go b/runtime/internal/runtime/z_defer_nonmoving.go similarity index 77% rename from runtime/internal/runtime/z_defer_baremetal.go rename to runtime/internal/runtime/z_defer_nonmoving.go index 0af31efef4..a6c9b8467c 100644 --- a/runtime/internal/runtime/z_defer_baremetal.go +++ b/runtime/internal/runtime/z_defer_nonmoving.go @@ -20,9 +20,6 @@ package runtime import "unsafe" -// FreeDeferNode is a no-op in baremetal environment. -// Defer nodes become unreachable after being unlinked from the chain, -// and tinygogc will reclaim them in the next GC cycle. +// FreeDeferNode leaves unreachable nodes for the collector. func FreeDeferNode(ptr unsafe.Pointer) { - // no-op: let tinygogc collect } diff --git a/runtime/internal/runtime/z_gc_baremetal.go b/runtime/internal/runtime/z_gc_nonmoving.go similarity index 74% rename from runtime/internal/runtime/z_gc_baremetal.go rename to runtime/internal/runtime/z_gc_nonmoving.go index 09fdb2a69d..b4de4952e3 100644 --- a/runtime/internal/runtime/z_gc_baremetal.go +++ b/runtime/internal/runtime/z_gc_nonmoving.go @@ -1,4 +1,4 @@ -//go:build !nogc && baremetal +//go:build baremetal && !nogc /* * Copyright (c) 2024 The XGo Authors (xgo.dev). All rights reserved. @@ -24,14 +24,12 @@ import ( "github.com/goplus/llgo/runtime/internal/runtime/tinygogc" ) -// AllocU allocates uninitialized memory. func AllocU(size uintptr) unsafe.Pointer { ret := tinygogc.Alloc(size) recordMemProfileAlloc(size) return ret } -// AllocZ allocates zero-initialized memory. func AllocZ(size uintptr) unsafe.Pointer { ret := tinygogc.Alloc(size) recordMemProfileAlloc(size) @@ -45,11 +43,7 @@ func AllocRoot(size uintptr) unsafe.Pointer { func FreeRoot(ptr unsafe.Pointer) { } -// AddCleanupPtr is not implemented in baremetal builds because tinygogc -// does not support finalizers. Cleanup functions will never be called. -// -// Returns: a no-op cancel function +// AddCleanupPtr is not implemented by the non-moving collector. func AddCleanupPtr(ptr unsafe.Pointer, cleanup func()) (cancel func()) { - // Not implemented: tinygogc does not support finalizers - return func() {} // no-op cancel + return func() {} } From 1685873c9ccc928567fc151c093432f1cfdb4931 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 28 Jul 2026 21:24:34 +0800 Subject: [PATCH 2/8] runtime/wasm: add opt-in non-moving collector --- internal/build/build.go | 22 +++++ internal/build/build_test.go | 27 +++++- internal/build/testdata/wasm-gc/abi.c | 23 +++++ internal/build/testdata/wasm-gc/abi.go | 8 ++ internal/build/testdata/wasm-gc/main.go | 89 +++++++++++++++++++ runtime/internal/clite/pthread/pthread_gc.go | 2 +- .../internal/clite/pthread/pthread_nogc.go | 2 +- runtime/internal/clite/tls/tls_gc.go | 2 +- runtime/internal/clite/tls/tls_nogc.go | 2 +- runtime/internal/lib/runtime/mfinal.go | 2 +- runtime/internal/lib/runtime/mfinal_nogc.go | 2 +- runtime/internal/lib/runtime/mfinal_wasm.go | 8 ++ runtime/internal/lib/runtime/runtime_gc.go | 2 +- .../lib/runtime/runtime_gc_nonmoving.go | 2 +- runtime/internal/lib/runtime/runtime_nogc.go | 2 +- .../internal/runtime/tinygogc/_wrap/gc_wasm.c | 53 +++++++++++ runtime/internal/runtime/tinygogc/gc.go | 2 +- .../internal/runtime/tinygogc/gc_tinygo.go | 2 +- runtime/internal/runtime/tinygogc/gc_wasm.go | 88 ++++++++++++++++++ .../internal/runtime/tinygogc/gc_wasm_js.go | 74 +++++++++++++++ .../runtime/tinygogc/gc_wasm_wasip1.go | 30 +++++++ runtime/internal/runtime/z_defer_gc.go | 2 +- runtime/internal/runtime/z_defer_nogc.go | 2 +- runtime/internal/runtime/z_defer_nonmoving.go | 2 +- runtime/internal/runtime/z_gc.go | 2 +- runtime/internal/runtime/z_gc_nonmoving.go | 2 +- runtime/internal/runtime/z_nogc.go | 3 +- 27 files changed, 437 insertions(+), 20 deletions(-) create mode 100644 internal/build/testdata/wasm-gc/abi.c create mode 100644 internal/build/testdata/wasm-gc/abi.go create mode 100644 internal/build/testdata/wasm-gc/main.go create mode 100644 runtime/internal/lib/runtime/mfinal_wasm.go create mode 100644 runtime/internal/runtime/tinygogc/_wrap/gc_wasm.c create mode 100644 runtime/internal/runtime/tinygogc/gc_wasm.go create mode 100644 runtime/internal/runtime/tinygogc/gc_wasm_js.go create mode 100644 runtime/internal/runtime/tinygogc/gc_wasm_wasip1.go diff --git a/internal/build/build.go b/internal/build/build.go index f0b08fd43d..80d61158fb 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -36,6 +36,7 @@ import ( "strings" "sync" "sync/atomic" + "unicode" "golang.org/x/tools/go/ssa" @@ -385,6 +386,7 @@ func Build(inv Invocation) ([]Package, error) { if conf.Target != "" && export.GOARCH != "" { conf.Goarch = export.GOARCH } + applyWasmGCLinkFlags(conf, &export) if conf.AppExt == "" { conf.AppExt = defaultAppExt(conf) } @@ -778,6 +780,26 @@ func defaultBuildTags(goarch, target string) string { return tags } +func applyWasmGCLinkFlags(conf *Config, export *crosscompile.Export) { + if conf.Goos != "js" || conf.Goarch != "wasm" || !hasBuildTag(conf.Tags, "llgo_wasm_gc") { + return + } + if !slices.Contains(export.LDFLAGS, "-sMALLOC=none") { + export.LDFLAGS = append(export.LDFLAGS, "-sMALLOC=none") + } +} + +func hasBuildTag(tags, want string) bool { + for _, tag := range strings.FieldsFunc(tags, func(r rune) bool { + return r == ',' || unicode.IsSpace(r) + }) { + if tag == want { + return true + } + } + return false +} + func effectiveTypeSizes(sizes types.Sizes, goos, goarch, target string) types.Sizes { // 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. diff --git a/internal/build/build_test.go b/internal/build/build_test.go index b33332928b..6ba3f18f68 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -322,6 +322,28 @@ func TestEffectiveWasmTypeSizes(t *testing.T) { } } +func TestApplyWasmGCLinkFlags(t *testing.T) { + tests := []struct { + name string + conf Config + want bool + }{ + {name: "wasm32", conf: Config{Goos: "js", Goarch: "wasm", Tags: "llgo_wasm_gc"}, want: true}, + {name: "comma separated tags", conf: Config{Goos: "js", Goarch: "wasm", Tags: "other,llgo_wasm_gc"}, want: true}, + {name: "default wasm", conf: Config{Goos: "js", Goarch: "wasm"}}, + {name: "WASI", conf: Config{Goos: "wasip1", Goarch: "wasm", Tags: "llgo_wasm_gc"}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + export := crosscompile.Export{} + applyWasmGCLinkFlags(&test.conf, &export) + if got := slices.Contains(export.LDFLAGS, "-sMALLOC=none"); got != test.want { + t.Fatalf("MALLOC=none present = %v, want %v", got, test.want) + } + }) + } +} + func TestWasmRuntimeAvoidsNativeHostDependencies(t *testing.T) { runtimeDir := filepath.Join(env.LLGoRuntimeDir(), "internal", "lib", "runtime") for _, goos := range []string{"js", "wasip1"} { @@ -329,7 +351,7 @@ func TestWasmRuntimeAvoidsNativeHostDependencies(t *testing.T) { ctx := gobuild.Default ctx.GOOS = goos ctx.GOARCH = "wasm" - ctx.BuildTags = []string{"llgo", "nogc"} + ctx.BuildTags = []string{"llgo", "nogc", "llgo_wasm_gc"} pkg, err := ctx.ImportDir(runtimeDir, 0) if err != nil { t.Fatal(err) @@ -356,8 +378,9 @@ func TestWasmRuntimeAvoidsNativeHostDependencies(t *testing.T) { } for _, name := range []string{ - "mfinal_nogc.go", + "mfinal_wasm.go", "runtime_baremetal.go", + "runtime_gc_nonmoving.go", "signal_baremetal_llgo.go", "time_wasm_llgo.go", "unwind_wasm_llgo.go", diff --git a/internal/build/testdata/wasm-gc/abi.c b/internal/build/testdata/wasm-gc/abi.c new file mode 100644 index 0000000000..998721d414 --- /dev/null +++ b/internal/build/testdata/wasm-gc/abi.c @@ -0,0 +1,23 @@ +#include +#include + +#if defined(__EMSCRIPTEN__) +#include +#endif + +int llgo_test_gc_aligned_alloc(void) { +#if defined(__EMSCRIPTEN__) + void *ptr = emscripten_builtin_memalign(65536, 257); + if (ptr == NULL || (uintptr_t)ptr % 65536 != 0) { + return 0; + } + unsigned char *bytes = ptr; + bytes[0] = 0x5a; + bytes[256] = 0xa5; + if (bytes[0] != 0x5a || bytes[256] != 0xa5) { + return 0; + } + emscripten_builtin_free(ptr); +#endif + return 1; +} diff --git a/internal/build/testdata/wasm-gc/abi.go b/internal/build/testdata/wasm-gc/abi.go new file mode 100644 index 0000000000..4eb18638a2 --- /dev/null +++ b/internal/build/testdata/wasm-gc/abi.go @@ -0,0 +1,8 @@ +package main + +import _ "unsafe" + +const LLGoFiles = "abi.c" + +//go:linkname testAlignedAlloc C.llgo_test_gc_aligned_alloc +func testAlignedAlloc() int32 diff --git a/internal/build/testdata/wasm-gc/main.go b/internal/build/testdata/wasm-gc/main.go new file mode 100644 index 0000000000..3167781a85 --- /dev/null +++ b/internal/build/testdata/wasm-gc/main.go @@ -0,0 +1,89 @@ +package main + +import "runtime" + +type payload struct { + value uint64 +} + +var ( + globalRoot *payload + garbage *payload + liveChunks [][]byte +) + +func main() { + runtime.ReadMemStats(nil) + if testAlignedAlloc() == 0 { + panic("aligned allocation failed") + } + testRoots() + testReclamation() + testHeapGrowth() + println("wasm gc ok") +} + +func testRoots() { + globalRoot = &payload{value: 0x12345678} + runtime.GC() + if globalRoot.value != 0x12345678 { + panic("global root was not retained") + } + globalRoot = nil +} + +//go:noinline +func allocateGarbage() { + for i := 0; i < 1024; i++ { + garbage = &payload{value: uint64(i)} + } + garbage = nil +} + +func testReclamation() { + runtime.GC() + var before runtime.MemStats + runtime.ReadMemStats(&before) + + allocateGarbage() + runtime.GC() + + var after runtime.MemStats + runtime.ReadMemStats(&after) + if after.TotalAlloc <= before.TotalAlloc || after.Mallocs <= before.Mallocs { + panic("allocation statistics did not advance") + } + if after.Frees <= before.Frees { + panic("unreachable objects were not reclaimed") + } +} + +func testHeapGrowth() { + var before runtime.MemStats + runtime.ReadMemStats(&before) + + const ( + chunkSize = 1 << 20 + chunkCount = 24 + ) + liveChunks = make([][]byte, 0, chunkCount) + for i := 0; i < chunkCount; i++ { + chunk := make([]byte, chunkSize) + chunk[0] = byte(i + 1) + chunk[len(chunk)-1] = byte(i + 2) + liveChunks = append(liveChunks, chunk) + } + + var after runtime.MemStats + runtime.ReadMemStats(&after) + if after.HeapSys <= before.HeapSys { + panic("heap did not grow") + } + runtime.GC() + for i, chunk := range liveChunks { + if chunk[0] != byte(i+1) || chunk[len(chunk)-1] != byte(i+2) { + panic("live object was corrupted during heap growth") + } + } + liveChunks = nil +} diff --git a/runtime/internal/clite/pthread/pthread_gc.go b/runtime/internal/clite/pthread/pthread_gc.go index 88409bc1c3..fdc067d2d2 100644 --- a/runtime/internal/clite/pthread/pthread_gc.go +++ b/runtime/internal/clite/pthread/pthread_gc.go @@ -1,4 +1,4 @@ -//go:build !nogc && !baremetal +//go:build !nogc && !baremetal && !wasm /* * Copyright (c) 2024 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/clite/pthread/pthread_nogc.go b/runtime/internal/clite/pthread/pthread_nogc.go index d61d39a3be..c72f1d597e 100644 --- a/runtime/internal/clite/pthread/pthread_nogc.go +++ b/runtime/internal/clite/pthread/pthread_nogc.go @@ -1,4 +1,4 @@ -//go:build nogc || baremetal +//go:build nogc || baremetal || wasm /* * Copyright (c) 2024 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/clite/tls/tls_gc.go b/runtime/internal/clite/tls/tls_gc.go index afd10494de..dee28069a9 100644 --- a/runtime/internal/clite/tls/tls_gc.go +++ b/runtime/internal/clite/tls/tls_gc.go @@ -1,4 +1,4 @@ -//go:build llgo && !baremetal && !nogc +//go:build llgo && !baremetal && !wasm && !nogc /* * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/clite/tls/tls_nogc.go b/runtime/internal/clite/tls/tls_nogc.go index 00923d4105..4e7283b41f 100644 --- a/runtime/internal/clite/tls/tls_nogc.go +++ b/runtime/internal/clite/tls/tls_nogc.go @@ -1,4 +1,4 @@ -//go:build llgo && (nogc || baremetal) +//go:build llgo && (nogc || baremetal || wasm) /* * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/lib/runtime/mfinal.go b/runtime/internal/lib/runtime/mfinal.go index a62305c8a2..70d43c918e 100644 --- a/runtime/internal/lib/runtime/mfinal.go +++ b/runtime/internal/lib/runtime/mfinal.go @@ -1,4 +1,4 @@ -//go:build !nogc +//go:build !nogc && !wasm // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style diff --git a/runtime/internal/lib/runtime/mfinal_nogc.go b/runtime/internal/lib/runtime/mfinal_nogc.go index c85111ea65..b4e55a948d 100644 --- a/runtime/internal/lib/runtime/mfinal_nogc.go +++ b/runtime/internal/lib/runtime/mfinal_nogc.go @@ -1,4 +1,4 @@ -//go:build nogc +//go:build nogc && (!wasm || !llgo_wasm_gc) package runtime diff --git a/runtime/internal/lib/runtime/mfinal_wasm.go b/runtime/internal/lib/runtime/mfinal_wasm.go new file mode 100644 index 0000000000..e18f6f6b7f --- /dev/null +++ b/runtime/internal/lib/runtime/mfinal_wasm.go @@ -0,0 +1,8 @@ +//go:build wasm && llgo_wasm_gc + +package runtime + +// SetFinalizer is not implemented by the initial WebAssembly collector. +func SetFinalizer(obj any, finalizer any) { + _, _ = obj, finalizer +} diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index d8656f93a4..7de1695009 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -1,4 +1,4 @@ -//go:build !nogc && !baremetal +//go:build !nogc && !baremetal && !wasm package runtime diff --git a/runtime/internal/lib/runtime/runtime_gc_nonmoving.go b/runtime/internal/lib/runtime/runtime_gc_nonmoving.go index b12c146609..a5c79953fd 100644 --- a/runtime/internal/lib/runtime/runtime_gc_nonmoving.go +++ b/runtime/internal/lib/runtime/runtime_gc_nonmoving.go @@ -1,4 +1,4 @@ -//go:build baremetal && !nogc +//go:build (baremetal && !nogc) || (wasm && llgo_wasm_gc) package runtime diff --git a/runtime/internal/lib/runtime/runtime_nogc.go b/runtime/internal/lib/runtime/runtime_nogc.go index 3f11426023..50c039b10f 100644 --- a/runtime/internal/lib/runtime/runtime_nogc.go +++ b/runtime/internal/lib/runtime/runtime_nogc.go @@ -1,4 +1,4 @@ -//go:build nogc +//go:build nogc && (!wasm || !llgo_wasm_gc) package runtime diff --git a/runtime/internal/runtime/tinygogc/_wrap/gc_wasm.c b/runtime/internal/runtime/tinygogc/_wrap/gc_wasm.c new file mode 100644 index 0000000000..d6ae8fd3d4 --- /dev/null +++ b/runtime/internal/runtime/tinygogc/_wrap/gc_wasm.c @@ -0,0 +1,53 @@ +#include +#include + +#if defined(__EMSCRIPTEN__) +#include +#include +#else +extern unsigned char __stack_high; +#endif + +extern unsigned char __data_end; +extern unsigned char __global_base; +extern unsigned char __heap_base; + +#define LLGO_WASM_PAGE_SIZE 65536 + +uintptr_t llgo_gc_globals_start(void) { + return (uintptr_t)&__global_base; +} + +uintptr_t llgo_gc_globals_end(void) { + return (uintptr_t)&__data_end; +} + +uintptr_t llgo_gc_heap_base(void) { + return (uintptr_t)&__heap_base; +} + +uintptr_t llgo_gc_stack_top(void) { +#if defined(__EMSCRIPTEN__) + return (uintptr_t)emscripten_stack_get_base(); +#else + return (uintptr_t)&__stack_high; +#endif +} + +uintptr_t llgo_gc_memory_size(void) { + return (uintptr_t)__builtin_wasm_memory_size(0) * LLGO_WASM_PAGE_SIZE; +} + +int llgo_gc_grow_memory(uintptr_t required) { +#if defined(__EMSCRIPTEN__) + return emscripten_resize_heap(required); +#else + uintptr_t current = llgo_gc_memory_size(); + if (required <= current) { + return 1; + } + uintptr_t pages = (required - current + LLGO_WASM_PAGE_SIZE - 1) / + LLGO_WASM_PAGE_SIZE; + return __builtin_wasm_memory_grow(0, pages) != (size_t)-1; +#endif +} diff --git a/runtime/internal/runtime/tinygogc/gc.go b/runtime/internal/runtime/tinygogc/gc.go index 2af66f6b7b..8163f7ff10 100644 --- a/runtime/internal/runtime/tinygogc/gc.go +++ b/runtime/internal/runtime/tinygogc/gc.go @@ -1,4 +1,4 @@ -//go:build baremetal && !nogc +//go:build (baremetal && !nogc) || (wasm && llgo_wasm_gc) package tinygogc diff --git a/runtime/internal/runtime/tinygogc/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go index fef3866151..65aceb822e 100644 --- a/runtime/internal/runtime/tinygogc/gc_tinygo.go +++ b/runtime/internal/runtime/tinygogc/gc_tinygo.go @@ -1,4 +1,4 @@ -//go:build baremetal && !nogc +//go:build (baremetal && !nogc) || (wasm && llgo_wasm_gc) /* * Copyright (c) 2018-2025 The TinyGo Authors. All rights reserved. diff --git a/runtime/internal/runtime/tinygogc/gc_wasm.go b/runtime/internal/runtime/tinygogc/gc_wasm.go new file mode 100644 index 0000000000..c1b7e6300a --- /dev/null +++ b/runtime/internal/runtime/tinygogc/gc_wasm.go @@ -0,0 +1,88 @@ +//go:build wasm && llgo_wasm_gc + +package tinygogc + +import ( + _ "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" +) + +const LLGoFiles = "_wrap/gc_wasm.c" + +const wasmPageSize = uintptr(64 << 10) + +func gcMemoryLayout() (heapStart, heapEnd, globalsStart, globalsEnd, stackTop uintptr) { + heapStart = alignUp(gcWasmHeapBase(), bytesPerBlock) + heapEnd = alignDown(gcWasmMemorySize(), bytesPerBlock) + globalsStart = gcWasmGlobalsStart() + globalsEnd = gcWasmGlobalsEnd() + stackTop = gcWasmStackTop() + return +} + +func gcGrowMemory(oldHeapEnd uintptr) uintptr { + current := gcWasmMemorySize() + if current < oldHeapEnd { + return oldHeapEnd + } + growth := oldHeapEnd - heapStart + if growth < 1<<20 { + growth = 1 << 20 + } + if current > ^uintptr(0)-growth { + return oldHeapEnd + } + required := alignUp(current+growth, wasmPageSize) + if gcWasmGrowMemory(required) == 0 { + return oldHeapEnd + } + return alignDown(gcWasmMemorySize(), bytesPerBlock) +} + +func gcMarkReachable() { + sp := uintptr(getsp()) + top := gcWasmStackTop() + if sp < top { + markRoots(sp, top) + } + if globalsStart < globalsEnd { + markRoots(globalsStart, globalsEnd) + } +} + +func gcStackStats() (inuse, sys uintptr) { + sp := uintptr(getsp()) + top := gcWasmStackTop() + if sp < top { + inuse = top - sp + sys = inuse + } + return +} + +func alignUp(value, alignment uintptr) uintptr { + return (value + alignment - 1) &^ (alignment - 1) +} + +func alignDown(value, alignment uintptr) uintptr { + return value &^ (alignment - 1) +} + +//go:linkname gcWasmGlobalsStart C.llgo_gc_globals_start +func gcWasmGlobalsStart() uintptr + +//go:linkname gcWasmGlobalsEnd C.llgo_gc_globals_end +func gcWasmGlobalsEnd() uintptr + +//go:linkname gcWasmHeapBase C.llgo_gc_heap_base +func gcWasmHeapBase() uintptr + +//go:linkname gcWasmStackTop C.llgo_gc_stack_top +func gcWasmStackTop() uintptr + +//go:linkname gcWasmMemorySize C.llgo_gc_memory_size +func gcWasmMemorySize() uintptr + +//go:linkname gcWasmGrowMemory C.llgo_gc_grow_memory +func gcWasmGrowMemory(required uintptr) c.Int diff --git a/runtime/internal/runtime/tinygogc/gc_wasm_js.go b/runtime/internal/runtime/tinygogc/gc_wasm_js.go new file mode 100644 index 0000000000..50731fff1f --- /dev/null +++ b/runtime/internal/runtime/tinygogc/gc_wasm_js.go @@ -0,0 +1,74 @@ +//go:build js && wasm && llgo_wasm_gc + +package tinygogc + +import "unsafe" + +func wasmCalloc(nmemb, size uintptr) unsafe.Pointer { + totalSize := nmemb * size + if nmemb != 0 && totalSize/nmemb != size { + return nil + } + return Alloc(totalSize) +} + +func wasmMemalign(alignment, size uintptr) unsafe.Pointer { + if alignment < unsafe.Sizeof(uintptr(0)) || alignment&(alignment-1) != 0 { + return nil + } + if alignment <= bytesPerBlock { + return Alloc(size) + } + if size > ^uintptr(0)-(alignment-1) { + return nil + } + return unsafe.Pointer(alignUp(uintptr(Alloc(size+alignment-1)), alignment)) +} + +//export malloc +func malloc(size uintptr) unsafe.Pointer { + return Alloc(size) +} + +//export free +func free(ptr unsafe.Pointer) { +} + +//export calloc +func calloc(nmemb, size uintptr) unsafe.Pointer { + return wasmCalloc(nmemb, size) +} + +//export realloc +func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + return Realloc(ptr, size) +} + +//export memalign +func memalign(alignment, size uintptr) unsafe.Pointer { + return wasmMemalign(alignment, size) +} + +//export emscripten_builtin_malloc +func emscripten_builtin_malloc(size uintptr) unsafe.Pointer { + return Alloc(size) +} + +//export emscripten_builtin_free +func emscripten_builtin_free(ptr unsafe.Pointer) { +} + +//export emscripten_builtin_calloc +func emscripten_builtin_calloc(nmemb, size uintptr) unsafe.Pointer { + return wasmCalloc(nmemb, size) +} + +//export emscripten_builtin_realloc +func emscripten_builtin_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + return Realloc(ptr, size) +} + +//export emscripten_builtin_memalign +func emscripten_builtin_memalign(alignment, size uintptr) unsafe.Pointer { + return wasmMemalign(alignment, size) +} diff --git a/runtime/internal/runtime/tinygogc/gc_wasm_wasip1.go b/runtime/internal/runtime/tinygogc/gc_wasm_wasip1.go new file mode 100644 index 0000000000..9dd5db8874 --- /dev/null +++ b/runtime/internal/runtime/tinygogc/gc_wasm_wasip1.go @@ -0,0 +1,30 @@ +//go:build wasip1 && wasm && llgo_wasm_gc + +package tinygogc + +import "unsafe" + +const LLGoPackage = "link: -Wl,--wrap=malloc -Wl,--wrap=free -Wl,--wrap=realloc -Wl,--wrap=calloc" + +//export __wrap_malloc +func __wrap_malloc(size uintptr) unsafe.Pointer { + return Alloc(size) +} + +//export __wrap_free +func __wrap_free(ptr unsafe.Pointer) { +} + +//export __wrap_calloc +func __wrap_calloc(nmemb, size uintptr) unsafe.Pointer { + totalSize := nmemb * size + if nmemb != 0 && totalSize/nmemb != size { + return nil + } + return Alloc(totalSize) +} + +//export __wrap_realloc +func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + return Realloc(ptr, size) +} diff --git a/runtime/internal/runtime/z_defer_gc.go b/runtime/internal/runtime/z_defer_gc.go index 88ee5eeae2..2739768e08 100644 --- a/runtime/internal/runtime/z_defer_gc.go +++ b/runtime/internal/runtime/z_defer_gc.go @@ -1,4 +1,4 @@ -//go:build !nogc && !baremetal +//go:build !nogc && !baremetal && !wasm /* * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/runtime/z_defer_nogc.go b/runtime/internal/runtime/z_defer_nogc.go index e7acb7bc6d..b16c883dce 100644 --- a/runtime/internal/runtime/z_defer_nogc.go +++ b/runtime/internal/runtime/z_defer_nogc.go @@ -1,4 +1,4 @@ -//go:build nogc && !baremetal +//go:build nogc && !baremetal && (!wasm || !llgo_wasm_gc) /* * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/runtime/z_defer_nonmoving.go b/runtime/internal/runtime/z_defer_nonmoving.go index a6c9b8467c..3142e3250c 100644 --- a/runtime/internal/runtime/z_defer_nonmoving.go +++ b/runtime/internal/runtime/z_defer_nonmoving.go @@ -1,4 +1,4 @@ -//go:build baremetal && !nogc +//go:build (baremetal && !nogc) || (wasm && llgo_wasm_gc) /* * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/runtime/z_gc.go b/runtime/internal/runtime/z_gc.go index 4b4d144678..32ba207edc 100644 --- a/runtime/internal/runtime/z_gc.go +++ b/runtime/internal/runtime/z_gc.go @@ -1,4 +1,4 @@ -//go:build !nogc && !baremetal +//go:build !nogc && !baremetal && !wasm /* * Copyright (c) 2024 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/runtime/z_gc_nonmoving.go b/runtime/internal/runtime/z_gc_nonmoving.go index b4de4952e3..5186cb7b62 100644 --- a/runtime/internal/runtime/z_gc_nonmoving.go +++ b/runtime/internal/runtime/z_gc_nonmoving.go @@ -1,4 +1,4 @@ -//go:build baremetal && !nogc +//go:build (baremetal && !nogc) || (wasm && llgo_wasm_gc) /* * Copyright (c) 2024 The XGo Authors (xgo.dev). All rights reserved. diff --git a/runtime/internal/runtime/z_nogc.go b/runtime/internal/runtime/z_nogc.go index e716ec1e94..2300034d4b 100644 --- a/runtime/internal/runtime/z_nogc.go +++ b/runtime/internal/runtime/z_nogc.go @@ -1,5 +1,4 @@ -//go:build nogc -// +build nogc +//go:build nogc && (!wasm || !llgo_wasm_gc) /* * Copyright (c) 2024 The XGo Authors (xgo.dev). All rights reserved. From b6797da9cdb85c2d279d791c9ddfc082bc464ea2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 28 Jul 2026 21:24:40 +0800 Subject: [PATCH 3/8] ci: exercise opt-in wasm collector --- .github/workflows/llgo.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index dadff854d0..9bb76e6918 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -489,6 +489,12 @@ jobs: run_wasm_scheduler "$RUNNER_TEMP/wasm-scheduler.mjs" 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" + GOOS=js GOARCH=wasm llgo build -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc-go.mjs" ./internal/build/testdata/wasm-gc + node --input-type=module -e "import Module from '$RUNNER_TEMP/wasm-gc-go.mjs'; await Module();" + llgo build -target wasm -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc.mjs" ./internal/build/testdata/wasm-gc + node --input-type=module -e "import Module from '$RUNNER_TEMP/wasm-gc.mjs'; await Module();" + GOOS=wasip1 GOARCH=wasm llgo build -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc-wasip1.wasm" ./internal/build/testdata/wasm-gc file "$RUNNER_TEMP/runtime-js.wasm" \ "$RUNNER_TEMP/runtime-wasip1.wasm" \ - "$RUNNER_TEMP/runtime-wasip1-threads.wasm" + "$RUNNER_TEMP/runtime-wasip1-threads.wasm" \ + "$RUNNER_TEMP/wasm-gc-wasip1.wasm" From 0c911808a6d686c3623f89de7e31dfe37aad3a1c Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 28 Jul 2026 21:36:43 +0800 Subject: [PATCH 4/8] test: size wasm heap growth probe dynamically --- internal/build/testdata/wasm-gc/main.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/build/testdata/wasm-gc/main.go b/internal/build/testdata/wasm-gc/main.go index 3167781a85..d7062bcf25 100644 --- a/internal/build/testdata/wasm-gc/main.go +++ b/internal/build/testdata/wasm-gc/main.go @@ -62,10 +62,11 @@ func testHeapGrowth() { var before runtime.MemStats runtime.ReadMemStats(&before) - const ( - chunkSize = 1 << 20 - chunkCount = 24 - ) + const chunkSize = 1 << 20 + chunkCount := int(before.HeapSys/chunkSize) + 8 + if chunkCount > 128 { + panic("initial heap is too large for the bounded growth test") + } liveChunks = make([][]byte, 0, chunkCount) for i := 0; i < chunkCount; i++ { chunk := make([]byte, chunkSize) From 40996b834377f3855e7c205e8487d676c1f39dd9 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 28 Jul 2026 22:26:44 +0800 Subject: [PATCH 5/8] build/wasm: reject GC with threaded WASI --- internal/build/build.go | 24 ++++++++++++++++++------ internal/build/build_test.go | 18 ++++++++++++++++-- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 80d61158fb..a245fccbe2 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -386,7 +386,9 @@ func Build(inv Invocation) ([]Package, error) { if conf.Target != "" && export.GOARCH != "" { conf.Goarch = export.GOARCH } - applyWasmGCLinkFlags(conf, &export) + if err := configureWasmGC(conf, &export); err != nil { + return nil, err + } if conf.AppExt == "" { conf.AppExt = defaultAppExt(conf) } @@ -780,13 +782,23 @@ func defaultBuildTags(goarch, target string) string { return tags } -func applyWasmGCLinkFlags(conf *Config, export *crosscompile.Export) { - if conf.Goos != "js" || conf.Goarch != "wasm" || !hasBuildTag(conf.Tags, "llgo_wasm_gc") { - return +func configureWasmGC(conf *Config, export *crosscompile.Export) error { + if conf.Goarch != "wasm" || !hasBuildTag(conf.Tags, "llgo_wasm_gc") { + return nil } - if !slices.Contains(export.LDFLAGS, "-sMALLOC=none") { - export.LDFLAGS = append(export.LDFLAGS, "-sMALLOC=none") + switch conf.Goos { + case "js": + if !slices.Contains(export.LDFLAGS, "-sMALLOC=none") { + export.LDFLAGS = append(export.LDFLAGS, "-sMALLOC=none") + } + case "wasip1": + if IsWasiThreadsEnabled() { + return errors.New("llgo_wasm_gc requires single-worker WASI (set LLGO_WASI_THREADS=0)") + } + default: + return fmt.Errorf("llgo_wasm_gc does not support GOOS=%s", conf.Goos) } + return nil } func hasBuildTag(tags, want string) bool { diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 6ba3f18f68..7f4dd4b936 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -322,21 +322,27 @@ func TestEffectiveWasmTypeSizes(t *testing.T) { } } -func TestApplyWasmGCLinkFlags(t *testing.T) { +func TestConfigureWasmGC(t *testing.T) { + t.Setenv("LLGO_WASI_THREADS", "0") tests := []struct { name string conf Config want bool + err bool }{ {name: "wasm32", conf: Config{Goos: "js", Goarch: "wasm", Tags: "llgo_wasm_gc"}, want: true}, {name: "comma separated tags", conf: Config{Goos: "js", Goarch: "wasm", Tags: "other,llgo_wasm_gc"}, want: true}, {name: "default wasm", conf: Config{Goos: "js", Goarch: "wasm"}}, {name: "WASI", conf: Config{Goos: "wasip1", Goarch: "wasm", Tags: "llgo_wasm_gc"}}, + {name: "unsupported host", conf: Config{Goos: "linux", Goarch: "wasm", Tags: "llgo_wasm_gc"}, err: true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { export := crosscompile.Export{} - applyWasmGCLinkFlags(&test.conf, &export) + err := configureWasmGC(&test.conf, &export) + if (err != nil) != test.err { + t.Fatalf("configureWasmGC error = %v, want error %v", err, test.err) + } if got := slices.Contains(export.LDFLAGS, "-sMALLOC=none"); got != test.want { t.Fatalf("MALLOC=none present = %v, want %v", got, test.want) } @@ -344,6 +350,14 @@ func TestApplyWasmGCLinkFlags(t *testing.T) { } } +func TestConfigureWasmGCRejectsWASIThreads(t *testing.T) { + t.Setenv("LLGO_WASI_THREADS", "1") + conf := Config{Goos: "wasip1", Goarch: "wasm", Tags: "llgo_wasm_gc"} + if err := configureWasmGC(&conf, &crosscompile.Export{}); err == nil { + t.Fatal("expected llgo_wasm_gc with WASI threads to fail") + } +} + func TestWasmRuntimeAvoidsNativeHostDependencies(t *testing.T) { runtimeDir := filepath.Join(env.LLGoRuntimeDir(), "internal", "lib", "runtime") for _, goos := range []string{"js", "wasip1"} { From 44bc2f8b896c4874d4271523d2975928f7dd18c2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 28 Jul 2026 22:29:36 +0800 Subject: [PATCH 6/8] ci: select single-worker P1 for wasm GC --- .github/workflows/llgo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 9bb76e6918..cf6475e78d 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -493,7 +493,7 @@ jobs: node --input-type=module -e "import Module from '$RUNNER_TEMP/wasm-gc-go.mjs'; await Module();" llgo build -target wasm -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc.mjs" ./internal/build/testdata/wasm-gc node --input-type=module -e "import Module from '$RUNNER_TEMP/wasm-gc.mjs'; await Module();" - GOOS=wasip1 GOARCH=wasm llgo build -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc-wasip1.wasm" ./internal/build/testdata/wasm-gc + GOOS=wasip1 GOARCH=wasm LLGO_WASI_THREADS=0 llgo build -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc-wasip1.wasm" ./internal/build/testdata/wasm-gc file "$RUNNER_TEMP/runtime-js.wasm" \ "$RUNNER_TEMP/runtime-wasip1.wasm" \ "$RUNNER_TEMP/runtime-wasip1-threads.wasm" \ From 39bf51d6fb7b3f4908a4a0665f88391ccc106d9f Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 28 Jul 2026 22:47:13 +0800 Subject: [PATCH 7/8] runtime: preserve ReadMemStats nil panic --- internal/build/testdata/wasm-gc/main.go | 1 - runtime/internal/lib/runtime/runtime_gc_nonmoving.go | 3 --- 2 files changed, 4 deletions(-) diff --git a/internal/build/testdata/wasm-gc/main.go b/internal/build/testdata/wasm-gc/main.go index d7062bcf25..f66fdd51fb 100644 --- a/internal/build/testdata/wasm-gc/main.go +++ b/internal/build/testdata/wasm-gc/main.go @@ -13,7 +13,6 @@ var ( ) func main() { - runtime.ReadMemStats(nil) if testAlignedAlloc() == 0 { panic("aligned allocation failed") } diff --git a/runtime/internal/lib/runtime/runtime_gc_nonmoving.go b/runtime/internal/lib/runtime/runtime_gc_nonmoving.go index a5c79953fd..e91c06c75b 100644 --- a/runtime/internal/lib/runtime/runtime_gc_nonmoving.go +++ b/runtime/internal/lib/runtime/runtime_gc_nonmoving.go @@ -9,9 +9,6 @@ import ( ) func ReadMemStats(m *runtime.MemStats) { - if m == nil { - return - } stats := tinygogc.ReadGCStats() m.Alloc = stats.Alloc m.TotalAlloc = stats.TotalAlloc From 0fa359652b08c9225bb20e31a17777ec0f9d7e4b Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 1 Aug 2026 18:55:57 +0800 Subject: [PATCH 8/8] ci: execute WASI collector fixture --- .github/workflows/llgo.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index cf6475e78d..fbb8bf92da 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -494,6 +494,8 @@ jobs: llgo build -target wasm -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc.mjs" ./internal/build/testdata/wasm-gc node --input-type=module -e "import Module from '$RUNNER_TEMP/wasm-gc.mjs'; await Module();" GOOS=wasip1 GOARCH=wasm LLGO_WASI_THREADS=0 llgo build -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc-wasip1.wasm" ./internal/build/testdata/wasm-gc + wasm-tools validate --features all "$RUNNER_TEMP/wasm-gc-wasip1.wasm" + test "$(wasmtime run -W exceptions=y "$RUNNER_TEMP/wasm-gc-wasip1.wasm" 2>&1)" = "wasm gc ok" file "$RUNNER_TEMP/runtime-js.wasm" \ "$RUNNER_TEMP/runtime-wasip1.wasm" \ "$RUNNER_TEMP/runtime-wasip1-threads.wasm" \