diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index dadff854d0..fbb8bf92da 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -489,6 +489,14 @@ 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_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" + "$RUNNER_TEMP/runtime-wasip1-threads.wasm" \ + "$RUNNER_TEMP/wasm-gc-wasip1.wasm" diff --git a/internal/build/build.go b/internal/build/build.go index f0b08fd43d..a245fccbe2 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,9 @@ func Build(inv Invocation) ([]Package, error) { if conf.Target != "" && export.GOARCH != "" { conf.Goarch = export.GOARCH } + if err := configureWasmGC(conf, &export); err != nil { + return nil, err + } if conf.AppExt == "" { conf.AppExt = defaultAppExt(conf) } @@ -778,6 +782,36 @@ func defaultBuildTags(goarch, target string) string { return tags } +func configureWasmGC(conf *Config, export *crosscompile.Export) error { + if conf.Goarch != "wasm" || !hasBuildTag(conf.Tags, "llgo_wasm_gc") { + return nil + } + 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 { + 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..7f4dd4b936 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -322,6 +322,42 @@ func TestEffectiveWasmTypeSizes(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{} + 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) + } + }) + } +} + +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"} { @@ -329,7 +365,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 +392,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..f66fdd51fb --- /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() { + 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 := 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) + 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_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..e91c06c75b 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) || (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 b83b8569d6..8163f7ff10 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) || (wasm && llgo_wasm_gc) 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..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 +//go:build (baremetal && !nogc) || (wasm && llgo_wasm_gc) /* * 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/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_baremetal.go b/runtime/internal/runtime/z_defer_nonmoving.go similarity index 74% rename from runtime/internal/runtime/z_defer_baremetal.go rename to runtime/internal/runtime/z_defer_nonmoving.go index 0af31efef4..3142e3250c 100644 --- a/runtime/internal/runtime/z_defer_baremetal.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. @@ -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.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_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..5186cb7b62 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) || (wasm && llgo_wasm_gc) /* * 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() {} } 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.