mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-09-11 15:09:32 +00:00
runtime: preserve malloc allocations until free
C malloc storage has explicit lifetime: it must remain allocated until free even when no GC-visible pointer references it. Treating it as an ordinary NoPtrs allocation breaks bare-metal C object graphs, while conservatively scanning arbitrary C bytes creates false Go roots. Add allocManual/freeManual so collectors can represent pointer-free, explicitly managed storage. Block GC keeps these objects permanently marked and releases their blocks on free; Boehm uses atomic uncollectable allocations; leaking and custom collectors provide equivalent behavior. Wasm keeps its allocation map only for validation and sizes, and WASIp2 realloc now copies min(oldSize, newSize). Bump the Boehm library cache version because enabling atomic uncollectable allocations changes its compiled flags and exported API. Also handle zero-size and overflowing allocations, serialize allocation registries, reject Go finalizers on manual storage, and add CGo regressions for C pointer graphs, hidden until-free allocations, repeated free/reuse, and allocation edge cases.
This commit is contained in:
+74
-36
@@ -205,8 +205,9 @@ func (b gcBlock) free() {
|
||||
|
||||
// objHeader is a structure appended to every heap object to hold metadata.
|
||||
type objHeader struct {
|
||||
// next is the next object to scan after this.
|
||||
next *objHeader
|
||||
// next links the GC scan list. Manual allocations remain permanently marked
|
||||
// and use the otherwise invalid value 1 as an until-free marker.
|
||||
next uintptr
|
||||
|
||||
// layout holds the layout bitmap used to find pointers in the object.
|
||||
layout gcLayout
|
||||
@@ -482,6 +483,7 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
|
||||
// Create the object header.
|
||||
size -= unsafe.Sizeof(objHeader{})
|
||||
header := (*objHeader)(unsafe.Add(pointer, size))
|
||||
header.next = 0
|
||||
header.layout = parseGCLayout(layout)
|
||||
|
||||
// We've claimed this allocation, now we can unlock the heap.
|
||||
@@ -500,42 +502,61 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
|
||||
return pointer
|
||||
}
|
||||
|
||||
func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
|
||||
if ptr == nil {
|
||||
return alloc(size, gclayout.NoPtrs.AsPtr())
|
||||
// allocManual allocates pointer-free memory that remains live until freeManual.
|
||||
func allocManual(size uintptr) unsafe.Pointer {
|
||||
if size == 0 {
|
||||
return alloc_zero(size, gclayout.NoPtrs.AsPtr())
|
||||
}
|
||||
ptr := alloc(size, gclayout.NoPtrs.AsPtr())
|
||||
|
||||
// Find the first block of the original allocation.
|
||||
firstBlock := blockFromAddr(uintptr(ptr))
|
||||
|
||||
// Find the last block of the original allocation.
|
||||
lastBlock := firstBlock.findHead()
|
||||
|
||||
// Calculate the size of the original allocation body.
|
||||
oldSize := uintptr(lastBlock-firstBlock)*bytesPerBlock + (bytesPerBlock - unsafe.Sizeof(objHeader{}))
|
||||
|
||||
if size <= oldSize {
|
||||
// The requested size is less than the old size.
|
||||
// There are likely scenarios for this:
|
||||
// - The caller intended to grow the allocation, but the original size
|
||||
// was rounded up by alloc to a multiple of the block size.
|
||||
// The rounded size is already sufficient.
|
||||
// - The caller intended to shrink the allocation.
|
||||
// We currently ignore this case.
|
||||
// Either way, the current allocation can be left alone.
|
||||
return ptr
|
||||
}
|
||||
|
||||
// Create a new allocation and copy the old data.
|
||||
newAlloc := alloc(size, gclayout.NoPtrs.AsPtr())
|
||||
memcpy(newAlloc, ptr, oldSize)
|
||||
free(ptr)
|
||||
|
||||
return newAlloc
|
||||
gcLock.Lock()
|
||||
head := blockFromAddr(uintptr(ptr)).findHead()
|
||||
head.setState(blockStateMark)
|
||||
header := (*objHeader)(unsafe.Add(head.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
|
||||
header.next = 1
|
||||
gcLock.Unlock()
|
||||
return ptr
|
||||
}
|
||||
|
||||
func free(ptr unsafe.Pointer) {
|
||||
// TODO: free blocks on request, when the compiler knows they're unused.
|
||||
if ptr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
gcLock.Lock()
|
||||
addr := uintptr(ptr)
|
||||
if !isOnHeap(addr) || (addr-heapStart)%bytesPerBlock != 0 {
|
||||
gcLock.Unlock()
|
||||
runtimeFatal("free: invalid pointer")
|
||||
}
|
||||
|
||||
firstBlock := blockFromAddr(addr)
|
||||
state := firstBlock.state()
|
||||
if state != blockStateTail && state != blockStateHead && state != blockStateMark {
|
||||
gcLock.Unlock()
|
||||
runtimeFatal("free: invalid pointer")
|
||||
}
|
||||
|
||||
allocationStart := firstBlock
|
||||
for allocationStart != 0 && (allocationStart-1).state() == blockStateTail {
|
||||
allocationStart--
|
||||
}
|
||||
if allocationStart != firstBlock {
|
||||
gcLock.Unlock()
|
||||
runtimeFatal("free: invalid pointer")
|
||||
}
|
||||
|
||||
lastBlock := firstBlock.findHead()
|
||||
header := (*objHeader)(unsafe.Add(lastBlock.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
|
||||
if header.next != 1 {
|
||||
gcLock.Unlock()
|
||||
runtimeFatal("free: invalid pointer")
|
||||
}
|
||||
for block := firstBlock; block <= lastBlock; block++ {
|
||||
block.free()
|
||||
}
|
||||
insertFreeRange(firstBlock.pointer(), uintptr(lastBlock-firstBlock+1))
|
||||
gcLock.Unlock()
|
||||
}
|
||||
|
||||
// GC performs a garbage collection cycle.
|
||||
@@ -666,7 +687,7 @@ func finishMark() {
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
scanList = obj.next
|
||||
scanList = (*objHeader)(unsafe.Pointer(obj.next))
|
||||
|
||||
// Check if the object may contain pointers.
|
||||
if obj.layout.pointerFree() {
|
||||
@@ -724,7 +745,7 @@ func markRoot(addr, root uintptr) {
|
||||
|
||||
// Add the object to the scan list.
|
||||
header := (*objHeader)(unsafe.Add(head.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
|
||||
header.next = scanList
|
||||
header.next = uintptr(unsafe.Pointer(scanList))
|
||||
scanList = header
|
||||
}
|
||||
|
||||
@@ -758,7 +779,10 @@ func sweep() uintptr {
|
||||
|
||||
// Unmark the next head.
|
||||
block--
|
||||
block.unmark()
|
||||
header := (*objHeader)(unsafe.Add(block.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
|
||||
if header.next != 1 {
|
||||
block.unmark()
|
||||
}
|
||||
|
||||
// Skip the tail.
|
||||
for block > 0 && (block-1).state() == blockStateTail {
|
||||
@@ -903,5 +927,19 @@ func SetFinalizer(obj interface{}, finalizer interface{}) {
|
||||
// A nil pointer has nothing to finalize.
|
||||
return
|
||||
}
|
||||
|
||||
gcLock.Lock()
|
||||
addr := uintptr(objPtr)
|
||||
manual := false
|
||||
if isOnHeap(addr) {
|
||||
head := blockFromAddr(addr).findHead()
|
||||
header := (*objHeader)(unsafe.Add(head.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
|
||||
manual = header.next == 1
|
||||
}
|
||||
gcLock.Unlock()
|
||||
if manual && finalizer != nil {
|
||||
runtimeFatal("runtime.SetFinalizer: manual allocation")
|
||||
}
|
||||
|
||||
registerFinalizer(uintptr(objPtr), finalizer)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user