runtime: require explicit GC layouts

This commit is contained in:
Jake Bailey
2026-08-07 10:21:19 -07:00
committed by Ron Evans
parent 1a4cb2032e
commit a7360d5ad3
28 changed files with 279 additions and 108 deletions
+6 -3
View File
@@ -2,7 +2,10 @@
package runtime
import "unsafe"
import (
"internal/gclayout"
"unsafe"
)
// The below functions override the default allocator of wasi-libc. This ensures
// code linked from other languages can allocate memory without colliding with
@@ -21,7 +24,7 @@ func libc_malloc(size uintptr) unsafe.Pointer {
if size == 0 {
return nil
}
ptr := alloc(size, nil)
ptr := alloc(size, gclayout.NoPtrs.AsPtr())
allocs[(*byte)(ptr)] = size
return ptr
}
@@ -54,7 +57,7 @@ func libc_realloc(oldPtr unsafe.Pointer, size uintptr) unsafe.Pointer {
// It's hard to optimize this to expand the current buffer with our GC, but
// it is theoretically possible. For now, just always allocate fresh.
// TODO: we could skip this if the new allocation is smaller than the old.
ptr := alloc(size, nil)
ptr := alloc(size, gclayout.NoPtrs.AsPtr())
if oldPtr != nil {
if oldSize, ok := allocs[(*byte)(oldPtr)]; ok {
+2 -1
View File
@@ -3,6 +3,7 @@
package runtime
import (
"internal/gclayout"
"sync/atomic"
"unsafe"
)
@@ -11,7 +12,7 @@ import (
func libc_malloc(size uintptr) unsafe.Pointer {
// Note: this zeroes the returned buffer which is not necessary.
// The same goes for bytealg.MakeNoZero.
return alloc(size, nil)
return alloc(size, gclayout.NoPtrs.AsPtr())
}
//export calloc
+2 -2
View File
@@ -137,11 +137,11 @@ type chanSelectState struct {
value unsafe.Pointer
}
func chanMake(elementSize uintptr, bufSize uintptr) *channel {
func chanMake(elementSize uintptr, bufSize uintptr, elementLayout unsafe.Pointer) *channel {
return &channel{
elementSize: elementSize,
bufCap: bufSize,
buf: alloc(elementSize*bufSize, nil),
buf: alloc(elementSize*bufSize, elementLayout),
}
}
+3 -2
View File
@@ -31,6 +31,7 @@ package runtime
// Moss.
import (
"internal/gclayout"
"internal/reflectlite"
"internal/task"
"runtime/interrupt"
@@ -501,7 +502,7 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
if ptr == nil {
return alloc(size, nil)
return alloc(size, gclayout.NoPtrs.AsPtr())
}
// Find the first block of the original allocation.
@@ -526,7 +527,7 @@ func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
}
// Create a new allocation and copy the old data.
newAlloc := alloc(size, nil)
newAlloc := alloc(size, gclayout.NoPtrs.AsPtr())
memcpy(newAlloc, ptr, oldSize)
free(ptr)
+2 -1
View File
@@ -7,6 +7,7 @@ package runtime
// may be the only memory allocator possible.
import (
"internal/gclayout"
"internal/task"
"sync/atomic"
"unsafe"
@@ -69,7 +70,7 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
}
func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
newAlloc := alloc(size, nil)
newAlloc := alloc(size, gclayout.NoPtrs.AsPtr())
if ptr == nil {
return newAlloc
}
+5 -4
View File
@@ -55,7 +55,10 @@
package runtime
import "unsafe"
import (
"internal/gclayout"
"unsafe"
)
const sizeFieldBits = 4 + (unsafe.Sizeof(uintptr(0)) / 4)
@@ -76,9 +79,7 @@ func (layout gcLayout) pointerFree() bool {
// The length is rounded down to a multiple of the element size.
func (layout gcLayout) scan(start, len uintptr) {
switch {
case layout == 0:
// This is an unknown layout.
// Scan conservatively.
case layout == gcLayout(gclayout.Conservative):
// NOTE: This is *NOT* equivalent to a slice of pointers on AVR.
scanConservative(start, len)
+27 -12
View File
@@ -14,6 +14,7 @@ import (
// The underlying hashmap structure for Go.
type hashmap struct {
buckets unsafe.Pointer // pointer to array of buckets
typeInfo *hashmapTypeInfo
seed uintptr
count uintptr
keySize uintptr
@@ -26,6 +27,17 @@ type hashmap struct {
keyHash func(key unsafe.Pointer, size, seed uintptr) uint32
}
type hashmapTypeInfo struct {
keyLayout unsafe.Pointer
valueLayout unsafe.Pointer
bucketLayout unsafe.Pointer
}
//go:inline
func hashmapType(m *hashmap) *hashmapTypeInfo {
return m.typeInfo
}
const (
hashmapMaxKeySize = 128
hashmapMaxValueSize = 128
@@ -113,7 +125,7 @@ func hashmapTopHash(hash uint32) uint8 {
}
// Create a new hashmap with the given keySize and valueSize.
func hashmapMake(keySize, valueSize uintptr, sizeHint uintptr, alg uint8) *hashmap {
func hashmapMake(keySize, valueSize uintptr, sizeHint uintptr, typeInfo unsafe.Pointer, alg uint8) *hashmap {
bucketBits := uint8(0)
for hashmapHasSpaceToGrow(bucketBits) && hashmapOverLoadFactor(sizeHint, bucketBits) {
bucketBits++
@@ -132,13 +144,14 @@ func hashmapMake(keySize, valueSize uintptr, sizeHint uintptr, alg uint8) *hashm
}
bucketBufSize := hashmapBucketHeaderSize + keySlotSize*8 + valueSlotSize*8
buckets := alloc(bucketBufSize*(1<<bucketBits), nil)
buckets := alloc(bucketBufSize*(1<<bucketBits), (*hashmapTypeInfo)(typeInfo).bucketLayout)
keyHash := hashmapKeyHashAlg(tinygo.HashmapAlgorithm(alg))
keyEqual := hashmapKeyEqualAlg(tinygo.HashmapAlgorithm(alg))
return &hashmap{
buckets: buckets,
typeInfo: (*hashmapTypeInfo)(typeInfo),
seed: uintptr(fastrand()),
keySize: keySize,
valueSize: valueSize,
@@ -325,7 +338,7 @@ func hashmapSet(m *hashmap, key unsafe.Pointer, value unsafe.Pointer, hash uint3
//go:inline
func hashmapStoreKey(m *hashmap, slotKey, key unsafe.Pointer) {
if m.flags&hashmapFlagIndirectKey != 0 {
p := alloc(m.keySize, nil)
p := alloc(m.keySize, hashmapType(m).keyLayout)
memcpy(p, key, m.keySize)
*(*unsafe.Pointer)(slotKey) = p
} else {
@@ -343,7 +356,7 @@ func hashmapStoreValue(m *hashmap, slotValue, value unsafe.Pointer) {
p := *(*unsafe.Pointer)(slotValue)
if p == nil {
// First insert: allocate backing storage.
p = alloc(m.valueSize, nil)
p = alloc(m.valueSize, hashmapType(m).valueLayout)
*(*unsafe.Pointer)(slotValue) = p
}
memcpy(p, value, m.valueSize)
@@ -356,7 +369,7 @@ func hashmapStoreValue(m *hashmap, slotValue, value unsafe.Pointer) {
// value into the bucket, and returns a pointer to this bucket.
func hashmapInsertIntoNewBucket(m *hashmap, key, value unsafe.Pointer, tophash uint8) *hashmapBucket {
bucketBufSize := hashmapBucketSize(m)
bucketBuf := alloc(bucketBufSize, nil)
bucketBuf := alloc(bucketBufSize, hashmapType(m).bucketLayout)
bucket := (*hashmapBucket)(bucketBuf)
// Insert into the first slot, which is empty as it has just been allocated.
@@ -392,13 +405,13 @@ func hashmapCopy(m *hashmap, sizeBits uint8) hashmap {
n.bucketBits = sizeBits
numBuckets := uintptr(1) << n.bucketBits
bucketBufSize := hashmapBucketSize(m)
n.buckets = alloc(bucketBufSize*numBuckets, nil)
n.buckets = alloc(bucketBufSize*numBuckets, hashmapType(m).bucketLayout)
// use a hashmap iterator to go through the old map
var it hashmapIterator
var key = alloc(m.keySize, nil)
var value = alloc(m.valueSize, nil)
var key = alloc(m.keySize, hashmapType(m).keyLayout)
var value = alloc(m.valueSize, hashmapType(m).valueLayout)
for hashmapNext(m, &it, key, value) {
h := n.keyHash(key, uintptr(n.keySize), n.seed)
@@ -624,6 +637,7 @@ func hashmapGenericDelete(m *hashmap, key unsafe.Pointer) {
// equal functions. This avoids the interface/reflection path for composite
// key types like structs containing strings.
func hashmapMakeGeneric(keySize, valueSize uintptr, sizeHint uintptr,
typeInfo unsafe.Pointer,
keyHash func(key unsafe.Pointer, size, seed uintptr) uint32,
keyEqual func(x, y unsafe.Pointer, n uintptr) bool) *hashmap {
bucketBits := uint8(0)
@@ -644,10 +658,11 @@ func hashmapMakeGeneric(keySize, valueSize uintptr, sizeHint uintptr,
}
bucketBufSize := hashmapBucketHeaderSize + keySlotSize*8 + valueSlotSize*8
buckets := alloc(bucketBufSize*(1<<bucketBits), nil)
buckets := alloc(bucketBufSize*(1<<bucketBits), (*hashmapTypeInfo)(typeInfo).bucketLayout)
return &hashmap{
buckets: buckets,
typeInfo: (*hashmapTypeInfo)(typeInfo),
seed: uintptr(fastrand()),
keySize: keySize,
valueSize: valueSize,
@@ -663,12 +678,12 @@ func hashmapMakeGeneric(keySize, valueSize uintptr, sizeHint uintptr,
// hashmapMakeReflect creates a hashmap for reflect.MakeMapWithSize using
// closures that reconstruct interface{} values from raw key bytes,
// delegating to hashmapInterfaceHash for hashing and == for equality.
func hashmapMakeReflect(keySize, valueSize, sizeHint uintptr, keyType unsafe.Pointer) *hashmap {
func hashmapMakeReflect(keySize, valueSize, sizeHint uintptr, typeInfo, keyType unsafe.Pointer) *hashmap {
t := (*reflectlite.RawType)(keyType)
if t.Kind() == reflectlite.Interface {
// Interface keys are already stored as interface values in the
// bucket; use the existing interface hash/equal directly.
return hashmapMakeGeneric(keySize, valueSize, sizeHint,
return hashmapMakeGeneric(keySize, valueSize, sizeHint, typeInfo,
hashmapInterfacePtrHash, hashmapInterfaceEqual)
}
keyHash := func(key unsafe.Pointer, size, seed uintptr) uint32 {
@@ -677,7 +692,7 @@ func hashmapMakeReflect(keySize, valueSize, sizeHint uintptr, keyType unsafe.Poi
keyEqual := func(x, y unsafe.Pointer, n uintptr) bool {
return rawToInterface(t, x) == rawToInterface(t, y)
}
return hashmapMakeGeneric(keySize, valueSize, sizeHint, keyHash, keyEqual)
return hashmapMakeGeneric(keySize, valueSize, sizeHint, typeInfo, keyHash, keyEqual)
}
// rawToInterface reconstructs an interface{} from raw bytes at ptr.