From 812dfd45c9c77303fe4958a43477a023b397d565 Mon Sep 17 00:00:00 2001 From: Haolan Date: Tue, 16 Sep 2025 09:39:23 +0800 Subject: [PATCH 01/15] feat: implement baremetal GC fix: pthread gc fix: xiao-esp32c3 symbol refactor: use clite memset instead of linking fix: stack top symbol --- internal/build/build.go | 6 +- runtime/internal/clite/bdwgc/bdwgc.go | 5 + runtime/internal/clite/pthread/pthread_gc.go | 3 +- .../internal/clite/pthread/pthread_nogc.go | 3 +- runtime/internal/lib/runtime/runtime_gc.go | 2 +- .../lib/runtime/runtime_gc_baremetal.go | 9 + runtime/internal/runtime/gc_tinygo.go | 537 ++++++++++++++++++ runtime/internal/runtime/tinygogc/gc.go | 9 + .../runtime/tinygogc/memory/memory.go | 71 +++ runtime/internal/runtime/z_gc.go | 3 +- runtime/internal/runtime/z_gc_baremetal.go | 36 ++ targets/esp32-riscv.app.elf.ld | 19 +- targets/esp32.app.elf.ld | 18 +- targets/esp32.memory.elf.ld | 4 +- 14 files changed, 707 insertions(+), 18 deletions(-) create mode 100644 runtime/internal/lib/runtime/runtime_gc_baremetal.go create mode 100644 runtime/internal/runtime/gc_tinygo.go create mode 100644 runtime/internal/runtime/tinygogc/gc.go create mode 100644 runtime/internal/runtime/tinygogc/memory/memory.go create mode 100644 runtime/internal/runtime/z_gc_baremetal.go diff --git a/internal/build/build.go b/internal/build/build.go index 1147d495..b94e88e0 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -753,7 +753,6 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { } func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global llssa.Package, outputPath string, verbose bool) error { - needRuntime := false needPyInit := false pkgsMap := make(map[*packages.Package]*aPackage, len(pkgs)) @@ -998,6 +997,10 @@ define weak void @runtime.init() { ret void } +define weak void @initGC() { + ret void +} + ; TODO(lijie): workaround for syscall patch define weak void @"syscall.init"() { ret void @@ -1009,6 +1012,7 @@ define weak void @"syscall.init"() { _llgo_0: store i32 %%0, ptr @__llgo_argc, align 4 store ptr %%1, ptr @__llgo_argv, align 8 + call void @initGC() %s %s %s diff --git a/runtime/internal/clite/bdwgc/bdwgc.go b/runtime/internal/clite/bdwgc/bdwgc.go index 8f0af818..83bf5f91 100644 --- a/runtime/internal/clite/bdwgc/bdwgc.go +++ b/runtime/internal/clite/bdwgc/bdwgc.go @@ -26,6 +26,11 @@ const ( LLGoPackage = "link: $(pkg-config --libs bdw-gc); -lgc" ) +//export initGC +func initGC() { + Init() +} + // ----------------------------------------------------------------------------- //go:linkname Init C.GC_init diff --git a/runtime/internal/clite/pthread/pthread_gc.go b/runtime/internal/clite/pthread/pthread_gc.go index 32c7823e..4d33b5a2 100644 --- a/runtime/internal/clite/pthread/pthread_gc.go +++ b/runtime/internal/clite/pthread/pthread_gc.go @@ -1,5 +1,4 @@ -//go:build !nogc -// +build !nogc +//go:build !nogc && !baremetal /* * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/clite/pthread/pthread_nogc.go b/runtime/internal/clite/pthread/pthread_nogc.go index 594e1b97..09acb0ac 100644 --- a/runtime/internal/clite/pthread/pthread_nogc.go +++ b/runtime/internal/clite/pthread/pthread_nogc.go @@ -1,5 +1,4 @@ -//go:build nogc -// +build nogc +//go:build nogc || baremetal /* * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index a80d00b9..a6d76401 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -1,4 +1,4 @@ -//go:build !nogc +//go:build !nogc && !baremetal package runtime diff --git a/runtime/internal/lib/runtime/runtime_gc_baremetal.go b/runtime/internal/lib/runtime/runtime_gc_baremetal.go new file mode 100644 index 00000000..b6192a44 --- /dev/null +++ b/runtime/internal/lib/runtime/runtime_gc_baremetal.go @@ -0,0 +1,9 @@ +//go:build !nogc && baremetal + +package runtime + +import "github.com/goplus/llgo/runtime/internal/runtime/tinygogc" + +func GC() { + tinygogc.GC() +} diff --git a/runtime/internal/runtime/gc_tinygo.go b/runtime/internal/runtime/gc_tinygo.go new file mode 100644 index 00000000..9962337c --- /dev/null +++ b/runtime/internal/runtime/gc_tinygo.go @@ -0,0 +1,537 @@ +//go:build baremetal + +/* + * Copyright (c) 2018-2025 The TinyGo Authors. All rights reserved. + * Copyright (c) 2024 The GoPlus Authors (goplus.org). 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" + _ "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + "github.com/goplus/llgo/runtime/internal/runtime/tinygogc/memory" +) + +const gcDebug = false +const needsStaticHeap = true + +// Some globals + constants for the entire GC. + +const ( + wordsPerBlock = 4 // number of pointers in an allocated block + bytesPerBlock = wordsPerBlock * unsafe.Sizeof(memory.HeapStart) + stateBits = 2 // how many bits a block state takes (see blockState type) + blocksPerStateByte = 8 / stateBits + markStackSize = 8 * unsafe.Sizeof((*int)(nil)) // number of to-be-marked blocks to queue before forcing a rescan +) + +// Provide some abc.Straction over heap blocks. + +// blockState stores the four states in which a block can be. It is two bits in +// size. + +const ( + blockStateFree uint8 = 0 // 00 + blockStateHead uint8 = 1 // 01 + blockStateTail uint8 = 2 // 10 + blockStateMark uint8 = 3 // 11 + blockStateMask uint8 = 3 // 11 +) + +//go:linkname getsp llgo.stackSave +func getsp() unsafe.Pointer + +func printlnAndPanic(c string) { + println(c) + panic("") +} + +var ( + nextAlloc uintptr // the next block that should be tried by the allocator + endBlock uintptr // the block just past the end of the available space + gcTotalAlloc uint64 // total number of bytes allocated + gcTotalBlocks uint64 // total number of allocated blocks + gcMallocs uint64 // total number of allocations + gcFrees uint64 // total number of objects freed + gcFreedBlocks uint64 // total number of freed blocks + + // stackOverflow is a flag which is set when the GC scans too deep while marking. + // After it is set, all marked allocations must be re-scanned. + markStackOverflow bool + + // zeroSizedAlloc is just a sentinel that gets returned when allocating 0 bytes. + zeroSizedAlloc uint8 +) + +// blockState stores the four states in which a block can be. It is two bits in +// size. +type blockState uint8 + +// The byte value of a block where every block is a 'tail' block. +const blockStateByteAllTails = 0 | + uint8(blockStateTail<<(stateBits*3)) | + uint8(blockStateTail<<(stateBits*2)) | + uint8(blockStateTail<<(stateBits*1)) | + uint8(blockStateTail<<(stateBits*0)) + +// blockFromAddr returns a block given an address somewhere in the heap (which +// might not be heap-aligned). +func blockFromAddr(addr uintptr) uintptr { + if addr < memory.HeapStart || addr >= uintptr(memory.MetadataStart) { + printlnAndPanic("gc: trying to get block from invalid address") + } + return (addr - memory.HeapStart) / bytesPerBlock +} + +// Return a pointer to the start of the allocated object. +func gcPointerOf(blockAddr uintptr) unsafe.Pointer { + return unsafe.Pointer(gcAddressOf(blockAddr)) +} + +// Return the address of the start of the allocated object. +func gcAddressOf(blockAddr uintptr) uintptr { + addr := memory.HeapStart + blockAddr*bytesPerBlock + if addr > uintptr(memory.MetadataStart) { + printlnAndPanic("gc: block pointing inside metadata") + } + return addr +} + +// findHead returns the head (first block) of an object, assuming the block +// points to an allocated object. It returns the same block if this block +// already points to the head. +func gcFindHead(blockAddr uintptr) uintptr { + for { + // Optimization: check whether the current block state byte (which + // contains the state of multiple blocks) is composed entirely of tail + // blocks. If so, we can skip back to the last block in the previous + // state byte. + // This optimization speeds up findHead for pointers that point into a + // large allocation. + stateByte := gcStateByteOf(blockAddr) + if stateByte == blockStateByteAllTails { + blockAddr -= (blockAddr % blocksPerStateByte) + 1 + continue + } + + // Check whether we've found a non-tail block, which means we found the + // head. + state := gcStateFromByte(blockAddr, stateByte) + if state != blockStateTail { + break + } + blockAddr-- + } + if gcStateOf(blockAddr) != blockStateHead && gcStateOf(blockAddr) != blockStateMark { + printlnAndPanic("gc: found tail without head") + } + return blockAddr +} + +// findNext returns the first block just past the end of the tail. This may or +// may not be the head of an object. +func gcFindNext(blockAddr uintptr) uintptr { + if gcStateOf(blockAddr) == blockStateHead || gcStateOf(blockAddr) == blockStateMark { + blockAddr++ + } + for gcAddressOf(blockAddr) < uintptr(memory.MetadataStart) && gcStateOf(blockAddr) == blockStateTail { + blockAddr++ + } + return blockAddr +} + +func gcStateByteOf(blockAddr uintptr) byte { + return *(*uint8)(unsafe.Add(memory.MetadataStart, blockAddr/blocksPerStateByte)) +} + +// Return the block state given a state byte. The state byte must have been +// obtained using b.stateByte(), otherwise the result is incorrect. +func gcStateFromByte(blockAddr uintptr, stateByte byte) uint8 { + return uint8(stateByte>>((blockAddr%blocksPerStateByte)*stateBits)) & blockStateMask +} + +// State returns the current block state. +func gcStateOf(blockAddr uintptr) uint8 { + return gcStateFromByte(blockAddr, gcStateByteOf(blockAddr)) +} + +// setState sets the current block to the given state, which must contain more +// bits than the current state. Allowed transitions: from free to any state and +// from head to mark. +func gcSetState(blockAddr uintptr, newState uint8) { + stateBytePtr := (*uint8)(unsafe.Add(memory.MetadataStart, blockAddr/blocksPerStateByte)) + *stateBytePtr |= uint8(newState << ((blockAddr % blocksPerStateByte) * stateBits)) + if gcStateOf(blockAddr) != newState { + printlnAndPanic("gc: setState() was not successful") + } +} + +// markFree sets the block state to free, no matter what state it was in before. +func gcMarkFree(blockAddr uintptr) { + stateBytePtr := (*uint8)(unsafe.Add(memory.MetadataStart, blockAddr/blocksPerStateByte)) + *stateBytePtr &^= uint8(blockStateMask << ((blockAddr % blocksPerStateByte) * stateBits)) + if gcStateOf(blockAddr) != blockStateFree { + printlnAndPanic("gc: markFree() was not successful") + } + *(*[wordsPerBlock]uintptr)(unsafe.Pointer(gcAddressOf(blockAddr))) = [wordsPerBlock]uintptr{} +} + +// unmark changes the state of the block from mark to head. It must be marked +// before calling this function. +func gcUnmark(blockAddr uintptr) { + if gcStateOf(blockAddr) != blockStateMark { + printlnAndPanic("gc: unmark() on a block that is not marked") + } + clearMask := blockStateMask ^ blockStateHead // the bits to clear from the state + stateBytePtr := (*uint8)(unsafe.Add(memory.MetadataStart, blockAddr/blocksPerStateByte)) + *stateBytePtr &^= uint8(clearMask << ((blockAddr % blocksPerStateByte) * stateBits)) + if gcStateOf(blockAddr) != blockStateHead { + printlnAndPanic("gc: unmark() was not successful") + } +} + +func isOnHeap(ptr uintptr) bool { + return ptr >= memory.HeapStart && ptr < uintptr(memory.MetadataStart) +} + +// alloc tries to find some free space on the heap, possibly doing a garbage +// collection cycle if needed. If no space is free, it panics. +// +//go:noinline +func alloc(size uintptr) unsafe.Pointer { + if size == 0 { + return unsafe.Pointer(&zeroSizedAlloc) + } + gcTotalAlloc += uint64(size) + gcMallocs++ + + neededBlocks := (size + (bytesPerBlock - 1)) / bytesPerBlock + gcTotalBlocks += uint64(neededBlocks) + + // Continue looping until a run of free blocks has been found that fits the + // requested size. + index := nextAlloc + numFreeBlocks := uintptr(0) + heapScanCount := uint8(0) + for { + if index == nextAlloc { + if heapScanCount == 0 { + heapScanCount = 1 + } else if heapScanCount == 1 { + // The entire heap has been searched for free memory, but none + // could be found. Run a garbage collection cycle to reclaim + // free memory and try again. + heapScanCount = 2 + freeBytes := GC() + heapSize := uintptr(memory.MetadataStart) - memory.HeapStart + if freeBytes < heapSize/3 { + // Ensure there is at least 33% headroom. + // This percentage was arbitrarily chosen, and may need to + // be tuned in the future. + growHeap() + } + } else { + // Even after garbage collection, no free memory could be found. + // Try to increase heap size. + if growHeap() { + // Success, the heap was increased in size. Try again with a + // larger heap. + } else { + // Unfortunately the heap could not be increased. This + // happens on baremetal systems for example (where all + // available RAM has already been dedicated to the heap). + printlnAndPanic("out of memory") + } + } + } + + // Wrap around the end of the heap. + if index == memory.EndBlock { + index = 0 + // Reset numFreeBlocks as allocations cannot wrap. + numFreeBlocks = 0 + // In rare cases, the initial heap might be so small that there are + // no blocks at all. In this case, it's better to jump back to the + // start of the loop and try again, until the GC realizes there is + // no memory and grows the heap. + // This can sometimes happen on WebAssembly, where the initial heap + // is created by whatever is left on the last memory page. + continue + } + + // Is the block we're looking at free? + if gcStateOf(index) != blockStateFree { + // This block is in use. Try again from this point. + numFreeBlocks = 0 + index++ + continue + } + numFreeBlocks++ + index++ + + // Are we finished? + if numFreeBlocks == neededBlocks { + // Found a big enough range of free blocks! + nextAlloc = index + thisAlloc := index - neededBlocks + + // Set the following blocks as being allocated. + gcSetState(thisAlloc, blockStateHead) + for i := thisAlloc + 1; i != nextAlloc; i++ { + gcSetState(i, blockStateTail) + } + + // Return a pointer to this allocation. + return gcPointerOf(thisAlloc) + } + } +} + +func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + if ptr == nil { + return alloc(size) + } + + ptrAddress := uintptr(ptr) + endOfTailAddress := gcAddressOf(gcFindNext(blockFromAddr(ptrAddress))) + + // this might be a few bytes longer than the original size of + // ptr, because we align to full blocks of size bytesPerBlock + oldSize := endOfTailAddress - ptrAddress + if size <= oldSize { + return ptr + } + + newAlloc := alloc(size) + c.Memcpy(newAlloc, ptr, oldSize) + free(ptr) + + return newAlloc +} + +func free(ptr unsafe.Pointer) { + // TODO: free blocks on request, when the compiler knows they're unused. +} + +// runGC performs a garbage collection cycle. It is the internal implementation +// of the runtime.GC() function. The difference is that it returns the number of +// free bytes in the heap after the GC is finished. +func GC() (freeBytes uintptr) { + if gcDebug { + println("running collection cycle...") + } + + // Mark phase: mark all reachable objects, recursively. + gcMarkReachable() + + finishMark() + + // If we're using threads, resume all other threads before starting the + // sweep. + gcResumeWorld() + + // Sweep phase: free all non-marked objects and unmark marked objects for + // the next collection cycle. + freeBytes = sweep() + + return +} + +// markRoots reads all pointers from start to end (exclusive) and if they look +// like a heap pointer and are unmarked, marks them and scans that object as +// well (recursively). The start and end parameters must be valid pointers and +// must be aligned. +func markRoots(start, end uintptr) { + + if true { + if start >= end { + printlnAndPanic("gc: unexpected range to mark") + } + if start%unsafe.Alignof(start) != 0 { + printlnAndPanic("gc: unaligned start pointer") + } + if end%unsafe.Alignof(end) != 0 { + printlnAndPanic("gc: unaligned end pointer") + } + } + + // Reduce the end bound to avoid reading too far on platforms where pointer alignment is smaller than pointer size. + // If the size of the range is 0, then end will be slightly below start after this. + end -= unsafe.Sizeof(end) - unsafe.Alignof(end) + + for addr := start; addr < end; addr += unsafe.Alignof(addr) { + root := *(*uintptr)(unsafe.Pointer(addr)) + markRoot(addr, root) + } +} + +// startMark starts the marking process on a root and all of its children. +func startMark(root uintptr) { + var stack [markStackSize]uintptr + stack[0] = root + gcSetState(root, blockStateMark) + stackLen := 1 + for stackLen > 0 { + // Pop a block off of the stack. + stackLen-- + block := stack[stackLen] + + start, end := gcAddressOf(block), gcAddressOf(gcFindNext(block)) + + for addr := start; addr != end; addr += unsafe.Alignof(addr) { + // Load the word. + word := *(*uintptr)(unsafe.Pointer(addr)) + + if !isOnHeap(word) { + // Not a heap pointer. + continue + } + + // Find the corresponding memory block. + referencedBlock := blockFromAddr(word) + + if gcStateOf(referencedBlock) == blockStateFree { + // The to-be-marked object doesn't actually exist. + // This is probably a false positive. + continue + } + + // Move to the block's head. + referencedBlock = gcFindHead(referencedBlock) + + if gcStateOf(referencedBlock) == blockStateMark { + // The block has already been marked by something else. + continue + } + + // Mark block. + + gcSetState(referencedBlock, blockStateMark) + + println("mark: %lx from %lx", gcPointerOf(referencedBlock), gcPointerOf(root)) + + if stackLen == len(stack) { + // The stack is full. + // It is necessary to rescan all marked blocks once we are done. + markStackOverflow = true + if gcDebug { + println("gc stack overflowed") + } + continue + } + + // Push the pointer onto the stack to be scanned later. + stack[stackLen] = referencedBlock + stackLen++ + } + } +} + +// finishMark finishes the marking process by processing all stack overflows. +func finishMark() { + for markStackOverflow { + // Re-mark all blocks. + markStackOverflow = false + for block := uintptr(0); block < memory.EndBlock; block++ { + if gcStateOf(block) != blockStateMark { + // Block is not marked, so we do not need to rescan it. + continue + } + + // Re-mark the block. + startMark(block) + } + } +} + +// mark a GC root at the address addr. +func markRoot(addr, root uintptr) { + if isOnHeap(root) { + println("on the heap: %lx", gcPointerOf(root)) + block := blockFromAddr(root) + if gcStateOf(block) == blockStateFree { + // The to-be-marked object doesn't actually exist. + // This could either be a dangling pointer (oops!) but most likely + // just a false positive. + return + } + head := gcFindHead(block) + + if gcStateOf(head) != blockStateMark { + startMark(head) + } + } +} + +// Sweep goes through all memory and frees unmarked memory. +// It returns how many bytes are free in the heap after the sweep. +func sweep() (freeBytes uintptr) { + freeCurrentObject := false + var freed uint64 + + var from uintptr + for block := uintptr(0); block < memory.EndBlock; block++ { + switch gcStateOf(block) { + case blockStateHead: + // Unmarked head. Free it, including all tail blocks following it. + gcMarkFree(block) + freeCurrentObject = true + gcFrees++ + freed++ + from = block + case blockStateTail: + if freeCurrentObject { + // This is a tail object following an unmarked head. + // Free it now. + gcMarkFree(block) + freed++ + } + println("free from %lx to %lx", gcPointerOf(from), gcPointerOf(block)) + case blockStateMark: + // This is a marked object. The next tail blocks must not be freed, + // but the mark bit must be removed so the next GC cycle will + // collect this object if it is unreferenced then. + gcUnmark(block) + freeCurrentObject = false + case blockStateFree: + freeBytes += bytesPerBlock + } + } + gcFreedBlocks += freed + freeBytes += uintptr(freed) * bytesPerBlock + return +} + +// 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 +} + +func gcMarkReachable() { + // a compiler trick to get current SP + println("scan stack", unsafe.Pointer(getsp()), unsafe.Pointer(memory.StackTop)) + markRoots(uintptr(getsp()), memory.StackTop) + println("scan global", unsafe.Pointer(memory.GlobalsStart), unsafe.Pointer(memory.GlobalsEnd)) + + markRoots(memory.GlobalsStart, memory.GlobalsEnd) +} + +func gcResumeWorld() { + // Nothing to do here (single threaded). +} diff --git a/runtime/internal/runtime/tinygogc/gc.go b/runtime/internal/runtime/tinygogc/gc.go new file mode 100644 index 00000000..6c93d876 --- /dev/null +++ b/runtime/internal/runtime/tinygogc/gc.go @@ -0,0 +1,9 @@ +package tinygogc + +import "github.com/goplus/llgo/runtime/internal/runtime" + +const LLGoPackage = "noinit" + +func GC() { + runtime.GC() +} diff --git a/runtime/internal/runtime/tinygogc/memory/memory.go b/runtime/internal/runtime/tinygogc/memory/memory.go new file mode 100644 index 00000000..80312784 --- /dev/null +++ b/runtime/internal/runtime/tinygogc/memory/memory.go @@ -0,0 +1,71 @@ +//go:build baremetal + +package memory + +import "unsafe" + +// no init function, we don't want to init this twice +const LLGoPackage = "noinit" + +//go:linkname _heapStart _heapStart +var _heapStart [0]byte + +//go:linkname _heapEnd _heapEnd +var _heapEnd [0]byte + +//go:linkname _stackStart _stack_top +var _stackStart [0]byte + +//go:linkname _globals_start _globals_start +var _globals_start [0]byte + +//go:linkname _globals_end _globals_end +var _globals_end [0]byte + +// since we don't have an init() function, these should be initalized by initHeap(), which is called by
entry +var ( + HeapStart uintptr // start address of heap area + HeapEnd uintptr // end address of heap area + GlobalsStart uintptr // start address of global variable area + GlobalsEnd uintptr // end address of global variable area + StackTop uintptr // the top of stack + EndBlock uintptr // GC end block index + MetadataStart unsafe.Pointer // start address of GC metadata +) + +// Some globals + constants for the entire GC. + +const ( + wordsPerBlock = 4 // number of pointers in an allocated block + bytesPerBlock = wordsPerBlock * unsafe.Sizeof(HeapStart) + stateBits = 2 // how many bits a block state takes (see blockState type) + blocksPerStateByte = 8 / stateBits + markStackSize = 8 * unsafe.Sizeof((*int)(nil)) // number of to-be-marked blocks to queue before forcing a rescan +) + +// zeroSizedAlloc is just a sentinel that gets returned when allocating 0 bytes. +var zeroSizedAlloc uint8 + +// when executing initGC(), we must ensure there's no any allocations. +// use linking here to avoid import clite +// +//go:linkname memset C.memset +func memset(unsafe.Pointer, int, uintptr) + +// this function MUST be initalized first, which means it's required to be initalized before runtime +// +//export initGC +func initGC() { + // reserve 2K blocks for malloc + HeapStart = uintptr(unsafe.Pointer(&_heapStart)) + 2048 + HeapEnd = uintptr(unsafe.Pointer(&_heapEnd)) + GlobalsStart = uintptr(unsafe.Pointer(&_globals_start)) + GlobalsEnd = uintptr(unsafe.Pointer(&_globals_end)) + 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)) + + memset(MetadataStart, 0, metadataSize) +} diff --git a/runtime/internal/runtime/z_gc.go b/runtime/internal/runtime/z_gc.go index 4a729a96..8bb820be 100644 --- a/runtime/internal/runtime/z_gc.go +++ b/runtime/internal/runtime/z_gc.go @@ -1,5 +1,4 @@ -//go:build !nogc -// +build !nogc +//go:build !nogc && !baremetal /* * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/runtime/z_gc_baremetal.go b/runtime/internal/runtime/z_gc_baremetal.go new file mode 100644 index 00000000..b3e4d47a --- /dev/null +++ b/runtime/internal/runtime/z_gc_baremetal.go @@ -0,0 +1,36 @@ +//go:build !nogc && baremetal + +/* + * Copyright (c) 2024 The GoPlus Authors (goplus.org). 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" + + c "github.com/goplus/llgo/runtime/internal/clite" +) + +// AllocU allocates uninitialized memory. +func AllocU(size uintptr) unsafe.Pointer { + return alloc(size) +} + +// AllocZ allocates zero-initialized memory. +func AllocZ(size uintptr) unsafe.Pointer { + ptr := alloc(size) + return c.Memset(ptr, 0, size) +} diff --git a/targets/esp32-riscv.app.elf.ld b/targets/esp32-riscv.app.elf.ld index 7f6ab8f6..a7c7d623 100644 --- a/targets/esp32-riscv.app.elf.ld +++ b/targets/esp32-riscv.app.elf.ld @@ -1,6 +1,4 @@ -__stack = ORIGIN(dram_seg) + LENGTH(dram_seg); -__MIN_STACK_SIZE = 0x1000; -_stack_top = __stack; +_heapEnd = ORIGIN(dram_seg) + LENGTH(dram_seg); /* Default entry point */ ENTRY(_start) @@ -104,6 +102,14 @@ SECTIONS . += ORIGIN(iram_seg) == ORIGIN(dram_seg) ? 0 : _iram_end - _iram_start; } > dram_seg + .stack (NOLOAD) : + { + __stack_end = .; + . = ALIGN(16); + . += 16K; + __stack = .; + } + .data : { _data_start = .; @@ -148,7 +154,7 @@ SECTIONS } > dram_seg /* Check if data + heap + stack exceeds RAM limit */ - ASSERT(_end <= __stack - __MIN_STACK_SIZE, "region DRAM overflowed by .data and .bss sections") + ASSERT(_end <= _heapEnd, "region DRAM overflowed by .data and .bss sections") /* Stabs debugging sections. */ .stab 0 : { *(.stab) } @@ -193,3 +199,8 @@ SECTIONS .gnu.attributes 0 : { KEEP (*(.gnu.attributes)) } /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) } } + +_globals_start = _data_start; +_globals_end = _end; +_heapStart = _end; +_stack_top = __stack; diff --git a/targets/esp32.app.elf.ld b/targets/esp32.app.elf.ld index beab0485..071b43d0 100755 --- a/targets/esp32.app.elf.ld +++ b/targets/esp32.app.elf.ld @@ -1,5 +1,4 @@ -__stack = ORIGIN(dram_seg) + LENGTH(dram_seg); -__MIN_STACK_SIZE = 0x2000; +_heapEnd = ORIGIN(dram_seg) + LENGTH(dram_seg); ENTRY(_start) SECTIONS @@ -26,6 +25,14 @@ SECTIONS the same address within the page on the next page up. */ . = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) & (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE)); + .stack (NOLOAD) : + { + __stack_end = .; + . = ALIGN(16); + . += 16K; + __stack = .; + } + .rodata : { @@ -116,7 +123,7 @@ SECTIONS . = DATA_SEGMENT_END (.); /* Check if data + heap + stack exceeds RAM limit */ - ASSERT(. <= __stack - __MIN_STACK_SIZE, "region DRAM overflowed by .data and .bss sections") + ASSERT(. <= _heapEnd, "region DRAM overflowed by .data and .bss sections") /* Stabs debugging sections. */ .stab 0 : { *(.stab) } @@ -165,4 +172,7 @@ SECTIONS _sbss = __bss_start; _ebss = _end; - +_globals_start = _data_start; +_globals_end = _end; +_heapStart = _end; +_stack_top = __stack; diff --git a/targets/esp32.memory.elf.ld b/targets/esp32.memory.elf.ld index 65976719..a13a1dfe 100755 --- a/targets/esp32.memory.elf.ld +++ b/targets/esp32.memory.elf.ld @@ -19,8 +19,8 @@ MEMORY /* 64k at the end of DRAM, after ROM bootloader stack * or entire DRAM (for QEMU only) */ - dram_seg (RW) : org = 0x3FFF0000 , - len = 0x10000 + dram_seg (RW) : org = 0x3ffae000 , + len = 0x52000 } INCLUDE "targets/esp32.app.elf.ld"; From 531f69ae6a353a40ed37ffc91a8183ae37461c13 Mon Sep 17 00:00:00 2001 From: Haolan Date: Tue, 16 Sep 2025 12:30:31 +0800 Subject: [PATCH 02/15] fix: bdwgc.init() causing archive mode building fail P P P --- internal/build/build.go | 5 - runtime/internal/clite/bdwgc/bdwgc.go | 5 - runtime/internal/runtime/tinygogc/gc.go | 8 +- .../runtime/{ => tinygogc}/gc_tinygo.go | 208 +++++++++++------- .../runtime/tinygogc/memory/memory.go | 71 ------ runtime/internal/runtime/z_gc.go | 1 + runtime/internal/runtime/z_gc_baremetal.go | 7 +- targets/esp32-riscv.app.elf.ld | 14 +- targets/esp32.app.elf.ld | 106 ++++++++- 9 files changed, 242 insertions(+), 183 deletions(-) rename runtime/internal/runtime/{ => tinygogc}/gc_tinygo.go (76%) delete mode 100644 runtime/internal/runtime/tinygogc/memory/memory.go diff --git a/internal/build/build.go b/internal/build/build.go index b94e88e0..e0f18ea8 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -997,10 +997,6 @@ define weak void @runtime.init() { ret void } -define weak void @initGC() { - ret void -} - ; TODO(lijie): workaround for syscall patch define weak void @"syscall.init"() { ret void @@ -1012,7 +1008,6 @@ define weak void @"syscall.init"() { _llgo_0: store i32 %%0, ptr @__llgo_argc, align 4 store ptr %%1, ptr @__llgo_argv, align 8 - call void @initGC() %s %s %s diff --git a/runtime/internal/clite/bdwgc/bdwgc.go b/runtime/internal/clite/bdwgc/bdwgc.go index 83bf5f91..8f0af818 100644 --- a/runtime/internal/clite/bdwgc/bdwgc.go +++ b/runtime/internal/clite/bdwgc/bdwgc.go @@ -26,11 +26,6 @@ const ( LLGoPackage = "link: $(pkg-config --libs bdw-gc); -lgc" ) -//export initGC -func initGC() { - Init() -} - // ----------------------------------------------------------------------------- //go:linkname Init C.GC_init diff --git a/runtime/internal/runtime/tinygogc/gc.go b/runtime/internal/runtime/tinygogc/gc.go index 6c93d876..0f91b82a 100644 --- a/runtime/internal/runtime/tinygogc/gc.go +++ b/runtime/internal/runtime/tinygogc/gc.go @@ -1,9 +1,3 @@ package tinygogc -import "github.com/goplus/llgo/runtime/internal/runtime" - -const LLGoPackage = "noinit" - -func GC() { - runtime.GC() -} +const LLGoPackage = "link: --wrap=malloc --wrap=realloc --wrap=calloc" diff --git a/runtime/internal/runtime/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go similarity index 76% rename from runtime/internal/runtime/gc_tinygo.go rename to runtime/internal/runtime/tinygogc/gc_tinygo.go index 9962337c..2efcda91 100644 --- a/runtime/internal/runtime/gc_tinygo.go +++ b/runtime/internal/runtime/tinygogc/gc_tinygo.go @@ -16,29 +16,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package runtime +package tinygogc import ( "unsafe" _ "unsafe" - - c "github.com/goplus/llgo/runtime/internal/clite" - "github.com/goplus/llgo/runtime/internal/runtime/tinygogc/memory" ) const gcDebug = false const needsStaticHeap = true -// Some globals + constants for the entire GC. - -const ( - wordsPerBlock = 4 // number of pointers in an allocated block - bytesPerBlock = wordsPerBlock * unsafe.Sizeof(memory.HeapStart) - stateBits = 2 // how many bits a block state takes (see blockState type) - blocksPerStateByte = 8 / stateBits - markStackSize = 8 * unsafe.Sizeof((*int)(nil)) // number of to-be-marked blocks to queue before forcing a rescan -) - // Provide some abc.Straction over heap blocks. // blockState stores the four states in which a block can be. It is two bits in @@ -52,17 +39,52 @@ const ( blockStateMask uint8 = 3 // 11 ) +// The byte value of a block where every block is a 'tail' block. +const blockStateByteAllTails = 0 | + uint8(blockStateTail<<(stateBits*3)) | + uint8(blockStateTail<<(stateBits*2)) | + uint8(blockStateTail<<(stateBits*1)) | + uint8(blockStateTail<<(stateBits*0)) + //go:linkname getsp llgo.stackSave func getsp() unsafe.Pointer -func printlnAndPanic(c string) { - println(c) - panic("") -} +// when executing initGC(), we must ensure there's no any allocations. +// use linking here to avoid import clite +// +//go:linkname memset C.memset +func memset(unsafe.Pointer, int, uintptr) unsafe.Pointer +//go:linkname memcpy C.memcpy +func memcpy(unsafe.Pointer, unsafe.Pointer, uintptr) + +//go:linkname _heapStart _heapStart +var _heapStart [0]byte + +//go:linkname _heapEnd _heapEnd +var _heapEnd [0]byte + +//go:linkname _stackStart _stack_top +var _stackStart [0]byte + +//go:linkname _globals_start _globals_start +var _globals_start [0]byte + +//go:linkname _globals_end _globals_end +var _globals_end [0]byte + +// since we don't have an init() function, these should be initalized by initHeap(), which is called by
entry var ( + heapStart uintptr // start address of heap area + heapEnd uintptr // end address of heap area + globalsStart uintptr // start address of global variable area + globalsEnd uintptr // end address of global variable area + stackTop uintptr // the top of stack + endBlock uintptr // GC end block index + metadataStart unsafe.Pointer // start address of GC metadata + isGCInit bool + nextAlloc uintptr // the next block that should be tried by the allocator - endBlock uintptr // the block just past the end of the available space gcTotalAlloc uint64 // total number of bytes allocated gcTotalBlocks uint64 // total number of allocated blocks gcMallocs uint64 // total number of allocations @@ -77,24 +99,61 @@ var ( zeroSizedAlloc uint8 ) -// blockState stores the four states in which a block can be. It is two bits in -// size. -type blockState uint8 +// Some globals + constants for the entire GC. -// The byte value of a block where every block is a 'tail' block. -const blockStateByteAllTails = 0 | - uint8(blockStateTail<<(stateBits*3)) | - uint8(blockStateTail<<(stateBits*2)) | - uint8(blockStateTail<<(stateBits*1)) | - uint8(blockStateTail<<(stateBits*0)) +const ( + wordsPerBlock = 4 // number of pointers in an allocated block + bytesPerBlock = wordsPerBlock * unsafe.Sizeof(heapStart) + stateBits = 2 // how many bits a block state takes (see blockState type) + blocksPerStateByte = 8 / stateBits + markStackSize = 8 * unsafe.Sizeof((*int)(nil)) // number of to-be-marked blocks to queue before forcing a rescan +) + +//export __wrap_malloc +func __wrap_malloc(size uintptr) unsafe.Pointer { + return Alloc(size) +} + +//export __wrap_calloc +func __wrap_calloc(size uintptr) unsafe.Pointer { + return Alloc(size) +} + +//export __wrap_realloc +func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + return Realloc(ptr, size) +} + +// 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 that function + heapStart = uintptr(unsafe.Pointer(&_heapStart)) + 2048 + heapEnd = uintptr(unsafe.Pointer(&_heapEnd)) + globalsStart = uintptr(unsafe.Pointer(&_globals_start)) + globalsEnd = uintptr(unsafe.Pointer(&_globals_end)) + 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)) + + memset(metadataStart, 0, metadataSize) +} + +func lazyInit() { + if !isGCInit { + initGC() + isGCInit = true + } +} // blockFromAddr returns a block given an address somewhere in the heap (which // might not be heap-aligned). func blockFromAddr(addr uintptr) uintptr { - if addr < memory.HeapStart || addr >= uintptr(memory.MetadataStart) { - printlnAndPanic("gc: trying to get block from invalid address") + if addr < heapStart || addr >= uintptr(metadataStart) { + println("gc: trying to get block from invalid address") } - return (addr - memory.HeapStart) / bytesPerBlock + return (addr - heapStart) / bytesPerBlock } // Return a pointer to the start of the allocated object. @@ -104,9 +163,9 @@ func gcPointerOf(blockAddr uintptr) unsafe.Pointer { // Return the address of the start of the allocated object. func gcAddressOf(blockAddr uintptr) uintptr { - addr := memory.HeapStart + blockAddr*bytesPerBlock - if addr > uintptr(memory.MetadataStart) { - printlnAndPanic("gc: block pointing inside metadata") + addr := heapStart + blockAddr*bytesPerBlock + if addr > uintptr(metadataStart) { + println("gc: block pointing inside metadata") } return addr } @@ -137,7 +196,7 @@ func gcFindHead(blockAddr uintptr) uintptr { blockAddr-- } if gcStateOf(blockAddr) != blockStateHead && gcStateOf(blockAddr) != blockStateMark { - printlnAndPanic("gc: found tail without head") + println("gc: found tail without head") } return blockAddr } @@ -148,14 +207,14 @@ func gcFindNext(blockAddr uintptr) uintptr { if gcStateOf(blockAddr) == blockStateHead || gcStateOf(blockAddr) == blockStateMark { blockAddr++ } - for gcAddressOf(blockAddr) < uintptr(memory.MetadataStart) && gcStateOf(blockAddr) == blockStateTail { + for gcAddressOf(blockAddr) < uintptr(metadataStart) && gcStateOf(blockAddr) == blockStateTail { blockAddr++ } return blockAddr } func gcStateByteOf(blockAddr uintptr) byte { - return *(*uint8)(unsafe.Add(memory.MetadataStart, blockAddr/blocksPerStateByte)) + return *(*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte)) } // Return the block state given a state byte. The state byte must have been @@ -173,19 +232,19 @@ func gcStateOf(blockAddr uintptr) uint8 { // bits than the current state. Allowed transitions: from free to any state and // from head to mark. func gcSetState(blockAddr uintptr, newState uint8) { - stateBytePtr := (*uint8)(unsafe.Add(memory.MetadataStart, blockAddr/blocksPerStateByte)) + stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte)) *stateBytePtr |= uint8(newState << ((blockAddr % blocksPerStateByte) * stateBits)) if gcStateOf(blockAddr) != newState { - printlnAndPanic("gc: setState() was not successful") + println("gc: setState() was not successful") } } // markFree sets the block state to free, no matter what state it was in before. func gcMarkFree(blockAddr uintptr) { - stateBytePtr := (*uint8)(unsafe.Add(memory.MetadataStart, blockAddr/blocksPerStateByte)) + stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte)) *stateBytePtr &^= uint8(blockStateMask << ((blockAddr % blocksPerStateByte) * stateBits)) if gcStateOf(blockAddr) != blockStateFree { - printlnAndPanic("gc: markFree() was not successful") + println("gc: markFree() was not successful") } *(*[wordsPerBlock]uintptr)(unsafe.Pointer(gcAddressOf(blockAddr))) = [wordsPerBlock]uintptr{} } @@ -194,25 +253,27 @@ func gcMarkFree(blockAddr uintptr) { // before calling this function. func gcUnmark(blockAddr uintptr) { if gcStateOf(blockAddr) != blockStateMark { - printlnAndPanic("gc: unmark() on a block that is not marked") + println("gc: unmark() on a block that is not marked") } clearMask := blockStateMask ^ blockStateHead // the bits to clear from the state - stateBytePtr := (*uint8)(unsafe.Add(memory.MetadataStart, blockAddr/blocksPerStateByte)) + stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte)) *stateBytePtr &^= uint8(clearMask << ((blockAddr % blocksPerStateByte) * stateBits)) if gcStateOf(blockAddr) != blockStateHead { - printlnAndPanic("gc: unmark() was not successful") + println("gc: unmark() was not successful") } } func isOnHeap(ptr uintptr) bool { - return ptr >= memory.HeapStart && ptr < uintptr(memory.MetadataStart) + return ptr >= heapStart && ptr < uintptr(metadataStart) } // alloc tries to find some free space on the heap, possibly doing a garbage // collection cycle if needed. If no space is free, it panics. // //go:noinline -func alloc(size uintptr) unsafe.Pointer { +func Alloc(size uintptr) unsafe.Pointer { + lazyInit() + if size == 0 { return unsafe.Pointer(&zeroSizedAlloc) } @@ -237,7 +298,7 @@ func alloc(size uintptr) unsafe.Pointer { // free memory and try again. heapScanCount = 2 freeBytes := GC() - heapSize := uintptr(memory.MetadataStart) - memory.HeapStart + heapSize := uintptr(metadataStart) - heapStart if freeBytes < heapSize/3 { // Ensure there is at least 33% headroom. // This percentage was arbitrarily chosen, and may need to @@ -254,13 +315,13 @@ func alloc(size uintptr) unsafe.Pointer { // Unfortunately the heap could not be increased. This // happens on baremetal systems for example (where all // available RAM has already been dedicated to the heap). - printlnAndPanic("out of memory") + println("out of memory") } } } // Wrap around the end of the heap. - if index == memory.EndBlock { + if index == endBlock { index = 0 // Reset numFreeBlocks as allocations cannot wrap. numFreeBlocks = 0 @@ -296,14 +357,15 @@ func alloc(size uintptr) unsafe.Pointer { } // Return a pointer to this allocation. - return gcPointerOf(thisAlloc) + return memset(gcPointerOf(thisAlloc), 0, size) } } } -func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { +func Realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + lazyInit() if ptr == nil { - return alloc(size) + return Alloc(size) } ptrAddress := uintptr(ptr) @@ -316,8 +378,8 @@ func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { return ptr } - newAlloc := alloc(size) - c.Memcpy(newAlloc, ptr, oldSize) + newAlloc := Alloc(size) + memcpy(newAlloc, ptr, oldSize) free(ptr) return newAlloc @@ -331,6 +393,8 @@ func free(ptr unsafe.Pointer) { // of the runtime.GC() function. The difference is that it returns the number of // free bytes in the heap after the GC is finished. func GC() (freeBytes uintptr) { + lazyInit() + if gcDebug { println("running collection cycle...") } @@ -356,19 +420,9 @@ func GC() (freeBytes uintptr) { // well (recursively). The start and end parameters must be valid pointers and // must be aligned. func markRoots(start, end uintptr) { - - if true { - if start >= end { - printlnAndPanic("gc: unexpected range to mark") - } - if start%unsafe.Alignof(start) != 0 { - printlnAndPanic("gc: unaligned start pointer") - } - if end%unsafe.Alignof(end) != 0 { - printlnAndPanic("gc: unaligned end pointer") - } + if start >= end { + println("gc: unexpected range to mark") } - // Reduce the end bound to avoid reading too far on platforms where pointer alignment is smaller than pointer size. // If the size of the range is 0, then end will be slightly below start after this. end -= unsafe.Sizeof(end) - unsafe.Alignof(end) @@ -419,11 +473,8 @@ func startMark(root uintptr) { } // Mark block. - gcSetState(referencedBlock, blockStateMark) - println("mark: %lx from %lx", gcPointerOf(referencedBlock), gcPointerOf(root)) - if stackLen == len(stack) { // The stack is full. // It is necessary to rescan all marked blocks once we are done. @@ -446,7 +497,7 @@ func finishMark() { for markStackOverflow { // Re-mark all blocks. markStackOverflow = false - for block := uintptr(0); block < memory.EndBlock; block++ { + for block := uintptr(0); block < endBlock; block++ { if gcStateOf(block) != blockStateMark { // Block is not marked, so we do not need to rescan it. continue @@ -461,7 +512,6 @@ func finishMark() { // mark a GC root at the address addr. func markRoot(addr, root uintptr) { if isOnHeap(root) { - println("on the heap: %lx", gcPointerOf(root)) block := blockFromAddr(root) if gcStateOf(block) == blockStateFree { // The to-be-marked object doesn't actually exist. @@ -477,14 +527,13 @@ func markRoot(addr, root uintptr) { } } -// Sweep goes through all memory and frees unmarked memory. +// Sweep goes through all memory and frees unmarked // It returns how many bytes are free in the heap after the sweep. func sweep() (freeBytes uintptr) { freeCurrentObject := false var freed uint64 - var from uintptr - for block := uintptr(0); block < memory.EndBlock; block++ { + for block := uintptr(0); block < endBlock; block++ { switch gcStateOf(block) { case blockStateHead: // Unmarked head. Free it, including all tail blocks following it. @@ -492,7 +541,6 @@ func sweep() (freeBytes uintptr) { freeCurrentObject = true gcFrees++ freed++ - from = block case blockStateTail: if freeCurrentObject { // This is a tail object following an unmarked head. @@ -500,7 +548,6 @@ func sweep() (freeBytes uintptr) { gcMarkFree(block) freed++ } - println("free from %lx to %lx", gcPointerOf(from), gcPointerOf(block)) case blockStateMark: // This is a marked object. The next tail blocks must not be freed, // but the mark bit must be removed so the next GC cycle will @@ -524,12 +571,9 @@ func growHeap() bool { } func gcMarkReachable() { - // a compiler trick to get current SP - println("scan stack", unsafe.Pointer(getsp()), unsafe.Pointer(memory.StackTop)) - markRoots(uintptr(getsp()), memory.StackTop) - println("scan global", unsafe.Pointer(memory.GlobalsStart), unsafe.Pointer(memory.GlobalsEnd)) - - markRoots(memory.GlobalsStart, memory.GlobalsEnd) + println("scan stack", getsp(), unsafe.Pointer(stackTop)) + markRoots(uintptr(getsp()), stackTop) + markRoots(globalsStart, globalsEnd) } func gcResumeWorld() { diff --git a/runtime/internal/runtime/tinygogc/memory/memory.go b/runtime/internal/runtime/tinygogc/memory/memory.go deleted file mode 100644 index 80312784..00000000 --- a/runtime/internal/runtime/tinygogc/memory/memory.go +++ /dev/null @@ -1,71 +0,0 @@ -//go:build baremetal - -package memory - -import "unsafe" - -// no init function, we don't want to init this twice -const LLGoPackage = "noinit" - -//go:linkname _heapStart _heapStart -var _heapStart [0]byte - -//go:linkname _heapEnd _heapEnd -var _heapEnd [0]byte - -//go:linkname _stackStart _stack_top -var _stackStart [0]byte - -//go:linkname _globals_start _globals_start -var _globals_start [0]byte - -//go:linkname _globals_end _globals_end -var _globals_end [0]byte - -// since we don't have an init() function, these should be initalized by initHeap(), which is called by
entry -var ( - HeapStart uintptr // start address of heap area - HeapEnd uintptr // end address of heap area - GlobalsStart uintptr // start address of global variable area - GlobalsEnd uintptr // end address of global variable area - StackTop uintptr // the top of stack - EndBlock uintptr // GC end block index - MetadataStart unsafe.Pointer // start address of GC metadata -) - -// Some globals + constants for the entire GC. - -const ( - wordsPerBlock = 4 // number of pointers in an allocated block - bytesPerBlock = wordsPerBlock * unsafe.Sizeof(HeapStart) - stateBits = 2 // how many bits a block state takes (see blockState type) - blocksPerStateByte = 8 / stateBits - markStackSize = 8 * unsafe.Sizeof((*int)(nil)) // number of to-be-marked blocks to queue before forcing a rescan -) - -// zeroSizedAlloc is just a sentinel that gets returned when allocating 0 bytes. -var zeroSizedAlloc uint8 - -// when executing initGC(), we must ensure there's no any allocations. -// use linking here to avoid import clite -// -//go:linkname memset C.memset -func memset(unsafe.Pointer, int, uintptr) - -// this function MUST be initalized first, which means it's required to be initalized before runtime -// -//export initGC -func initGC() { - // reserve 2K blocks for malloc - HeapStart = uintptr(unsafe.Pointer(&_heapStart)) + 2048 - HeapEnd = uintptr(unsafe.Pointer(&_heapEnd)) - GlobalsStart = uintptr(unsafe.Pointer(&_globals_start)) - GlobalsEnd = uintptr(unsafe.Pointer(&_globals_end)) - 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)) - - memset(MetadataStart, 0, metadataSize) -} diff --git a/runtime/internal/runtime/z_gc.go b/runtime/internal/runtime/z_gc.go index 8bb820be..5669667b 100644 --- a/runtime/internal/runtime/z_gc.go +++ b/runtime/internal/runtime/z_gc.go @@ -23,6 +23,7 @@ import ( c "github.com/goplus/llgo/runtime/internal/clite" "github.com/goplus/llgo/runtime/internal/clite/bdwgc" + _ "github.com/goplus/llgo/runtime/internal/runtime/bdwgc" ) // AllocU allocates uninitialized memory. diff --git a/runtime/internal/runtime/z_gc_baremetal.go b/runtime/internal/runtime/z_gc_baremetal.go index b3e4d47a..a2d3115c 100644 --- a/runtime/internal/runtime/z_gc_baremetal.go +++ b/runtime/internal/runtime/z_gc_baremetal.go @@ -21,16 +21,15 @@ package runtime import ( "unsafe" - c "github.com/goplus/llgo/runtime/internal/clite" + "github.com/goplus/llgo/runtime/internal/runtime/tinygogc" ) // AllocU allocates uninitialized memory. func AllocU(size uintptr) unsafe.Pointer { - return alloc(size) + return tinygogc.Alloc(size) } // AllocZ allocates zero-initialized memory. func AllocZ(size uintptr) unsafe.Pointer { - ptr := alloc(size) - return c.Memset(ptr, 0, size) + return tinygogc.Alloc(size) } diff --git a/targets/esp32-riscv.app.elf.ld b/targets/esp32-riscv.app.elf.ld index a7c7d623..d6cd85d5 100644 --- a/targets/esp32-riscv.app.elf.ld +++ b/targets/esp32-riscv.app.elf.ld @@ -92,6 +92,12 @@ SECTIONS _iram_end = .; } > iram_seg + .stack (NOLOAD) : + { + . += 16K; + __stack = .; + } > dram_seg + /** * This section is required to skip .iram0.text area because iram0_0_seg and * dram0_0_seg reflect the same address space on different buses. @@ -102,14 +108,6 @@ SECTIONS . += ORIGIN(iram_seg) == ORIGIN(dram_seg) ? 0 : _iram_end - _iram_start; } > dram_seg - .stack (NOLOAD) : - { - __stack_end = .; - . = ALIGN(16); - . += 16K; - __stack = .; - } - .data : { _data_start = .; diff --git a/targets/esp32.app.elf.ld b/targets/esp32.app.elf.ld index 071b43d0..e3c27fdc 100755 --- a/targets/esp32.app.elf.ld +++ b/targets/esp32.app.elf.ld @@ -27,7 +27,6 @@ SECTIONS .stack (NOLOAD) : { - __stack_end = .; . = ALIGN(16); . += 16K; __stack = .; @@ -176,3 +175,108 @@ _globals_start = _data_start; _globals_end = _end; _heapStart = _end; _stack_top = __stack; + + +/* From ESP-IDF: + * components/esp_rom/esp32/ld/esp32.rom.newlib-funcs.ld + * This is the subset that is sometimes used by LLVM during codegen, and thus + * must always be present. + */ +memcpy = 0x4000c2c8; +memmove = 0x4000c3c0; +memset = 0x4000c44c; + +/* From ESP-IDF: + * components/esp_rom/esp32/ld/esp32.rom.libgcc.ld + * These are called from LLVM during codegen. The original license is Apache + * 2.0, but I believe that a list of function names and addresses can't really + * be copyrighted. + */ +__absvdi2 = 0x4006387c; +__absvsi2 = 0x40063868; +__adddf3 = 0x40002590; +__addsf3 = 0x400020e8; +__addvdi3 = 0x40002cbc; +__addvsi3 = 0x40002c98; +__ashldi3 = 0x4000c818; +__ashrdi3 = 0x4000c830; +__bswapdi2 = 0x40064b08; +__bswapsi2 = 0x40064ae0; +__clrsbdi2 = 0x40064b7c; +__clrsbsi2 = 0x40064b64; +__clzdi2 = 0x4000ca50; +__clzsi2 = 0x4000c7e8; +__cmpdi2 = 0x40063820; +__ctzdi2 = 0x4000ca64; +__ctzsi2 = 0x4000c7f0; +__divdc3 = 0x400645a4; +__divdf3 = 0x40002954; +__divdi3 = 0x4000ca84; +__divsi3 = 0x4000c7b8; +__eqdf2 = 0x400636a8; +__eqsf2 = 0x40063374; +__extendsfdf2 = 0x40002c34; +__ffsdi2 = 0x4000ca2c; +__ffssi2 = 0x4000c804; +__fixdfdi = 0x40002ac4; +__fixdfsi = 0x40002a78; +__fixsfdi = 0x4000244c; +__fixsfsi = 0x4000240c; +__fixunsdfsi = 0x40002b30; +__fixunssfdi = 0x40002504; +__fixunssfsi = 0x400024ac; +__floatdidf = 0x4000c988; +__floatdisf = 0x4000c8c0; +__floatsidf = 0x4000c944; +__floatsisf = 0x4000c870; +__floatundidf = 0x4000c978; +__floatundisf = 0x4000c8b0; +__floatunsidf = 0x4000c938; +__floatunsisf = 0x4000c864; +__gcc_bcmp = 0x40064a70; +__gedf2 = 0x40063768; +__gesf2 = 0x4006340c; +__gtdf2 = 0x400636dc; +__gtsf2 = 0x400633a0; +__ledf2 = 0x40063704; +__lesf2 = 0x400633c0; +__lshrdi3 = 0x4000c84c; +__ltdf2 = 0x40063790; +__ltsf2 = 0x4006342c; +__moddi3 = 0x4000cd4c; +__modsi3 = 0x4000c7c0; +__muldc3 = 0x40063c90; +__muldf3 = 0x4006358c; +__muldi3 = 0x4000c9fc; +__mulsf3 = 0x400632c8; +__mulsi3 = 0x4000c7b0; +__mulvdi3 = 0x40002d78; +__mulvsi3 = 0x40002d60; +__nedf2 = 0x400636a8; +__negdf2 = 0x400634a0; +__negdi2 = 0x4000ca14; +__negsf2 = 0x400020c0; +__negvdi2 = 0x40002e98; +__negvsi2 = 0x40002e78; +__nesf2 = 0x40063374; +__nsau_data = 0x3ff96544; +__paritysi2 = 0x40002f3c; +__popcount_tab = 0x3ff96544; +__popcountdi2 = 0x40002ef8; +__popcountsi2 = 0x40002ed0; +__powidf2 = 0x400638e4; +__subdf3 = 0x400026e4; +__subsf3 = 0x400021d0; +__subvdi3 = 0x40002d20; +__subvsi3 = 0x40002cf8; +__truncdfsf2 = 0x40002b90; +__ucmpdi2 = 0x40063840; +__udiv_w_sdiv = 0x40064bec; +__udivdi3 = 0x4000cff8; +__udivmoddi4 = 0x40064bf4; +__udivsi3 = 0x4000c7c8; +__umoddi3 = 0x4000d280; +__umodsi3 = 0x4000c7d0; +__umulsidi3 = 0x4000c7d8; +__unorddf2 = 0x400637f4; +__unordsf2 = 0x40063478; From e4a69ce4138eb673684c46363f2a3623819a30fd Mon Sep 17 00:00:00 2001 From: Haolan Date: Tue, 16 Sep 2025 15:43:14 +0800 Subject: [PATCH 03/15] fix: disable buffers --- runtime/internal/clite/stdio_baremetal.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/runtime/internal/clite/stdio_baremetal.go b/runtime/internal/clite/stdio_baremetal.go index cd9a649e..49b8cdc1 100644 --- a/runtime/internal/clite/stdio_baremetal.go +++ b/runtime/internal/clite/stdio_baremetal.go @@ -25,3 +25,11 @@ import ( var Stdin FilePtr = Fopen(Str("/dev/stdin"), Str("r")) var Stdout FilePtr = Fopen(Str("/dev/stdout"), Str("w")) var Stderr FilePtr = Stdout + +//go:linkname setvbuf C.setvbuf +func setvbuf(fp FilePtr, buf *Char, typ Int, size SizeT) + +func init() { + setvbuf(Stdout, nil, 2, 0) + setvbuf(Stdin, nil, 2, 0) +} From 33a00dff1b87ea9c0d8009e60ad91a48bf2a264f Mon Sep 17 00:00:00 2001 From: Haolan Date: Tue, 16 Sep 2025 16:08:13 +0800 Subject: [PATCH 04/15] fix: invalid import and improve tests refactor: remove initGC test: add test for GC fix: invalid import in z_gc ci: test baremetal GC for coverage ci: test baremetal GC for coverage --- .github/workflows/go.yml | 5 + runtime/internal/clite/stdio_baremetal.go | 9 +- runtime/internal/clite/stdio_darwin.go | 3 +- runtime/internal/runtime/tinygogc/gc.go | 19 + runtime/internal/runtime/tinygogc/gc_llgo.go | 35 ++ runtime/internal/runtime/tinygogc/gc_test.go | 35 ++ .../internal/runtime/tinygogc/gc_tinygo.go | 52 +-- .../internal/runtime/tinygogc/pc_mock_test.go | 377 ++++++++++++++++++ runtime/internal/runtime/z_gc.go | 1 - targets/esp32.app.elf.ld | 105 ----- 10 files changed, 481 insertions(+), 160 deletions(-) create mode 100644 runtime/internal/runtime/tinygogc/gc_llgo.go create mode 100644 runtime/internal/runtime/tinygogc/gc_test.go create mode 100644 runtime/internal/runtime/tinygogc/pc_mock_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 2e706690..e09403fe 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -52,6 +52,11 @@ jobs: if: ${{!startsWith(matrix.os, 'macos')}} run: go test ./... + - name: Test Baremetal GC + if: ${{!startsWith(matrix.os, 'macos')}} + working-directory: runtime/internal/runtime/tinygogc + run: go test -tags testGC . + - name: Test with coverage if: startsWith(matrix.os, 'macos') run: go test -coverprofile="coverage.txt" -covermode=atomic ./... diff --git a/runtime/internal/clite/stdio_baremetal.go b/runtime/internal/clite/stdio_baremetal.go index 49b8cdc1..f318c065 100644 --- a/runtime/internal/clite/stdio_baremetal.go +++ b/runtime/internal/clite/stdio_baremetal.go @@ -29,7 +29,12 @@ var Stderr FilePtr = Stdout //go:linkname setvbuf C.setvbuf func setvbuf(fp FilePtr, buf *Char, typ Int, size SizeT) +const ( + _IONBF = 2 // No buffering - immediate output +) + func init() { - setvbuf(Stdout, nil, 2, 0) - setvbuf(Stdin, nil, 2, 0) + // Disable buffering for baremetal targets to ensure immediate output + setvbuf(Stdout, nil, _IONBF, 0) + setvbuf(Stdin, nil, _IONBF, 0) } diff --git a/runtime/internal/clite/stdio_darwin.go b/runtime/internal/clite/stdio_darwin.go index 324403f5..cd30c073 100644 --- a/runtime/internal/clite/stdio_darwin.go +++ b/runtime/internal/clite/stdio_darwin.go @@ -1,5 +1,4 @@ -//go:build darwin -// +build darwin +//go:build darwin && !baremetal /* * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/runtime/tinygogc/gc.go b/runtime/internal/runtime/tinygogc/gc.go index 0f91b82a..bfe4149e 100644 --- a/runtime/internal/runtime/tinygogc/gc.go +++ b/runtime/internal/runtime/tinygogc/gc.go @@ -1,3 +1,22 @@ +//go:build baremetal && !testGC + package tinygogc +import "unsafe" + const LLGoPackage = "link: --wrap=malloc --wrap=realloc --wrap=calloc" + +//export __wrap_malloc +func __wrap_malloc(size uintptr) unsafe.Pointer { + return Alloc(size) +} + +//export __wrap_calloc +func __wrap_calloc(size uintptr) unsafe.Pointer { + return Alloc(size) +} + +//export __wrap_realloc +func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + return Realloc(ptr, size) +} diff --git a/runtime/internal/runtime/tinygogc/gc_llgo.go b/runtime/internal/runtime/tinygogc/gc_llgo.go new file mode 100644 index 00000000..5f2d73e8 --- /dev/null +++ b/runtime/internal/runtime/tinygogc/gc_llgo.go @@ -0,0 +1,35 @@ +//go:build !testGC + +package tinygogc + +import ( + "unsafe" + _ "unsafe" +) + +//go:linkname getsp llgo.stackSave +func getsp() unsafe.Pointer + +// when executing initGC(), we must ensure there's no any allocations. +// use linking here to avoid import clite +// +//go:linkname memset C.memset +func memset(unsafe.Pointer, int, uintptr) unsafe.Pointer + +//go:linkname memcpy C.memcpy +func memcpy(unsafe.Pointer, unsafe.Pointer, uintptr) + +//go:linkname _heapStart _heapStart +var _heapStart [0]byte + +//go:linkname _heapEnd _heapEnd +var _heapEnd [0]byte + +//go:linkname _stackStart _stack_top +var _stackStart [0]byte + +//go:linkname _globals_start _globals_start +var _globals_start [0]byte + +//go:linkname _globals_end _globals_end +var _globals_end [0]byte diff --git a/runtime/internal/runtime/tinygogc/gc_test.go b/runtime/internal/runtime/tinygogc/gc_test.go new file mode 100644 index 00000000..0234b5fa --- /dev/null +++ b/runtime/internal/runtime/tinygogc/gc_test.go @@ -0,0 +1,35 @@ +//go:build testGC + +package tinygogc + +import ( + "unsafe" + _ "unsafe" +) + +var currentStack uintptr + +func getsp() uintptr { + return currentStack +} + +var _heapStart [0]byte + +var _heapEnd [0]byte + +var _stackStart [0]byte + +var _globals_start [0]byte + +var _globals_end [0]byte + +//go:linkname memclrNoHeapPointers runtime.memclrNoHeapPointers +func memclrNoHeapPointers(unsafe.Pointer, uintptr) unsafe.Pointer + +//go:linkname memcpy runtime.memmove +func memcpy(to unsafe.Pointer, from unsafe.Pointer, size uintptr) + +func memset(ptr unsafe.Pointer, n int, size uintptr) unsafe.Pointer { + memclrNoHeapPointers(ptr, size) + return ptr +} diff --git a/runtime/internal/runtime/tinygogc/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go index 2efcda91..1081dafb 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 || testGC /* * Copyright (c) 2018-2025 The TinyGo Authors. All rights reserved. @@ -20,17 +20,12 @@ package tinygogc import ( "unsafe" - _ "unsafe" ) const gcDebug = false -const needsStaticHeap = true - -// Provide some abc.Straction over heap blocks. // blockState stores the four states in which a block can be. It is two bits in // size. - const ( blockStateFree uint8 = 0 // 00 blockStateHead uint8 = 1 // 01 @@ -46,33 +41,6 @@ const blockStateByteAllTails = 0 | uint8(blockStateTail<<(stateBits*1)) | uint8(blockStateTail<<(stateBits*0)) -//go:linkname getsp llgo.stackSave -func getsp() unsafe.Pointer - -// when executing initGC(), we must ensure there's no any allocations. -// use linking here to avoid import clite -// -//go:linkname memset C.memset -func memset(unsafe.Pointer, int, uintptr) unsafe.Pointer - -//go:linkname memcpy C.memcpy -func memcpy(unsafe.Pointer, unsafe.Pointer, uintptr) - -//go:linkname _heapStart _heapStart -var _heapStart [0]byte - -//go:linkname _heapEnd _heapEnd -var _heapEnd [0]byte - -//go:linkname _stackStart _stack_top -var _stackStart [0]byte - -//go:linkname _globals_start _globals_start -var _globals_start [0]byte - -//go:linkname _globals_end _globals_end -var _globals_end [0]byte - // since we don't have an init() function, these should be initalized by initHeap(), which is called by
entry var ( heapStart uintptr // start address of heap area @@ -109,24 +77,9 @@ const ( markStackSize = 8 * unsafe.Sizeof((*int)(nil)) // number of to-be-marked blocks to queue before forcing a rescan ) -//export __wrap_malloc -func __wrap_malloc(size uintptr) unsafe.Pointer { - return Alloc(size) -} - -//export __wrap_calloc -func __wrap_calloc(size uintptr) unsafe.Pointer { - return Alloc(size) -} - -//export __wrap_realloc -func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { - return Realloc(ptr, size) -} - // 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 that function + // 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)) @@ -571,7 +524,6 @@ func growHeap() bool { } func gcMarkReachable() { - println("scan stack", getsp(), unsafe.Pointer(stackTop)) markRoots(uintptr(getsp()), stackTop) markRoots(globalsStart, globalsEnd) } diff --git a/runtime/internal/runtime/tinygogc/pc_mock_test.go b/runtime/internal/runtime/tinygogc/pc_mock_test.go new file mode 100644 index 00000000..9c37b136 --- /dev/null +++ b/runtime/internal/runtime/tinygogc/pc_mock_test.go @@ -0,0 +1,377 @@ +//go:build testGC + +package tinygogc + +import ( + "testing" + "unsafe" +) + +const ( + // Mock a typical embedded system with 128KB RAM + mockHeapSize = 128 * 1024 // 128KB + mockGlobalsSize = 4 * 1024 // 4KB for globals + mockStackSize = 8 * 1024 // 8KB for stack + mockReservedSize = 2048 // 2KB reserved as in real implementation +) + +type testObject struct { + data [4]uintptr +} + +// mockMemoryLayout simulates the memory layout of an embedded system +type mockMemoryLayout struct { + memory []byte + heapStart uintptr + heapEnd uintptr + globalsStart uintptr + globalsEnd uintptr + stackStart uintptr + stackEnd uintptr +} + +// createMockMemoryLayout creates a simulated 128KB memory environment +func createMockMemoryLayout() *mockMemoryLayout { + totalMemory := mockHeapSize + mockGlobalsSize + mockStackSize + memory := make([]byte, totalMemory) + baseAddr := uintptr(unsafe.Pointer(&memory[0])) + + layout := &mockMemoryLayout{ + memory: memory, + globalsStart: baseAddr, + globalsEnd: baseAddr + mockGlobalsSize, + heapStart: baseAddr + mockGlobalsSize + mockReservedSize, + heapEnd: baseAddr + mockGlobalsSize + mockHeapSize, + stackStart: baseAddr + mockGlobalsSize + mockHeapSize, + stackEnd: baseAddr + uintptr(totalMemory), + } + + return layout +} + +// setupMockGC initializes the GC with mock memory layout +func (m *mockMemoryLayout) setupMockGC() { + // Set mock values + heapStart = m.heapStart + heapEnd = m.heapEnd + globalsStart = m.globalsStart + globalsEnd = m.globalsEnd + stackTop = m.stackEnd + + // Set currentStack to the start of the mock stack + currentStack = m.stackStart + + // Calculate metadata layout + totalSize := heapEnd - heapStart + metadataSize := (totalSize + blocksPerStateByte*bytesPerBlock) / (1 + blocksPerStateByte*bytesPerBlock) + metadataStart = unsafe.Pointer(heapEnd - metadataSize) + endBlock = (uintptr(metadataStart) - heapStart) / bytesPerBlock + + // Clear metadata + metadataBytes := (*[1024]byte)(metadataStart)[:metadataSize:metadataSize] + for i := range metadataBytes { + metadataBytes[i] = 0 + } + + // Reset allocator state + nextAlloc = 0 + isGCInit = true +} + +// createTestObjects creates a network of objects for testing reachability +func createTestObjects(layout *mockMemoryLayout) []*testObject { + // Allocate several test objects + objects := make([]*testObject, 0, 10) + + // Dependencies Graph + // root1 -> child1 -> grandchild1 -> child2 + // root1 -> child2 -> grandchild1 + + // Create root objects (reachable from stack/globals) + root1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{}))) + root2 := (*testObject)(Alloc(unsafe.Sizeof(testObject{}))) + objects = append(objects, root1, root2) + + // Create objects reachable from root1 + child1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{}))) + child2 := (*testObject)(Alloc(unsafe.Sizeof(testObject{}))) + root1.data[0] = uintptr(unsafe.Pointer(child1)) + root1.data[1] = uintptr(unsafe.Pointer(child2)) + objects = append(objects, child1, child2) + + // Create objects reachable from child1 + grandchild1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{}))) + child1.data[0] = uintptr(unsafe.Pointer(grandchild1)) + objects = append(objects, grandchild1) + + // Create circular reference between child2 and grandchild1 + child2.data[0] = uintptr(unsafe.Pointer(grandchild1)) + grandchild1.data[0] = uintptr(unsafe.Pointer(child2)) + + // Create unreachable objects (garbage) + garbage1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{}))) + garbage2 := (*testObject)(Alloc(unsafe.Sizeof(testObject{}))) + // Create circular reference in garbage + garbage1.data[0] = uintptr(unsafe.Pointer(garbage2)) + garbage2.data[0] = uintptr(unsafe.Pointer(garbage1)) + objects = append(objects, garbage1, garbage2) + + return objects +} + +// mockStackScan simulates scanning stack for root pointers +func mockStackScan(roots []*testObject) { + // Simulate stack by creating local variables pointing to roots + + for _, root := range roots[:2] { // Only first 2 are actually roots + addr := uintptr(unsafe.Pointer(&root)) + ptr := uintptr(unsafe.Pointer(root)) + markRoot(addr, ptr) + } +} + +func TestMockGCBasicAllocation(t *testing.T) { + layout := createMockMemoryLayout() + layout.setupMockGC() + + // Test basic allocation + ptr1 := Alloc(32) + if ptr1 == nil { + t.Fatal("Failed to allocate 32 bytes") + } + + ptr2 := Alloc(64) + if ptr2 == nil { + t.Fatal("Failed to allocate 64 bytes") + } + + // Verify pointers are within heap bounds + addr1 := uintptr(ptr1) + addr2 := uintptr(ptr2) + + if addr1 < heapStart || addr1 >= uintptr(metadataStart) { + t.Errorf("ptr1 %x not within heap bounds [%x, %x)", addr1, heapStart, uintptr(metadataStart)) + } + + if addr2 < heapStart || addr2 >= uintptr(metadataStart) { + t.Errorf("ptr2 %x not within heap bounds [%x, %x)", addr2, heapStart, uintptr(metadataStart)) + } + + t.Logf("Allocated ptr1 at %x, ptr2 at %x", addr1, addr2) + t.Logf("Heap bounds: [%x, %x)", heapStart, uintptr(metadataStart)) +} + +func TestMockGCReachabilityAndSweep(t *testing.T) { + layout := createMockMemoryLayout() + layout.setupMockGC() + + // Track initial stats + initialMallocs := gcMallocs + initialFrees := gcFrees + + // Create test object network + objects := createTestObjects(layout) + roots := objects[:2] // First 2 are roots + + t.Logf("Created %d objects, %d are roots", len(objects), len(roots)) + t.Logf("Mallocs: %d", gcMallocs-initialMallocs) + + // Verify all objects are initially allocated + for i, obj := range objects { + addr := uintptr(unsafe.Pointer(obj)) + block := blockFromAddr(addr) + state := gcStateOf(block) + if state != blockStateHead { + t.Errorf("Object %d at %x has state %d, expected %d (HEAD)", i, addr, state, blockStateHead) + } + } + + // Perform GC with manual root scanning + // Mark reachable objects first + mockStackScan(roots) + finishMark() + + // Then sweep unreachable objects + freedBytes := sweep() + t.Logf("Freed %d bytes during GC", freedBytes) + t.Logf("Frees: %d (delta: %d)", gcFrees, gcFrees-initialFrees) + + // Verify reachable objects are still allocated + reachableObjects := []unsafe.Pointer{ + unsafe.Pointer(objects[0]), // root1 + unsafe.Pointer(objects[1]), // root2 + unsafe.Pointer(objects[2]), // child1 (reachable from root1) + unsafe.Pointer(objects[3]), // child2 (reachable from root1) + unsafe.Pointer(objects[4]), // grandchild1 (reachable from child1, child2) + } + + for i, obj := range reachableObjects { + addr := uintptr(obj) + block := blockFromAddr(addr) + state := gcStateOf(block) + if state != blockStateHead { + t.Errorf("Reachable object %d at %x has state %d, expected %d (HEAD)", i, addr, state, blockStateHead) + } + } + + // Verify unreachable objects are freed + unreachableObjects := []unsafe.Pointer{ + unsafe.Pointer(objects[5]), // garbage1 + unsafe.Pointer(objects[6]), // garbage2 + } + + for i, obj := range unreachableObjects { + addr := uintptr(obj) + block := blockFromAddr(addr) + state := gcStateOf(block) + if state != blockStateFree { + t.Errorf("Unreachable object %d at %x has state %d, expected %d (FREE)", i, addr, state, blockStateFree) + } + } + + // Verify some memory was actually freed + if freedBytes == 0 { + t.Error("Expected some memory to be freed, but freed 0 bytes") + } + + if gcFrees == initialFrees { + t.Error("Expected some objects to be freed, but free count didn't change") + } + + // clear ref for grandchild + objects[2].data[0] = 0 + objects[3].data[0] = 0 + + // Perform GC with manual root scanning + // Mark reachable objects first + mockStackScan(roots) + finishMark() + + // Then sweep unreachable objects + freedBytes = sweep() + + blockAddr := blockFromAddr(uintptr(unsafe.Pointer(objects[3]))) + + state := gcStateOf(blockAddr) + if state != blockStateHead { + t.Errorf("Unreachable object %d at %x has state %d, expected %d (HEAD)", 3, blockAddr, state, blockStateHead) + } + + blockAddr = blockFromAddr(uintptr(unsafe.Pointer(objects[4]))) + + state = gcStateOf(blockAddr) + if state != blockStateFree { + t.Errorf("Reachable object %d at %x has state %d, expected %d (HEAD)", 4, blockAddr, state, blockStateHead) + } +} + +func TestMockGCMemoryPressure(t *testing.T) { + layout := createMockMemoryLayout() + layout.setupMockGC() + + // Calculate available heap space + heapSize := uintptr(metadataStart) - heapStart + blockSize := bytesPerBlock + maxBlocks := heapSize / blockSize + + t.Logf("Heap size: %d bytes, Block size: %d bytes, Max blocks: %d", + heapSize, blockSize, maxBlocks) + + // Allocate until we trigger GC + var allocations []unsafe.Pointer + allocSize := uintptr(32) // Small allocations + + // Allocate about 80% of heap to trigger GC pressure + targetAllocations := int(maxBlocks * 4 / 5) // 80% capacity + + for i := 0; i < targetAllocations; i++ { + ptr := Alloc(allocSize) + if ptr == nil { + t.Fatalf("Failed to allocate at iteration %d", i) + } + allocations = append(allocations, ptr) + } + + initialMallocs := gcMallocs + t.Logf("Allocated %d objects (%d mallocs total)", len(allocations), initialMallocs) + + // Clear references to half the allocations (make them garbage) + garbageCount := len(allocations) / 2 + allocations = allocations[garbageCount:] + + // Force GC + freeBytes := GC() + + t.Logf("GC freed %d bytes", freeBytes) + t.Logf("Objects freed: %d", gcFrees) + + // Try to allocate more after GC + for i := 0; i < 10; i++ { + ptr := Alloc(allocSize) + if ptr == nil { + t.Fatalf("Failed to allocate after GC at iteration %d", i) + } + } + + t.Log("Successfully allocated more objects after GC") +} + +func TestMockGCCircularReferences(t *testing.T) { + layout := createMockMemoryLayout() + layout.setupMockGC() + + type Node struct { + data [3]uintptr + next uintptr + } + + // Create a circular linked list + nodes := make([]*Node, 5) + for i := range nodes { + nodes[i] = (*Node)(Alloc(unsafe.Sizeof(Node{}))) + nodes[i].data[0] = uintptr(i) // Store index as data + } + + // Link them in a circle + for i := range nodes { + nextIdx := (i + 1) % len(nodes) + nodes[i].next = uintptr(unsafe.Pointer(nodes[nextIdx])) + } + + t.Logf("Created circular list of %d nodes", len(nodes)) + + // Initially all should be allocated + for i, node := range nodes { + addr := uintptr(unsafe.Pointer(node)) + block := blockFromAddr(addr) + state := gcStateOf(block) + if state != blockStateHead { + t.Errorf("Node %d at %x has state %d, expected %d", i, addr, state, blockStateHead) + } + } + + // Clear references (make entire circle unreachable) + // for i := range nodes { + // nodes[zi] = nil + // } + + // Force GC without roots + freeBytes := GC() + + t.Logf("GC freed %d bytes", freeBytes) + + // All nodes should now be freed since they're not reachable + // Note: We can't check the specific nodes since we cleared the references, + // but we can verify that significant memory was freed + expectedFreed := uintptr(len(nodes)) * ((unsafe.Sizeof(Node{}) + bytesPerBlock - 1) / bytesPerBlock) * bytesPerBlock + + if freeBytes < expectedFreed { + t.Errorf("Expected at least %d bytes freed, got %d", expectedFreed, freeBytes) + } + + // Verify we can allocate new objects in the freed space + newPtr := Alloc(unsafe.Sizeof(Node{})) + if newPtr == nil { + t.Error("Failed to allocate after freeing circular references") + } +} diff --git a/runtime/internal/runtime/z_gc.go b/runtime/internal/runtime/z_gc.go index 5669667b..8bb820be 100644 --- a/runtime/internal/runtime/z_gc.go +++ b/runtime/internal/runtime/z_gc.go @@ -23,7 +23,6 @@ import ( c "github.com/goplus/llgo/runtime/internal/clite" "github.com/goplus/llgo/runtime/internal/clite/bdwgc" - _ "github.com/goplus/llgo/runtime/internal/runtime/bdwgc" ) // AllocU allocates uninitialized memory. diff --git a/targets/esp32.app.elf.ld b/targets/esp32.app.elf.ld index e3c27fdc..94b90611 100755 --- a/targets/esp32.app.elf.ld +++ b/targets/esp32.app.elf.ld @@ -175,108 +175,3 @@ _globals_start = _data_start; _globals_end = _end; _heapStart = _end; _stack_top = __stack; - - -/* From ESP-IDF: - * components/esp_rom/esp32/ld/esp32.rom.newlib-funcs.ld - * This is the subset that is sometimes used by LLVM during codegen, and thus - * must always be present. - */ -memcpy = 0x4000c2c8; -memmove = 0x4000c3c0; -memset = 0x4000c44c; - -/* From ESP-IDF: - * components/esp_rom/esp32/ld/esp32.rom.libgcc.ld - * These are called from LLVM during codegen. The original license is Apache - * 2.0, but I believe that a list of function names and addresses can't really - * be copyrighted. - */ -__absvdi2 = 0x4006387c; -__absvsi2 = 0x40063868; -__adddf3 = 0x40002590; -__addsf3 = 0x400020e8; -__addvdi3 = 0x40002cbc; -__addvsi3 = 0x40002c98; -__ashldi3 = 0x4000c818; -__ashrdi3 = 0x4000c830; -__bswapdi2 = 0x40064b08; -__bswapsi2 = 0x40064ae0; -__clrsbdi2 = 0x40064b7c; -__clrsbsi2 = 0x40064b64; -__clzdi2 = 0x4000ca50; -__clzsi2 = 0x4000c7e8; -__cmpdi2 = 0x40063820; -__ctzdi2 = 0x4000ca64; -__ctzsi2 = 0x4000c7f0; -__divdc3 = 0x400645a4; -__divdf3 = 0x40002954; -__divdi3 = 0x4000ca84; -__divsi3 = 0x4000c7b8; -__eqdf2 = 0x400636a8; -__eqsf2 = 0x40063374; -__extendsfdf2 = 0x40002c34; -__ffsdi2 = 0x4000ca2c; -__ffssi2 = 0x4000c804; -__fixdfdi = 0x40002ac4; -__fixdfsi = 0x40002a78; -__fixsfdi = 0x4000244c; -__fixsfsi = 0x4000240c; -__fixunsdfsi = 0x40002b30; -__fixunssfdi = 0x40002504; -__fixunssfsi = 0x400024ac; -__floatdidf = 0x4000c988; -__floatdisf = 0x4000c8c0; -__floatsidf = 0x4000c944; -__floatsisf = 0x4000c870; -__floatundidf = 0x4000c978; -__floatundisf = 0x4000c8b0; -__floatunsidf = 0x4000c938; -__floatunsisf = 0x4000c864; -__gcc_bcmp = 0x40064a70; -__gedf2 = 0x40063768; -__gesf2 = 0x4006340c; -__gtdf2 = 0x400636dc; -__gtsf2 = 0x400633a0; -__ledf2 = 0x40063704; -__lesf2 = 0x400633c0; -__lshrdi3 = 0x4000c84c; -__ltdf2 = 0x40063790; -__ltsf2 = 0x4006342c; -__moddi3 = 0x4000cd4c; -__modsi3 = 0x4000c7c0; -__muldc3 = 0x40063c90; -__muldf3 = 0x4006358c; -__muldi3 = 0x4000c9fc; -__mulsf3 = 0x400632c8; -__mulsi3 = 0x4000c7b0; -__mulvdi3 = 0x40002d78; -__mulvsi3 = 0x40002d60; -__nedf2 = 0x400636a8; -__negdf2 = 0x400634a0; -__negdi2 = 0x4000ca14; -__negsf2 = 0x400020c0; -__negvdi2 = 0x40002e98; -__negvsi2 = 0x40002e78; -__nesf2 = 0x40063374; -__nsau_data = 0x3ff96544; -__paritysi2 = 0x40002f3c; -__popcount_tab = 0x3ff96544; -__popcountdi2 = 0x40002ef8; -__popcountsi2 = 0x40002ed0; -__powidf2 = 0x400638e4; -__subdf3 = 0x400026e4; -__subsf3 = 0x400021d0; -__subvdi3 = 0x40002d20; -__subvsi3 = 0x40002cf8; -__truncdfsf2 = 0x40002b90; -__ucmpdi2 = 0x40063840; -__udiv_w_sdiv = 0x40064bec; -__udivdi3 = 0x4000cff8; -__udivmoddi4 = 0x40064bf4; -__udivsi3 = 0x4000c7c8; -__umoddi3 = 0x4000d280; -__umodsi3 = 0x4000c7d0; -__umulsidi3 = 0x4000c7d8; -__unorddf2 = 0x400637f4; -__unordsf2 = 0x40063478; From 66a537ad29cbc3b5f149f5889ecb6b9832e4bd6d Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 19 Sep 2025 11:27:33 +0800 Subject: [PATCH 05/15] fix: add gc dummy mutex --- runtime/internal/lib/runtime/runtime2.go | 6 - runtime/internal/lib/runtime/runtime_gc.go | 10 +- .../lib/runtime/runtime_gc_baremetal.go | 20 +- runtime/internal/runtime/tinygogc/gc.go | 177 ++++++++++++++++++ .../tinygogc/{gc_llgo.go => gc_link.go} | 3 + .../internal/runtime/tinygogc/gc_tinygo.go | 32 +++- runtime/internal/runtime/tinygogc/mutex.go | 7 + targets/esp32.app.elf.ld | 1 + 8 files changed, 240 insertions(+), 16 deletions(-) rename runtime/internal/runtime/tinygogc/{gc_llgo.go => gc_link.go} (92%) create mode 100644 runtime/internal/runtime/tinygogc/mutex.go diff --git a/runtime/internal/lib/runtime/runtime2.go b/runtime/internal/lib/runtime/runtime2.go index db2db492..0fc08064 100644 --- a/runtime/internal/lib/runtime/runtime2.go +++ b/runtime/internal/lib/runtime/runtime2.go @@ -4,8 +4,6 @@ package runtime -import "runtime" - // Layout of in-memory per-function information prepared by linker // See https://golang.org/s/go12symtab. // Keep in sync with linker (../cmd/link/internal/ld/pcln.go:/pclntab) @@ -30,10 +28,6 @@ func StopTrace() { panic("todo: runtime.StopTrace") } -func ReadMemStats(m *runtime.MemStats) { - panic("todo: runtime.ReadMemStats") -} - func SetMutexProfileFraction(rate int) int { panic("todo: runtime.SetMutexProfileFraction") } diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index a6d76401..38de11a0 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -2,7 +2,15 @@ package runtime -import "github.com/goplus/llgo/runtime/internal/clite/bdwgc" +import ( + "runtime" + + "github.com/goplus/llgo/runtime/internal/clite/bdwgc" +) + +func ReadMemStats(m *runtime.MemStats) { + panic("todo: runtime.ReadMemStats") +} func GC() { bdwgc.Gcollect() diff --git a/runtime/internal/lib/runtime/runtime_gc_baremetal.go b/runtime/internal/lib/runtime/runtime_gc_baremetal.go index b6192a44..43ab5573 100644 --- a/runtime/internal/lib/runtime/runtime_gc_baremetal.go +++ b/runtime/internal/lib/runtime/runtime_gc_baremetal.go @@ -2,7 +2,25 @@ package runtime -import "github.com/goplus/llgo/runtime/internal/runtime/tinygogc" +import ( + "runtime" + + "github.com/goplus/llgo/runtime/internal/runtime/tinygogc" +) + +func ReadMemStats(m *runtime.MemStats) { + stats := tinygogc.ReadGCStats() + m.StackInuse = stats.StackInuse + m.StackSys = stats.StackSys + m.HeapSys = stats.HeapSys + m.GCSys = stats.GCSys + m.TotalAlloc = stats.TotalAlloc + m.Mallocs = stats.Mallocs + m.Frees = stats.Frees + m.Sys = stats.Sys + m.HeapAlloc = stats.HeapAlloc + m.Alloc = stats.Alloc +} func GC() { tinygogc.GC() diff --git a/runtime/internal/runtime/tinygogc/gc.go b/runtime/internal/runtime/tinygogc/gc.go index bfe4149e..5bb673b7 100644 --- a/runtime/internal/runtime/tinygogc/gc.go +++ b/runtime/internal/runtime/tinygogc/gc.go @@ -20,3 +20,180 @@ func __wrap_calloc(size uintptr) unsafe.Pointer { func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { return Realloc(ptr, size) } + +type GCStats struct { + // General statistics. + + // Alloc is bytes of allocated heap objects. + // + // This is the same as HeapAlloc (see below). + Alloc uint64 + + // TotalAlloc is cumulative bytes allocated for heap objects. + // + // TotalAlloc increases as heap objects are allocated, but + // unlike Alloc and HeapAlloc, it does not decrease when + // objects are freed. + TotalAlloc uint64 + + // Sys is the total bytes of memory obtained from the OS. + // + // Sys is the sum of the XSys fields below. Sys measures the + // virtual address space reserved by the Go runtime for the + // heap, stacks, and other internal data structures. It's + // likely that not all of the virtual address space is backed + // by physical memory at any given moment, though in general + // it all was at some point. + Sys uint64 + + // Mallocs is the cumulative count of heap objects allocated. + // The number of live objects is Mallocs - Frees. + Mallocs uint64 + + // Frees is the cumulative count of heap objects freed. + Frees uint64 + + // Heap memory statistics. + // + // Interpreting the heap statistics requires some knowledge of + // how Go organizes memory. Go divides the virtual address + // space of the heap into "spans", which are contiguous + // regions of memory 8K or larger. A span may be in one of + // three states: + // + // An "idle" span contains no objects or other data. The + // physical memory backing an idle span can be released back + // to the OS (but the virtual address space never is), or it + // can be converted into an "in use" or "stack" span. + // + // An "in use" span contains at least one heap object and may + // have free space available to allocate more heap objects. + // + // A "stack" span is used for goroutine stacks. Stack spans + // are not considered part of the heap. A span can change + // between heap and stack memory; it is never used for both + // simultaneously. + + // HeapAlloc is bytes of allocated heap objects. + // + // "Allocated" heap objects include all reachable objects, as + // well as unreachable objects that the garbage collector has + // not yet freed. Specifically, HeapAlloc increases as heap + // objects are allocated and decreases as the heap is swept + // and unreachable objects are freed. Sweeping occurs + // incrementally between GC cycles, so these two processes + // occur simultaneously, and as a result HeapAlloc tends to + // change smoothly (in contrast with the sawtooth that is + // typical of stop-the-world garbage collectors). + HeapAlloc uint64 + + // HeapSys is bytes of heap memory obtained from the OS. + // + // HeapSys measures the amount of virtual address space + // reserved for the heap. This includes virtual address space + // that has been reserved but not yet used, which consumes no + // physical memory, but tends to be small, as well as virtual + // address space for which the physical memory has been + // returned to the OS after it became unused (see HeapReleased + // for a measure of the latter). + // + // HeapSys estimates the largest size the heap has had. + HeapSys uint64 + + // HeapIdle is bytes in idle (unused) spans. + // + // Idle spans have no objects in them. These spans could be + // (and may already have been) returned to the OS, or they can + // be reused for heap allocations, or they can be reused as + // stack memory. + // + // HeapIdle minus HeapReleased estimates the amount of memory + // that could be returned to the OS, but is being retained by + // the runtime so it can grow the heap without requesting more + // memory from the OS. If this difference is significantly + // larger than the heap size, it indicates there was a recent + // transient spike in live heap size. + HeapIdle uint64 + + // HeapInuse is bytes in in-use spans. + // + // In-use spans have at least one object in them. These spans + // can only be used for other objects of roughly the same + // size. + // + // HeapInuse minus HeapAlloc estimates the amount of memory + // that has been dedicated to particular size classes, but is + // not currently being used. This is an upper bound on + // fragmentation, but in general this memory can be reused + // efficiently. + HeapInuse uint64 + + // Stack memory statistics. + // + // Stacks are not considered part of the heap, but the runtime + // can reuse a span of heap memory for stack memory, and + // vice-versa. + + // StackInuse is bytes in stack spans. + // + // In-use stack spans have at least one stack in them. These + // spans can only be used for other stacks of the same size. + // + // There is no StackIdle because unused stack spans are + // returned to the heap (and hence counted toward HeapIdle). + StackInuse uint64 + + // StackSys is bytes of stack memory obtained from the OS. + // + // StackSys is StackInuse, plus any memory obtained directly + // from the OS for OS thread stacks. + // + // In non-cgo programs this metric is currently equal to StackInuse + // (but this should not be relied upon, and the value may change in + // the future). + // + // In cgo programs this metric includes OS thread stacks allocated + // directly from the OS. Currently, this only accounts for one stack in + // c-shared and c-archive build modes and other sources of stacks from + // the OS (notably, any allocated by C code) are not currently measured. + // Note this too may change in the future. + StackSys uint64 + + // GCSys is bytes of memory in garbage collection metadata. + GCSys uint64 +} + +func ReadGCStats() GCStats { + var heapInuse, heapIdle uint64 + + lock(&gcMutex) + + for block := uintptr(0); block < endBlock; block++ { + bstate := gcStateOf(block) + if bstate == blockStateFree { + heapIdle += uint64(bytesPerBlock) + } else { + heapInuse += uint64(bytesPerBlock) + } + } + + stackEnd := uintptr(unsafe.Pointer(&_stackEnd)) + stackSys := stackTop - stackEnd + + stats := GCStats{ + StackInuse: uint64(stackTop - uintptr(getsp())), + StackSys: uint64(stackSys), + HeapSys: heapInuse + heapIdle, + GCSys: uint64(heapEnd - uintptr(metadataStart)), + TotalAlloc: gcTotalAlloc, + Mallocs: gcMallocs, + Frees: gcFrees, + Sys: uint64(heapEnd - heapStart), + HeapAlloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock), + Alloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock), + } + + unlock(&gcMutex) + + return stats +} diff --git a/runtime/internal/runtime/tinygogc/gc_llgo.go b/runtime/internal/runtime/tinygogc/gc_link.go similarity index 92% rename from runtime/internal/runtime/tinygogc/gc_llgo.go rename to runtime/internal/runtime/tinygogc/gc_link.go index 5f2d73e8..31e025c0 100644 --- a/runtime/internal/runtime/tinygogc/gc_llgo.go +++ b/runtime/internal/runtime/tinygogc/gc_link.go @@ -28,6 +28,9 @@ var _heapEnd [0]byte //go:linkname _stackStart _stack_top var _stackStart [0]byte +//go:linkname _stackEnd _stack_end +var _stackEnd [0]byte + //go:linkname _globals_start _globals_start var _globals_start [0]byte diff --git a/runtime/internal/runtime/tinygogc/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go index 1081dafb..5aa476ae 100644 --- a/runtime/internal/runtime/tinygogc/gc_tinygo.go +++ b/runtime/internal/runtime/tinygogc/gc_tinygo.go @@ -50,7 +50,6 @@ var ( stackTop uintptr // the top of stack endBlock uintptr // GC end block index metadataStart unsafe.Pointer // start address of GC metadata - isGCInit bool nextAlloc uintptr // the next block that should be tried by the allocator gcTotalAlloc uint64 // total number of bytes allocated @@ -65,6 +64,9 @@ var ( // zeroSizedAlloc is just a sentinel that gets returned when allocating 0 bytes. zeroSizedAlloc uint8 + + gcMutex mutex // gcMutex protects GC related variables + isGCInit bool // isGCInit indicates GC initialization state ) // Some globals + constants for the entire GC. @@ -220,16 +222,22 @@ func isOnHeap(ptr uintptr) bool { return ptr >= heapStart && ptr < uintptr(metadataStart) } +func isPointer(ptr uintptr) bool { + // TODO: implement precise GC + return isOnHeap(ptr) +} + // alloc tries to find some free space on the heap, possibly doing a garbage // collection cycle if needed. If no space is free, it panics. // //go:noinline func Alloc(size uintptr) unsafe.Pointer { - lazyInit() - if size == 0 { return unsafe.Pointer(&zeroSizedAlloc) } + lock(&gcMutex) + lazyInit() + gcTotalAlloc += uint64(size) gcMallocs++ @@ -250,7 +258,7 @@ func Alloc(size uintptr) unsafe.Pointer { // could be found. Run a garbage collection cycle to reclaim // free memory and try again. heapScanCount = 2 - freeBytes := GC() + freeBytes := gc() heapSize := uintptr(metadataStart) - heapStart if freeBytes < heapSize/3 { // Ensure there is at least 33% headroom. @@ -308,7 +316,7 @@ func Alloc(size uintptr) unsafe.Pointer { for i := thisAlloc + 1; i != nextAlloc; i++ { gcSetState(i, blockStateTail) } - + unlock(&gcMutex) // Return a pointer to this allocation. return memset(gcPointerOf(thisAlloc), 0, size) } @@ -316,10 +324,12 @@ func Alloc(size uintptr) unsafe.Pointer { } func Realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { - lazyInit() if ptr == nil { return Alloc(size) } + lock(&gcMutex) + lazyInit() + unlock(&gcMutex) ptrAddress := uintptr(ptr) endOfTailAddress := gcAddressOf(gcFindNext(blockFromAddr(ptrAddress))) @@ -342,10 +352,16 @@ func free(ptr unsafe.Pointer) { // TODO: free blocks on request, when the compiler knows they're unused. } +func GC() { + lock(&gcMutex) + gc() + unlock(&gcMutex) +} + // runGC performs a garbage collection cycle. It is the internal implementation // of the runtime.GC() function. The difference is that it returns the number of // free bytes in the heap after the GC is finished. -func GC() (freeBytes uintptr) { +func gc() (freeBytes uintptr) { lazyInit() if gcDebug { @@ -403,7 +419,7 @@ func startMark(root uintptr) { // Load the word. word := *(*uintptr)(unsafe.Pointer(addr)) - if !isOnHeap(word) { + if !isPointer(word) { // Not a heap pointer. continue } diff --git a/runtime/internal/runtime/tinygogc/mutex.go b/runtime/internal/runtime/tinygogc/mutex.go new file mode 100644 index 00000000..76d6fbbb --- /dev/null +++ b/runtime/internal/runtime/tinygogc/mutex.go @@ -0,0 +1,7 @@ +package tinygogc + +type mutex struct{} + +func lock(m *mutex) {} + +func unlock(m *mutex) {} diff --git a/targets/esp32.app.elf.ld b/targets/esp32.app.elf.ld index 94b90611..7bd5e38d 100755 --- a/targets/esp32.app.elf.ld +++ b/targets/esp32.app.elf.ld @@ -27,6 +27,7 @@ SECTIONS .stack (NOLOAD) : { + _stack_end = .; . = ALIGN(16); . += 16K; __stack = .; From b36be05c1e6e9bc092516b712706b806b83d40aa Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 19 Sep 2025 11:37:10 +0800 Subject: [PATCH 06/15] fix: GC() signature --- runtime/internal/runtime/tinygogc/gc_tinygo.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/runtime/internal/runtime/tinygogc/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go index 5aa476ae..a2a71aa5 100644 --- a/runtime/internal/runtime/tinygogc/gc_tinygo.go +++ b/runtime/internal/runtime/tinygogc/gc_tinygo.go @@ -352,10 +352,11 @@ func free(ptr unsafe.Pointer) { // TODO: free blocks on request, when the compiler knows they're unused. } -func GC() { +func GC() uintptr { lock(&gcMutex) - gc() + freeBytes := gc() unlock(&gcMutex) + return freeBytes } // runGC performs a garbage collection cycle. It is the internal implementation From c46ca84122f4e384c94b65a62e337223e24bd141 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 13 Nov 2025 11:31:27 +0800 Subject: [PATCH 07/15] revert disabling stdio buffer --- runtime/internal/clite/pthread/pthread_gc.go | 3 ++- runtime/internal/clite/pthread/pthread_nogc.go | 3 ++- runtime/internal/clite/stdio_baremetal.go | 13 ------------- runtime/internal/clite/stdio_darwin.go | 3 ++- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/runtime/internal/clite/pthread/pthread_gc.go b/runtime/internal/clite/pthread/pthread_gc.go index 4d33b5a2..32c7823e 100644 --- a/runtime/internal/clite/pthread/pthread_gc.go +++ b/runtime/internal/clite/pthread/pthread_gc.go @@ -1,4 +1,5 @@ -//go:build !nogc && !baremetal +//go:build !nogc +// +build !nogc /* * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/clite/pthread/pthread_nogc.go b/runtime/internal/clite/pthread/pthread_nogc.go index 09acb0ac..594e1b97 100644 --- a/runtime/internal/clite/pthread/pthread_nogc.go +++ b/runtime/internal/clite/pthread/pthread_nogc.go @@ -1,4 +1,5 @@ -//go:build nogc || baremetal +//go:build nogc +// +build nogc /* * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/clite/stdio_baremetal.go b/runtime/internal/clite/stdio_baremetal.go index f318c065..cd9a649e 100644 --- a/runtime/internal/clite/stdio_baremetal.go +++ b/runtime/internal/clite/stdio_baremetal.go @@ -25,16 +25,3 @@ import ( var Stdin FilePtr = Fopen(Str("/dev/stdin"), Str("r")) var Stdout FilePtr = Fopen(Str("/dev/stdout"), Str("w")) var Stderr FilePtr = Stdout - -//go:linkname setvbuf C.setvbuf -func setvbuf(fp FilePtr, buf *Char, typ Int, size SizeT) - -const ( - _IONBF = 2 // No buffering - immediate output -) - -func init() { - // Disable buffering for baremetal targets to ensure immediate output - setvbuf(Stdout, nil, _IONBF, 0) - setvbuf(Stdin, nil, _IONBF, 0) -} diff --git a/runtime/internal/clite/stdio_darwin.go b/runtime/internal/clite/stdio_darwin.go index cd30c073..324403f5 100644 --- a/runtime/internal/clite/stdio_darwin.go +++ b/runtime/internal/clite/stdio_darwin.go @@ -1,4 +1,5 @@ -//go:build darwin && !baremetal +//go:build darwin +// +build darwin /* * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. From 1ed924ed5059235301948fffe18b56692a112e35 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 13 Nov 2025 11:32:51 +0800 Subject: [PATCH 08/15] fix: add pthread GC support for baremetal test: fix test logic chore: format codes --- runtime/internal/clite/pthread/pthread_gc.go | 3 +- .../internal/clite/pthread/pthread_nogc.go | 3 +- .../internal/runtime/tinygogc/pc_mock_test.go | 273 ++++++++++++------ 3 files changed, 193 insertions(+), 86 deletions(-) diff --git a/runtime/internal/clite/pthread/pthread_gc.go b/runtime/internal/clite/pthread/pthread_gc.go index 32c7823e..4d33b5a2 100644 --- a/runtime/internal/clite/pthread/pthread_gc.go +++ b/runtime/internal/clite/pthread/pthread_gc.go @@ -1,5 +1,4 @@ -//go:build !nogc -// +build !nogc +//go:build !nogc && !baremetal /* * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/clite/pthread/pthread_nogc.go b/runtime/internal/clite/pthread/pthread_nogc.go index 594e1b97..09acb0ac 100644 --- a/runtime/internal/clite/pthread/pthread_nogc.go +++ b/runtime/internal/clite/pthread/pthread_nogc.go @@ -1,5 +1,4 @@ -//go:build nogc -// +build nogc +//go:build nogc || baremetal /* * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/runtime/tinygogc/pc_mock_test.go b/runtime/internal/runtime/tinygogc/pc_mock_test.go index 9c37b136..dde398ce 100644 --- a/runtime/internal/runtime/tinygogc/pc_mock_test.go +++ b/runtime/internal/runtime/tinygogc/pc_mock_test.go @@ -19,8 +19,8 @@ type testObject struct { data [4]uintptr } -// mockMemoryLayout simulates the memory layout of an embedded system -type mockMemoryLayout struct { +// mockGCEnv provides a controlled root environment for GC testing +type mockGCEnv struct { memory []byte heapStart uintptr heapEnd uintptr @@ -28,15 +28,29 @@ type mockMemoryLayout struct { globalsEnd uintptr stackStart uintptr stackEnd uintptr + // Controlled root sets for testing + rootObjects []unsafe.Pointer + // Original GC state to restore + originalHeapStart uintptr + originalHeapEnd uintptr + originalGlobalsStart uintptr + originalGlobalsEnd uintptr + originalStackTop uintptr + originalEndBlock uintptr + originalMetadataStart unsafe.Pointer + originalNextAlloc uintptr + originalIsGCInit bool + // Mock mode flag + mockMode bool } -// createMockMemoryLayout creates a simulated 128KB memory environment -func createMockMemoryLayout() *mockMemoryLayout { +// createMockGCEnv creates a completely isolated GC environment +func createMockGCEnv() *mockGCEnv { totalMemory := mockHeapSize + mockGlobalsSize + mockStackSize memory := make([]byte, totalMemory) baseAddr := uintptr(unsafe.Pointer(&memory[0])) - layout := &mockMemoryLayout{ + env := &mockGCEnv{ memory: memory, globalsStart: baseAddr, globalsEnd: baseAddr + mockGlobalsSize, @@ -44,42 +58,123 @@ func createMockMemoryLayout() *mockMemoryLayout { heapEnd: baseAddr + mockGlobalsSize + mockHeapSize, stackStart: baseAddr + mockGlobalsSize + mockHeapSize, stackEnd: baseAddr + uintptr(totalMemory), + rootObjects: make([]unsafe.Pointer, 0), + mockMode: false, } - return layout + return env } -// setupMockGC initializes the GC with mock memory layout -func (m *mockMemoryLayout) setupMockGC() { - // Set mock values - heapStart = m.heapStart - heapEnd = m.heapEnd - globalsStart = m.globalsStart - globalsEnd = m.globalsEnd - stackTop = m.stackEnd +// setupMockGC initializes the GC with mock memory layout using initGC's logic +func (env *mockGCEnv) setupMockGC() { + // Save original GC state + env.originalHeapStart = heapStart + env.originalHeapEnd = heapEnd + env.originalGlobalsStart = globalsStart + env.originalGlobalsEnd = globalsEnd + env.originalStackTop = stackTop + env.originalEndBlock = endBlock + env.originalMetadataStart = metadataStart + env.originalNextAlloc = nextAlloc + env.originalIsGCInit = isGCInit - // Set currentStack to the start of the mock stack - currentStack = m.stackStart + // Set currentStack for getsp() + currentStack = env.stackStart + + // Apply initGC's logic with our mock memory layout + // This is the same logic as initGC() but with our mock addresses + heapStart = env.heapStart + 2048 // reserve 2K blocks like initGC does + heapEnd = env.heapEnd + globalsStart = env.globalsStart + globalsEnd = env.globalsEnd + stackTop = env.stackEnd - // Calculate metadata layout totalSize := heapEnd - heapStart metadataSize := (totalSize + blocksPerStateByte*bytesPerBlock) / (1 + blocksPerStateByte*bytesPerBlock) metadataStart = unsafe.Pointer(heapEnd - metadataSize) endBlock = (uintptr(metadataStart) - heapStart) / bytesPerBlock - // Clear metadata - metadataBytes := (*[1024]byte)(metadataStart)[:metadataSize:metadataSize] - for i := range metadataBytes { - metadataBytes[i] = 0 - } + // Clear metadata using memset like initGC does + memset(metadataStart, 0, metadataSize) - // Reset allocator state + // Reset allocator state (initGC doesn't reset these, but we need to) nextAlloc = 0 isGCInit = true } +// restoreOriginalGC restores the original GC state +func (env *mockGCEnv) restoreOriginalGC() { + heapStart = env.originalHeapStart + heapEnd = env.originalHeapEnd + globalsStart = env.originalGlobalsStart + globalsEnd = env.originalGlobalsEnd + stackTop = env.originalStackTop + endBlock = env.originalEndBlock + metadataStart = env.originalMetadataStart + nextAlloc = env.originalNextAlloc + isGCInit = env.originalIsGCInit +} + +// enableMockMode enables mock root scanning mode +func (env *mockGCEnv) enableMockMode() { + env.mockMode = true +} + +// disableMockMode disables mock root scanning mode +func (env *mockGCEnv) disableMockMode() { + env.mockMode = false +} + +// addRoot adds an object to the controlled root set +func (env *mockGCEnv) addRoot(ptr unsafe.Pointer) { + env.rootObjects = append(env.rootObjects, ptr) +} + +// clearRoots removes all objects from the controlled root set +func (env *mockGCEnv) clearRoots() { + env.rootObjects = env.rootObjects[:0] +} + +// mockMarkReachable replaces gcMarkReachable when in mock mode +func (env *mockGCEnv) mockMarkReachable() { + if !env.mockMode { + // Use original logic + markRoots(uintptr(getsp()), stackTop) + markRoots(globalsStart, globalsEnd) + return + } + + // Mock mode: only scan our controlled roots + for _, root := range env.rootObjects { + addr := uintptr(root) + markRoot(addr, addr) + } +} + +// runMockGC runs standard GC but with controlled root scanning +func (env *mockGCEnv) runMockGC() uintptr { + lock(&gcMutex) + defer unlock(&gcMutex) + + lazyInit() + + if gcDebug { + println("running mock collection cycle...") + } + + // Mark phase: use our mock root scanning + env.mockMarkReachable() + finishMark() + + // Resume world (no-op in single threaded) + gcResumeWorld() + + // Sweep phase: use standard sweep logic + return sweep() +} + // createTestObjects creates a network of objects for testing reachability -func createTestObjects(layout *mockMemoryLayout) []*testObject { +func createTestObjects(env *mockGCEnv) []*testObject { // Allocate several test objects objects := make([]*testObject, 0, 10) @@ -119,20 +214,10 @@ func createTestObjects(layout *mockMemoryLayout) []*testObject { return objects } -// mockStackScan simulates scanning stack for root pointers -func mockStackScan(roots []*testObject) { - // Simulate stack by creating local variables pointing to roots - - for _, root := range roots[:2] { // Only first 2 are actually roots - addr := uintptr(unsafe.Pointer(&root)) - ptr := uintptr(unsafe.Pointer(root)) - markRoot(addr, ptr) - } -} - func TestMockGCBasicAllocation(t *testing.T) { - layout := createMockMemoryLayout() - layout.setupMockGC() + env := createMockGCEnv() + env.setupMockGC() + defer env.restoreOriginalGC() // Test basic allocation ptr1 := Alloc(32) @@ -162,18 +247,23 @@ func TestMockGCBasicAllocation(t *testing.T) { } func TestMockGCReachabilityAndSweep(t *testing.T) { - layout := createMockMemoryLayout() - layout.setupMockGC() + env := createMockGCEnv() + env.setupMockGC() + defer env.restoreOriginalGC() // Track initial stats initialMallocs := gcMallocs initialFrees := gcFrees // Create test object network - objects := createTestObjects(layout) - roots := objects[:2] // First 2 are roots + objects := createTestObjects(env) - t.Logf("Created %d objects, %d are roots", len(objects), len(roots)) + // Add first 2 objects as roots using mock control + env.enableMockMode() + env.addRoot(unsafe.Pointer(objects[0])) // root1 + env.addRoot(unsafe.Pointer(objects[1])) // root2 + + t.Logf("Created %d objects, 2 are roots", len(objects)) t.Logf("Mallocs: %d", gcMallocs-initialMallocs) // Verify all objects are initially allocated @@ -186,13 +276,8 @@ func TestMockGCReachabilityAndSweep(t *testing.T) { } } - // Perform GC with manual root scanning - // Mark reachable objects first - mockStackScan(roots) - finishMark() - - // Then sweep unreachable objects - freedBytes := sweep() + // Perform GC with controlled root scanning + freedBytes := env.runMockGC() t.Logf("Freed %d bytes during GC", freedBytes) t.Logf("Frees: %d (delta: %d)", gcFrees, gcFrees-initialFrees) @@ -238,36 +323,32 @@ func TestMockGCReachabilityAndSweep(t *testing.T) { t.Error("Expected some objects to be freed, but free count didn't change") } - // clear ref for grandchild - objects[2].data[0] = 0 - objects[3].data[0] = 0 + // Clear refs to make grandchild1 unreachable + objects[2].data[0] = 0 // child1 -> grandchild1 + objects[3].data[0] = 0 // child2 -> grandchild1 - // Perform GC with manual root scanning - // Mark reachable objects first - mockStackScan(roots) - finishMark() - - // Then sweep unreachable objects - freedBytes = sweep() + // Run GC again with same roots + freedBytes = env.runMockGC() + // child2 should still be reachable (through root1) blockAddr := blockFromAddr(uintptr(unsafe.Pointer(objects[3]))) - state := gcStateOf(blockAddr) if state != blockStateHead { - t.Errorf("Unreachable object %d at %x has state %d, expected %d (HEAD)", 3, blockAddr, state, blockStateHead) + t.Errorf("Object child2 at %x has state %d, expected %d (HEAD)", blockAddr, state, blockStateHead) } + // grandchild1 should now be unreachable and freed blockAddr = blockFromAddr(uintptr(unsafe.Pointer(objects[4]))) - state = gcStateOf(blockAddr) if state != blockStateFree { - t.Errorf("Reachable object %d at %x has state %d, expected %d (HEAD)", 4, blockAddr, state, blockStateHead) + t.Errorf("Object grandchild1 at %x has state %d, expected %d (FREE)", blockAddr, state, blockStateFree) } } func TestMockGCMemoryPressure(t *testing.T) { - layout := createMockMemoryLayout() - layout.setupMockGC() + env := createMockGCEnv() + env.setupMockGC() + defer env.restoreOriginalGC() // Calculate available heap space heapSize := uintptr(metadataStart) - heapStart @@ -295,12 +376,17 @@ func TestMockGCMemoryPressure(t *testing.T) { initialMallocs := gcMallocs t.Logf("Allocated %d objects (%d mallocs total)", len(allocations), initialMallocs) - // Clear references to half the allocations (make them garbage) - garbageCount := len(allocations) / 2 - allocations = allocations[garbageCount:] + // Enable mock mode and keep only half the allocations as roots + env.enableMockMode() + keepCount := len(allocations) / 2 + for i := 0; i < keepCount; i++ { + env.addRoot(allocations[i]) + } - // Force GC - freeBytes := GC() + t.Logf("Keeping %d objects as roots, %d should be freed", keepCount, len(allocations)-keepCount) + + // Force GC with controlled roots + freeBytes := env.runMockGC() t.Logf("GC freed %d bytes", freeBytes) t.Logf("Objects freed: %d", gcFrees) @@ -317,8 +403,9 @@ func TestMockGCMemoryPressure(t *testing.T) { } func TestMockGCCircularReferences(t *testing.T) { - layout := createMockMemoryLayout() - layout.setupMockGC() + env := createMockGCEnv() + env.setupMockGC() + defer env.restoreOriginalGC() type Node struct { data [3]uintptr @@ -350,25 +437,47 @@ func TestMockGCCircularReferences(t *testing.T) { } } - // Clear references (make entire circle unreachable) - // for i := range nodes { - // nodes[zi] = nil - // } + // Test 1: With root references - objects should NOT be freed + env.enableMockMode() + // Add the first node as root (keeps entire circle reachable) + env.addRoot(unsafe.Pointer(nodes[0])) - // Force GC without roots - freeBytes := GC() + freeBytes := env.runMockGC() + t.Logf("GC with root reference freed %d bytes", freeBytes) - t.Logf("GC freed %d bytes", freeBytes) + // All nodes should still be allocated since they're reachable through the root + for i, node := range nodes { + addr := uintptr(unsafe.Pointer(node)) + block := blockFromAddr(addr) + state := gcStateOf(block) + if state != blockStateHead { + t.Errorf("Node %d at %x should still be allocated, but has state %d", i, addr, state) + } + } - // All nodes should now be freed since they're not reachable - // Note: We can't check the specific nodes since we cleared the references, - // but we can verify that significant memory was freed + // Test 2: Without root references - all circular objects should be freed + env.clearRoots() // Remove all root references + + freeBytes = env.runMockGC() + t.Logf("GC without roots freed %d bytes", freeBytes) + + // All nodes should now be freed since they're not reachable from any roots expectedFreed := uintptr(len(nodes)) * ((unsafe.Sizeof(Node{}) + bytesPerBlock - 1) / bytesPerBlock) * bytesPerBlock if freeBytes < expectedFreed { t.Errorf("Expected at least %d bytes freed, got %d", expectedFreed, freeBytes) } + // Verify all nodes are actually freed + for i, node := range nodes { + addr := uintptr(unsafe.Pointer(node)) + block := blockFromAddr(addr) + state := gcStateOf(block) + if state != blockStateFree { + t.Errorf("Node %d at %x should be freed, but has state %d", i, addr, state) + } + } + // Verify we can allocate new objects in the freed space newPtr := Alloc(unsafe.Sizeof(Node{})) if newPtr == nil { From eec2c271bd7bd7d47df15449323a1cc6dbef50fb Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 13 Nov 2025 20:20:41 +0800 Subject: [PATCH 09/15] feat: replace println with gcPanic --- runtime/internal/runtime/tinygogc/gc_link.go | 3 +-- .../internal/runtime/tinygogc/gc_tinygo.go | 25 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/runtime/internal/runtime/tinygogc/gc_link.go b/runtime/internal/runtime/tinygogc/gc_link.go index 31e025c0..818ce249 100644 --- a/runtime/internal/runtime/tinygogc/gc_link.go +++ b/runtime/internal/runtime/tinygogc/gc_link.go @@ -10,8 +10,7 @@ import ( //go:linkname getsp llgo.stackSave func getsp() unsafe.Pointer -// when executing initGC(), we must ensure there's no any allocations. -// use linking here to avoid import clite +// link here for testing // //go:linkname memset C.memset func memset(unsafe.Pointer, int, uintptr) unsafe.Pointer diff --git a/runtime/internal/runtime/tinygogc/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go index a2a71aa5..e57a7e19 100644 --- a/runtime/internal/runtime/tinygogc/gc_tinygo.go +++ b/runtime/internal/runtime/tinygogc/gc_tinygo.go @@ -20,6 +20,8 @@ package tinygogc import ( "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" ) const gcDebug = false @@ -102,11 +104,16 @@ func lazyInit() { } } +func gcPanic(s *c.Char) { + c.Printf(c.Str("%s"), s) + c.Exit(2) +} + // blockFromAddr returns a block given an address somewhere in the heap (which // might not be heap-aligned). func blockFromAddr(addr uintptr) uintptr { if addr < heapStart || addr >= uintptr(metadataStart) { - println("gc: trying to get block from invalid address") + gcPanic(c.Str("gc: trying to get block from invalid address")) } return (addr - heapStart) / bytesPerBlock } @@ -120,7 +127,7 @@ func gcPointerOf(blockAddr uintptr) unsafe.Pointer { func gcAddressOf(blockAddr uintptr) uintptr { addr := heapStart + blockAddr*bytesPerBlock if addr > uintptr(metadataStart) { - println("gc: block pointing inside metadata") + gcPanic(c.Str("gc: block pointing inside metadata")) } return addr } @@ -151,7 +158,7 @@ func gcFindHead(blockAddr uintptr) uintptr { blockAddr-- } if gcStateOf(blockAddr) != blockStateHead && gcStateOf(blockAddr) != blockStateMark { - println("gc: found tail without head") + gcPanic(c.Str("gc: found tail without head")) } return blockAddr } @@ -190,7 +197,7 @@ func gcSetState(blockAddr uintptr, newState uint8) { stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte)) *stateBytePtr |= uint8(newState << ((blockAddr % blocksPerStateByte) * stateBits)) if gcStateOf(blockAddr) != newState { - println("gc: setState() was not successful") + gcPanic(c.Str("gc: setState() was not successful")) } } @@ -199,7 +206,7 @@ func gcMarkFree(blockAddr uintptr) { stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte)) *stateBytePtr &^= uint8(blockStateMask << ((blockAddr % blocksPerStateByte) * stateBits)) if gcStateOf(blockAddr) != blockStateFree { - println("gc: markFree() was not successful") + gcPanic(c.Str("gc: markFree() was not successful")) } *(*[wordsPerBlock]uintptr)(unsafe.Pointer(gcAddressOf(blockAddr))) = [wordsPerBlock]uintptr{} } @@ -208,13 +215,13 @@ func gcMarkFree(blockAddr uintptr) { // before calling this function. func gcUnmark(blockAddr uintptr) { if gcStateOf(blockAddr) != blockStateMark { - println("gc: unmark() on a block that is not marked") + gcPanic(c.Str("gc: unmark() on a block that is not marked")) } clearMask := blockStateMask ^ blockStateHead // the bits to clear from the state stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte)) *stateBytePtr &^= uint8(clearMask << ((blockAddr % blocksPerStateByte) * stateBits)) if gcStateOf(blockAddr) != blockStateHead { - println("gc: unmark() was not successful") + gcPanic(c.Str("gc: unmark() was not successful")) } } @@ -276,7 +283,7 @@ func Alloc(size uintptr) unsafe.Pointer { // Unfortunately the heap could not be increased. This // happens on baremetal systems for example (where all // available RAM has already been dedicated to the heap). - println("out of memory") + gcPanic(c.Str("out of memory")) } } } @@ -391,7 +398,7 @@ func gc() (freeBytes uintptr) { // must be aligned. func markRoots(start, end uintptr) { if start >= end { - println("gc: unexpected range to mark") + gcPanic(c.Str("gc: unexpected range to mark")) } // Reduce the end bound to avoid reading too far on platforms where pointer alignment is smaller than pointer size. // If the size of the range is 0, then end will be slightly below start after this. From bb29e8c76898b46de23689108064598414acdaea Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 13 Nov 2025 20:23:15 +0800 Subject: [PATCH 10/15] docs: add commets for gc mutex docs: add commets for tinygo gc --- runtime/internal/runtime/tinygogc/gc.go | 12 ++++++++++-- runtime/internal/runtime/tinygogc/gc_tinygo.go | 14 ++++++++++++++ runtime/internal/runtime/tinygogc/mutex.go | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/runtime/internal/runtime/tinygogc/gc.go b/runtime/internal/runtime/tinygogc/gc.go index 5bb673b7..965bc3bf 100644 --- a/runtime/internal/runtime/tinygogc/gc.go +++ b/runtime/internal/runtime/tinygogc/gc.go @@ -4,6 +4,9 @@ package tinygogc import "unsafe" +// LLGoPackage instructs the LLGo linker to wrap C standard library memory allocation +// functions (malloc, realloc, calloc) so they use the tinygogc allocator instead. +// This ensures all memory allocations go through the GC, including C library calls. const LLGoPackage = "link: --wrap=malloc --wrap=realloc --wrap=calloc" //export __wrap_malloc @@ -12,8 +15,13 @@ func __wrap_malloc(size uintptr) unsafe.Pointer { } //export __wrap_calloc -func __wrap_calloc(size uintptr) unsafe.Pointer { - return Alloc(size) +func __wrap_calloc(nmemb, size uintptr) unsafe.Pointer { + totalSize := nmemb * size + // Check for multiplication overflow + if nmemb != 0 && totalSize/nmemb != size { + return nil // Overflow + } + return Alloc(totalSize) } //export __wrap_realloc diff --git a/runtime/internal/runtime/tinygogc/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go index e57a7e19..d36f1806 100644 --- a/runtime/internal/runtime/tinygogc/gc_tinygo.go +++ b/runtime/internal/runtime/tinygogc/gc_tinygo.go @@ -16,6 +16,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +// Package tinygogc implements a conservative mark-and-sweep garbage collector +// for baremetal environments 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 +// - testGC: Enables testing mode with mock implementations +// +// 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). package tinygogc import ( diff --git a/runtime/internal/runtime/tinygogc/mutex.go b/runtime/internal/runtime/tinygogc/mutex.go index 76d6fbbb..ba13813a 100644 --- a/runtime/internal/runtime/tinygogc/mutex.go +++ b/runtime/internal/runtime/tinygogc/mutex.go @@ -1,5 +1,6 @@ package tinygogc +// TODO(MeteorsLiu): mutex lock for baremetal GC type mutex struct{} func lock(m *mutex) {} From af27d0475d4f72794a3bec1122b233b294ff87eb Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 13 Nov 2025 20:30:19 +0800 Subject: [PATCH 11/15] revert some unnecessary change --- internal/build/build.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/build/build.go b/internal/build/build.go index e0f18ea8..1147d495 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -753,6 +753,7 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { } func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global llssa.Package, outputPath string, verbose bool) error { + needRuntime := false needPyInit := false pkgsMap := make(map[*packages.Package]*aPackage, len(pkgs)) From 7f1e07755a9e7c5c2424662618813f7654134d60 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 14 Nov 2025 11:07:15 +0800 Subject: [PATCH 12/15] ci: use llgo test instead ci: use llgo test instead --- .github/workflows/go.yml | 5 ----- .github/workflows/llgo.yml | 8 ++++++-- runtime/internal/runtime/tinygogc/gc_link.go | 8 -------- runtime/internal/runtime/tinygogc/gc_test.go | 12 ------------ runtime/internal/runtime/tinygogc/gc_tinygo.go | 6 +++--- runtime/internal/runtime/tinygogc/pc_mock_test.go | 4 +++- 6 files changed, 12 insertions(+), 31 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e09403fe..2e706690 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -52,11 +52,6 @@ jobs: if: ${{!startsWith(matrix.os, 'macos')}} run: go test ./... - - name: Test Baremetal GC - if: ${{!startsWith(matrix.os, 'macos')}} - working-directory: runtime/internal/runtime/tinygogc - run: go test -tags testGC . - - name: Test with coverage if: startsWith(matrix.os, 'macos') run: go test -coverprofile="coverage.txt" -covermode=atomic ./... diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 4441d04a..124b2cd2 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -61,7 +61,7 @@ jobs: if ${{ startsWith(matrix.os, 'macos') }}; then DEMO_PKG="cargs_darwin_arm64.zip" else - DEMO_PKG="cargs_linux_amd64.zip" + DEMO_PKG="cargs_linux_amd64.zip" fi mkdir -p ./_demo/c/cargs/libs @@ -186,11 +186,15 @@ jobs: uses: actions/setup-go@v6 with: go-version: ${{matrix.go}} - + - name: Test Baremetal GC + if: ${{!startsWith(matrix.os, 'macos')}} + working-directory: runtime/internal/runtime/tinygogc + run: llgo test -tags testGC . - name: run llgo test run: | llgo test ./... + hello: continue-on-error: true timeout-minutes: 30 diff --git a/runtime/internal/runtime/tinygogc/gc_link.go b/runtime/internal/runtime/tinygogc/gc_link.go index 818ce249..13ed3a74 100644 --- a/runtime/internal/runtime/tinygogc/gc_link.go +++ b/runtime/internal/runtime/tinygogc/gc_link.go @@ -10,14 +10,6 @@ import ( //go:linkname getsp llgo.stackSave func getsp() unsafe.Pointer -// link here for testing -// -//go:linkname memset C.memset -func memset(unsafe.Pointer, int, uintptr) unsafe.Pointer - -//go:linkname memcpy C.memcpy -func memcpy(unsafe.Pointer, unsafe.Pointer, uintptr) - //go:linkname _heapStart _heapStart var _heapStart [0]byte diff --git a/runtime/internal/runtime/tinygogc/gc_test.go b/runtime/internal/runtime/tinygogc/gc_test.go index 0234b5fa..07fe7571 100644 --- a/runtime/internal/runtime/tinygogc/gc_test.go +++ b/runtime/internal/runtime/tinygogc/gc_test.go @@ -3,7 +3,6 @@ package tinygogc import ( - "unsafe" _ "unsafe" ) @@ -22,14 +21,3 @@ var _stackStart [0]byte var _globals_start [0]byte var _globals_end [0]byte - -//go:linkname memclrNoHeapPointers runtime.memclrNoHeapPointers -func memclrNoHeapPointers(unsafe.Pointer, uintptr) unsafe.Pointer - -//go:linkname memcpy runtime.memmove -func memcpy(to unsafe.Pointer, from unsafe.Pointer, size uintptr) - -func memset(ptr unsafe.Pointer, n int, size uintptr) unsafe.Pointer { - memclrNoHeapPointers(ptr, size) - return ptr -} diff --git a/runtime/internal/runtime/tinygogc/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go index d36f1806..89fe320a 100644 --- a/runtime/internal/runtime/tinygogc/gc_tinygo.go +++ b/runtime/internal/runtime/tinygogc/gc_tinygo.go @@ -108,7 +108,7 @@ func initGC() { endBlock = (uintptr(metadataStart) - heapStart) / bytesPerBlock stackTop = uintptr(unsafe.Pointer(&_stackStart)) - memset(metadataStart, 0, metadataSize) + c.Memset(metadataStart, 0, metadataSize) } func lazyInit() { @@ -339,7 +339,7 @@ func Alloc(size uintptr) unsafe.Pointer { } unlock(&gcMutex) // Return a pointer to this allocation. - return memset(gcPointerOf(thisAlloc), 0, size) + return c.Memset(gcPointerOf(thisAlloc), 0, size) } } } @@ -363,7 +363,7 @@ func Realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { } newAlloc := Alloc(size) - memcpy(newAlloc, ptr, oldSize) + c.Memcpy(newAlloc, ptr, oldSize) free(ptr) return newAlloc diff --git a/runtime/internal/runtime/tinygogc/pc_mock_test.go b/runtime/internal/runtime/tinygogc/pc_mock_test.go index dde398ce..ba141c7d 100644 --- a/runtime/internal/runtime/tinygogc/pc_mock_test.go +++ b/runtime/internal/runtime/tinygogc/pc_mock_test.go @@ -5,6 +5,8 @@ package tinygogc import ( "testing" "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" ) const ( @@ -95,7 +97,7 @@ func (env *mockGCEnv) setupMockGC() { endBlock = (uintptr(metadataStart) - heapStart) / bytesPerBlock // Clear metadata using memset like initGC does - memset(metadataStart, 0, metadataSize) + c.Memset(metadataStart, 0, metadataSize) // Reset allocator state (initGC doesn't reset these, but we need to) nextAlloc = 0 From 36e84196c6e3cbea382fe3f006a95955095ccaff Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 14 Nov 2025 14:54:04 +0800 Subject: [PATCH 13/15] test: add test for gc stats --- runtime/internal/runtime/tinygogc/gc.go | 41 ++---- runtime/internal/runtime/tinygogc/gc_link.go | 25 ++++ runtime/internal/runtime/tinygogc/gc_test.go | 2 + .../internal/runtime/tinygogc/gc_tinygo.go | 1 - .../internal/runtime/tinygogc/pc_mock_test.go | 120 +++++++++++++++++- 5 files changed, 154 insertions(+), 35 deletions(-) diff --git a/runtime/internal/runtime/tinygogc/gc.go b/runtime/internal/runtime/tinygogc/gc.go index 965bc3bf..fd74479f 100644 --- a/runtime/internal/runtime/tinygogc/gc.go +++ b/runtime/internal/runtime/tinygogc/gc.go @@ -1,34 +1,9 @@ -//go:build baremetal && !testGC +//go:build baremetal || testGC package tinygogc import "unsafe" -// LLGoPackage instructs the LLGo linker to wrap C standard library memory allocation -// functions (malloc, realloc, calloc) so they use the tinygogc allocator instead. -// This ensures all memory allocations go through the GC, including C library calls. -const LLGoPackage = "link: --wrap=malloc --wrap=realloc --wrap=calloc" - -//export __wrap_malloc -func __wrap_malloc(size uintptr) unsafe.Pointer { - return Alloc(size) -} - -//export __wrap_calloc -func __wrap_calloc(nmemb, size uintptr) unsafe.Pointer { - totalSize := nmemb * size - // Check for multiplication overflow - if nmemb != 0 && totalSize/nmemb != size { - return nil // Overflow - } - return Alloc(totalSize) -} - -//export __wrap_realloc -func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { - return Realloc(ptr, size) -} - type GCStats struct { // General statistics. @@ -189,16 +164,18 @@ func ReadGCStats() GCStats { stackSys := stackTop - stackEnd stats := GCStats{ - StackInuse: uint64(stackTop - uintptr(getsp())), - StackSys: uint64(stackSys), - HeapSys: heapInuse + heapIdle, - GCSys: uint64(heapEnd - uintptr(metadataStart)), + Alloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock), TotalAlloc: gcTotalAlloc, + Sys: uint64(heapEnd - heapStart), Mallocs: gcMallocs, Frees: gcFrees, - Sys: uint64(heapEnd - heapStart), HeapAlloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock), - Alloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock), + HeapSys: heapInuse + heapIdle, + HeapIdle: heapIdle, + HeapInuse: heapInuse, + StackInuse: uint64(stackTop - uintptr(getsp())), + StackSys: uint64(stackSys), + GCSys: uint64(heapEnd - uintptr(metadataStart)), } unlock(&gcMutex) diff --git a/runtime/internal/runtime/tinygogc/gc_link.go b/runtime/internal/runtime/tinygogc/gc_link.go index 13ed3a74..9f5c04da 100644 --- a/runtime/internal/runtime/tinygogc/gc_link.go +++ b/runtime/internal/runtime/tinygogc/gc_link.go @@ -7,6 +7,31 @@ import ( _ "unsafe" ) +// LLGoPackage instructs the LLGo linker to wrap C standard library memory allocation +// functions (malloc, realloc, calloc) so they use the tinygogc allocator instead. +// This ensures all memory allocations go through the GC, including C library calls. +const LLGoPackage = "link: --wrap=malloc --wrap=realloc --wrap=calloc" + +//export __wrap_malloc +func __wrap_malloc(size uintptr) unsafe.Pointer { + return Alloc(size) +} + +//export __wrap_calloc +func __wrap_calloc(nmemb, size uintptr) unsafe.Pointer { + totalSize := nmemb * size + // Check for multiplication overflow + if nmemb != 0 && totalSize/nmemb != size { + return nil // Overflow + } + return Alloc(totalSize) +} + +//export __wrap_realloc +func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + return Realloc(ptr, size) +} + //go:linkname getsp llgo.stackSave func getsp() unsafe.Pointer diff --git a/runtime/internal/runtime/tinygogc/gc_test.go b/runtime/internal/runtime/tinygogc/gc_test.go index 07fe7571..3ff26113 100644 --- a/runtime/internal/runtime/tinygogc/gc_test.go +++ b/runtime/internal/runtime/tinygogc/gc_test.go @@ -18,6 +18,8 @@ var _heapEnd [0]byte var _stackStart [0]byte +var _stackEnd [0]byte + var _globals_start [0]byte var _globals_end [0]byte diff --git a/runtime/internal/runtime/tinygogc/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go index 89fe320a..e4923d48 100644 --- a/runtime/internal/runtime/tinygogc/gc_tinygo.go +++ b/runtime/internal/runtime/tinygogc/gc_tinygo.go @@ -57,7 +57,6 @@ const blockStateByteAllTails = 0 | uint8(blockStateTail<<(stateBits*1)) | uint8(blockStateTail<<(stateBits*0)) -// since we don't have an init() function, these should be initalized by initHeap(), which is called by
entry var ( heapStart uintptr // start address of heap area heapEnd uintptr // end address of heap area diff --git a/runtime/internal/runtime/tinygogc/pc_mock_test.go b/runtime/internal/runtime/tinygogc/pc_mock_test.go index ba141c7d..c09bb9ff 100644 --- a/runtime/internal/runtime/tinygogc/pc_mock_test.go +++ b/runtime/internal/runtime/tinygogc/pc_mock_test.go @@ -99,9 +99,17 @@ func (env *mockGCEnv) setupMockGC() { // Clear metadata using memset like initGC does c.Memset(metadataStart, 0, metadataSize) - // Reset allocator state (initGC doesn't reset these, but we need to) + // Reset allocator state and all GC statistics for clean test environment nextAlloc = 0 isGCInit = true + + // Reset all GC statistics to start from clean state + gcTotalAlloc = 0 + gcTotalBlocks = 0 + gcMallocs = 0 + gcFrees = 0 + gcFreedBlocks = 0 + markStackOverflow = false } // restoreOriginalGC restores the original GC state @@ -114,7 +122,7 @@ func (env *mockGCEnv) restoreOriginalGC() { endBlock = env.originalEndBlock metadataStart = env.originalMetadataStart nextAlloc = env.originalNextAlloc - isGCInit = env.originalIsGCInit + isGCInit = false } // enableMockMode enables mock root scanning mode @@ -404,6 +412,114 @@ func TestMockGCMemoryPressure(t *testing.T) { t.Log("Successfully allocated more objects after GC") } +func TestMockGCStats(t *testing.T) { + env := createMockGCEnv() + env.setupMockGC() + defer env.restoreOriginalGC() + + // Get initial stats + initialStats := ReadGCStats() + t.Logf("Initial stats - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d", + initialStats.Mallocs, initialStats.Frees, initialStats.TotalAlloc, initialStats.Alloc) + + // Verify basic system stats + expectedSys := uint64(env.heapEnd - env.heapStart - 2048) + if initialStats.Sys != expectedSys { + t.Errorf("Expected Sys %d, got %d", expectedSys, initialStats.Sys) + } + + expectedGCSys := uint64(env.heapEnd - uintptr(metadataStart)) + if initialStats.GCSys != expectedGCSys { + t.Errorf("Expected GCSys %d, got %d", expectedGCSys, initialStats.GCSys) + } + + // Allocate some objects + var allocations []unsafe.Pointer + allocSize := uintptr(64) + numAllocs := 10 + + for i := 0; i < numAllocs; i++ { + ptr := Alloc(allocSize) + if ptr == nil { + t.Fatalf("Failed to allocate at iteration %d", i) + } + allocations = append(allocations, ptr) + } + + // Check stats after allocation + afterAllocStats := ReadGCStats() + t.Logf("After allocation - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d", + afterAllocStats.Mallocs, afterAllocStats.Frees, afterAllocStats.TotalAlloc, afterAllocStats.Alloc) + + // Verify allocation stats increased + if afterAllocStats.Mallocs <= initialStats.Mallocs { + t.Errorf("Expected Mallocs to increase from %d, got %d", initialStats.Mallocs, afterAllocStats.Mallocs) + } + + if afterAllocStats.TotalAlloc <= initialStats.TotalAlloc { + t.Errorf("Expected TotalAlloc to increase from %d, got %d", initialStats.TotalAlloc, afterAllocStats.TotalAlloc) + } + + if afterAllocStats.Alloc <= initialStats.Alloc { + t.Errorf("Expected Alloc to increase from %d, got %d", initialStats.Alloc, afterAllocStats.Alloc) + } + + // Verify Alloc and HeapAlloc are the same + if afterAllocStats.Alloc != afterAllocStats.HeapAlloc { + t.Errorf("Expected Alloc (%d) to equal HeapAlloc (%d)", afterAllocStats.Alloc, afterAllocStats.HeapAlloc) + } + + // Perform GC with controlled roots - keep only half the allocations + env.enableMockMode() + keepCount := len(allocations) / 2 + for i := 0; i < keepCount; i++ { + env.addRoot(allocations[i]) + } + + freedBytes := env.runMockGC() + t.Logf("GC freed %d bytes", freedBytes) + + // Check stats after GC + afterGCStats := ReadGCStats() + t.Logf("After GC - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d", + afterGCStats.Mallocs, afterGCStats.Frees, afterGCStats.TotalAlloc, afterGCStats.Alloc) + + // Verify GC stats + if afterGCStats.Frees <= afterAllocStats.Frees { + t.Errorf("Expected Frees to increase from %d, got %d", afterAllocStats.Frees, afterGCStats.Frees) + } + + // TotalAlloc should not decrease (cumulative) + if afterGCStats.TotalAlloc != afterAllocStats.TotalAlloc { + t.Errorf("Expected TotalAlloc to remain %d after GC, got %d", afterAllocStats.TotalAlloc, afterGCStats.TotalAlloc) + } + + // Alloc should decrease (freed objects) + if afterGCStats.Alloc >= afterAllocStats.Alloc { + t.Errorf("Expected Alloc to decrease from %d after GC, got %d", afterAllocStats.Alloc, afterGCStats.Alloc) + } + + // Verify heap statistics consistency + if afterGCStats.HeapSys != afterGCStats.HeapInuse+afterGCStats.HeapIdle { + t.Errorf("Expected HeapSys (%d) to equal HeapInuse (%d) + HeapIdle (%d)", + afterGCStats.HeapSys, afterGCStats.HeapInuse, afterGCStats.HeapIdle) + } + + // Verify live objects calculation + expectedLiveObjects := afterGCStats.Mallocs - afterGCStats.Frees + t.Logf("Live objects: %d (Mallocs: %d - Frees: %d)", expectedLiveObjects, afterGCStats.Mallocs, afterGCStats.Frees) + + // The number of live objects should be reasonable (we kept half the allocations plus some overhead) + if expectedLiveObjects < uint64(keepCount) { + t.Errorf("Expected at least %d live objects, got %d", keepCount, expectedLiveObjects) + } + + // Test stack statistics + if afterGCStats.StackInuse > afterGCStats.StackSys { + t.Errorf("StackInuse (%d) should not exceed StackSys (%d)", afterGCStats.StackInuse, afterGCStats.StackSys) + } +} + func TestMockGCCircularReferences(t *testing.T) { env := createMockGCEnv() env.setupMockGC() From 552156ff40bb8cd464bf48b147d1b464f92ca51f Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 14 Nov 2025 15:28:52 +0800 Subject: [PATCH 14/15] fix: adjust gc stats struct --- .../internal/lib/runtime/runtime_gc_baremetal.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/runtime/internal/lib/runtime/runtime_gc_baremetal.go b/runtime/internal/lib/runtime/runtime_gc_baremetal.go index 43ab5573..f384b6d0 100644 --- a/runtime/internal/lib/runtime/runtime_gc_baremetal.go +++ b/runtime/internal/lib/runtime/runtime_gc_baremetal.go @@ -10,16 +10,18 @@ import ( func ReadMemStats(m *runtime.MemStats) { stats := tinygogc.ReadGCStats() - m.StackInuse = stats.StackInuse - m.StackSys = stats.StackSys - m.HeapSys = stats.HeapSys - m.GCSys = stats.GCSys + m.Alloc = stats.Alloc m.TotalAlloc = stats.TotalAlloc + m.Sys = stats.Sys m.Mallocs = stats.Mallocs m.Frees = stats.Frees - m.Sys = stats.Sys m.HeapAlloc = stats.HeapAlloc - m.Alloc = stats.Alloc + m.HeapSys = stats.HeapSys + m.HeapIdle = stats.HeapIdle + m.HeapInuse = stats.HeapInuse + m.StackInuse = stats.StackInuse + m.StackSys = stats.StackSys + m.GCSys = stats.GCSys } func GC() { From 065126e270d29c00622d28a29fea67e01dd57055 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 14 Nov 2025 16:00:40 +0800 Subject: [PATCH 15/15] feat: make defer tls stub for baremetal --- runtime/internal/clite/tls/tls_common.go | 2 +- runtime/internal/clite/tls/tls_gc.go | 2 +- runtime/internal/clite/tls/tls_nogc.go | 2 +- runtime/internal/clite/tls/tls_stub.go | 2 +- runtime/internal/runtime/z_defer_gc.go | 2 +- runtime/internal/runtime/z_defer_nogc.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/runtime/internal/clite/tls/tls_common.go b/runtime/internal/clite/tls/tls_common.go index 4741365e..1a6707cd 100644 --- a/runtime/internal/clite/tls/tls_common.go +++ b/runtime/internal/clite/tls/tls_common.go @@ -1,4 +1,4 @@ -//go:build llgo +//go:build llgo && !baremetal /* * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/clite/tls/tls_gc.go b/runtime/internal/clite/tls/tls_gc.go index d6341827..ce672440 100644 --- a/runtime/internal/clite/tls/tls_gc.go +++ b/runtime/internal/clite/tls/tls_gc.go @@ -1,4 +1,4 @@ -//go:build llgo && !nogc +//go:build llgo && !baremetal && !nogc /* * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/clite/tls/tls_nogc.go b/runtime/internal/clite/tls/tls_nogc.go index 15f7cd1e..81c172f0 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 +//go:build llgo && (nogc || baremetal) /* * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/clite/tls/tls_stub.go b/runtime/internal/clite/tls/tls_stub.go index f209bb5a..657d2bb7 100644 --- a/runtime/internal/clite/tls/tls_stub.go +++ b/runtime/internal/clite/tls/tls_stub.go @@ -1,4 +1,4 @@ -//go:build !llgo +//go:build !llgo || baremetal /* * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/runtime/z_defer_gc.go b/runtime/internal/runtime/z_defer_gc.go index fdfd008b..3d8fe8ec 100644 --- a/runtime/internal/runtime/z_defer_gc.go +++ b/runtime/internal/runtime/z_defer_gc.go @@ -1,4 +1,4 @@ -//go:build !nogc +//go:build !nogc && !baremetal /* * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. diff --git a/runtime/internal/runtime/z_defer_nogc.go b/runtime/internal/runtime/z_defer_nogc.go index 336462c8..3a7f2bce 100644 --- a/runtime/internal/runtime/z_defer_nogc.go +++ b/runtime/internal/runtime/z_defer_nogc.go @@ -1,4 +1,4 @@ -//go:build nogc +//go:build nogc || baremetal /* * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.