From 7202103c39a1ecad17d3d82dac43d8b7a173f1ee Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Oct 2025 13:34:04 +0800 Subject: [PATCH 1/5] tls: add gc-safe pthread helper --- c/tls/tls.go | 121 ++++++++++++++++++++++++++++++++++++++++++++ c/tls/tls_nogc.go | 87 +++++++++++++++++++++++++++++++ c/tls/tls_test.go | 89 ++++++++++++++++++++++++++++++++ docs/gc-safe-tls.md | 40 +++++++++++++++ 4 files changed, 337 insertions(+) create mode 100644 c/tls/tls.go create mode 100644 c/tls/tls_nogc.go create mode 100644 c/tls/tls_test.go create mode 100644 docs/gc-safe-tls.md diff --git a/c/tls/tls.go b/c/tls/tls.go new file mode 100644 index 0000000..c410cc9 --- /dev/null +++ b/c/tls/tls.go @@ -0,0 +1,121 @@ +//go:build !nogc + +package tls + +import ( + "unsafe" + + c "github.com/goplus/lib/c" + "github.com/goplus/lib/c/bdwgc" + "github.com/goplus/lib/c/pthread" +) + +const slotRegistered = 1 << iota + +type descriptor[T any] struct { + destructor func(*T) +} + +type slot[T any] struct { + value T + state uintptr + desc *descriptor[T] +} + +type Handle[T any] struct { + key pthread.Key + desc *descriptor[T] +} + +// Alloc creates a TLS handle whose storage is visible to the Boehm GC. +func Alloc[T any](destructor func(*T)) Handle[T] { + d := &descriptor[T]{destructor: destructor} + var key pthread.Key + key.Create(slotDestructor[T]) + return Handle[T]{key: key, desc: d} +} + +// Get returns the value stored in the current thread's slot. +func (h Handle[T]) Get() T { + if ptr := h.key.Get(); ptr != nil { + return (*slot[T])(ptr).value + } + var zero T + return zero +} + +// Set stores v in the current thread's slot, creating it if necessary. +func (h Handle[T]) Set(v T) { + s := h.ensureSlot() + s.value = v +} + +// Clear zeroes the current thread's slot value without freeing the slot. +func (h Handle[T]) Clear() { + if ptr := h.key.Get(); ptr != nil { + s := (*slot[T])(ptr) + var zero T + s.value = zero + } +} + +func (h Handle[T]) ensureSlot() *slot[T] { + if ptr := h.key.Get(); ptr != nil { + return (*slot[T])(ptr) + } + size := unsafe.Sizeof(slot[T]{}) + mem := c.Malloc(size) + if mem == nil { + panic("tls: failed to allocate thread slot") + } + c.Memset(mem, 0, size) + s := (*slot[T])(mem) + s.desc = h.desc + registerSlot(s) + if existing := h.key.Get(); existing != nil { + deregisterSlot(s) + c.Free(mem) + return (*slot[T])(existing) + } + h.key.Set(mem) + return s +} + +func slotDestructor[T any](ptr c.Pointer) { + s := (*slot[T])(ptr) + if s == nil { + return + } + deregisterSlot(s) + if s.desc != nil && s.desc.destructor != nil { + s.desc.destructor(&s.value) + } + var zero T + s.value = zero + s.desc = nil + c.Free(ptr) +} + +func registerSlot[T any](s *slot[T]) { + if s.state&slotRegistered != 0 { + return + } + start, end := s.rootRange() + bdwgc.AddRoots(start, end) + s.state |= slotRegistered +} + +func deregisterSlot[T any](s *slot[T]) { + if s == nil || s.state&slotRegistered == 0 { + return + } + start, end := s.rootRange() + bdwgc.RemoveRoots(start, end) + s.state &^= slotRegistered +} + +func (s *slot[T]) rootRange() (start, end c.Pointer) { + head := unsafe.Pointer(&s.value) + endPtr := unsafe.Pointer(uintptr(head) + unsafe.Sizeof(s.value)) + return c.Pointer(head), c.Pointer(endPtr) +} diff --git a/c/tls/tls_nogc.go b/c/tls/tls_nogc.go new file mode 100644 index 0000000..7b5e124 --- /dev/null +++ b/c/tls/tls_nogc.go @@ -0,0 +1,87 @@ +//go:build nogc +// +build nogc + +package tls + +import ( + "unsafe" + + c "github.com/goplus/lib/c" + "github.com/goplus/lib/c/pthread" +) + +type descriptor[T any] struct { + destructor func(*T) +} + +type slot[T any] struct { + value T + desc *descriptor[T] +} + +type Handle[T any] struct { + key pthread.Key + desc *descriptor[T] +} + +func Alloc[T any](destructor func(*T)) Handle[T] { + d := &descriptor[T]{destructor: destructor} + var key pthread.Key + key.Create(slotDestructor[T]) + return Handle[T]{key: key, desc: d} +} + +func (h Handle[T]) Get() T { + if ptr := h.key.Get(); ptr != nil { + return (*slot[T])(ptr).value + } + var zero T + return zero +} + +func (h Handle[T]) Set(v T) { + s := h.ensureSlot() + s.value = v +} + +func (h Handle[T]) Clear() { + if ptr := h.key.Get(); ptr != nil { + s := (*slot[T])(ptr) + var zero T + s.value = zero + } +} + +func (h Handle[T]) ensureSlot() *slot[T] { + if ptr := h.key.Get(); ptr != nil { + return (*slot[T])(ptr) + } + size := unsafe.Sizeof(slot[T]{}) + mem := c.Malloc(size) + if mem == nil { + panic("tls: failed to allocate thread slot") + } + c.Memset(mem, 0, size) + s := (*slot[T])(mem) + s.desc = h.desc + if existing := h.key.Get(); existing != nil { + c.Free(mem) + return (*slot[T])(existing) + } + h.key.Set(mem) + return s +} + +func slotDestructor[T any](ptr c.Pointer) { + s := (*slot[T])(ptr) + if s == nil { + return + } + if s.desc != nil && s.desc.destructor != nil { + s.desc.destructor(&s.value) + } + var zero T + s.value = zero + s.desc = nil + c.Free(ptr) +} diff --git a/c/tls/tls_test.go b/c/tls/tls_test.go new file mode 100644 index 0000000..28803f6 --- /dev/null +++ b/c/tls/tls_test.go @@ -0,0 +1,89 @@ +//go:build llgo +// +build llgo + +package tls_test + +import ( + "sync" + "testing" + + "github.com/goplus/lib/c/tls" +) + +// slotValue is the type stored in the TLS tests. +type slotValue struct { + calls int +} + +func TestAllocSetGetClear(t *testing.T) { + var mu sync.Mutex + var destructCount int + handle := tls.Alloc[*slotValue](func(v **slotValue) { + mu.Lock() + defer mu.Unlock() + destructCount++ + if v != nil && *v != nil { + (*v).calls = -1 + } + }) + + if v := handle.Get(); v != nil { + t.Fatalf("unexpected initial value: %v", v) + } + + val := &slotValue{calls: 1} + handle.Set(val) + if got := handle.Get(); got != val { + t.Fatalf("Get() returned %p; want %p", got, val) + } + + handle.Clear() + if got := handle.Get(); got != nil { + t.Fatalf("expected nil after Clear, got %p", got) + } + + // Ensure destructor runs when the goroutine exits. + done := make(chan struct{}) + go func() { + defer close(done) + tmp := &slotValue{calls: 42} + handle.Set(tmp) + if got := handle.Get(); got != tmp { + t.Errorf("nested goroutine Get = %p; want %p", got, tmp) + } + }() + <-done + + mu.Lock() + deferCount := destructCount + mu.Unlock() + if deferCount == 0 { + t.Fatalf("expected destructor to run for goroutine TLS slot") + } +} + +func TestAllocIsThreadLocal(t *testing.T) { + handle := tls.Alloc[int](nil) + handle.Set(7) + if got := handle.Get(); got != 7 { + t.Fatalf("main goroutine value = %d; want 7", got) + } + + const want = 99 + done := make(chan struct{}) + go func() { + defer close(done) + if got := handle.Get(); got != 0 { + t.Errorf("new goroutine initial value = %d; want 0", got) + } + handle.Set(want) + if got := handle.Get(); got != want { + t.Errorf("new goroutine value = %d; want %d", got, want) + } + }() + <-done + + if got := handle.Get(); got != 7 { + t.Fatalf("main goroutine value changed to %d", got) + } +} diff --git a/docs/gc-safe-tls.md b/docs/gc-safe-tls.md new file mode 100644 index 0000000..dc60838 --- /dev/null +++ b/docs/gc-safe-tls.md @@ -0,0 +1,40 @@ +# GC-safe Thread Local Storage + +The `c/tls` package implements a small helper around pthread TLS that keeps the +slot visible to the Boehm garbage collector when it is enabled. It is useful for +runtime features (such as defer stacks) that keep GC-managed pointers inside the +pthread-specific area, which Boehm does not scan by default. + +## Key ideas + +- The package exposes a generic `tls.Handle[T]` type. `tls.Alloc` creates a + handle and registers the slot with Boehm (`GC_add_roots`) in GC builds, or + falls back to a plain `pthread_setspecific` slot in `-tags nogc` builds. +- A slot stores a value of type `T` directly, so even non-pointer data (for + example counters) can be stored without an additional allocation. +- An optional destructor can be supplied when allocating the handle. It is + invoked whenever the owning thread exits, allowing callers to release + resources or reset global state. + +## Example + +```go +var deferHeadTLS = tls.Alloc[*runtime.Defer](func(head **runtime.Defer) { + if head != nil { + *head = nil + } +}) + +func pushDefer(head *runtime.Defer) { + deferHeadTLS.Set(head) +} + +func popDefer() *runtime.Defer { + return deferHeadTLS.Get() +} +``` + +In GC builds the slot is automatically registered as a root, which prevents +Boehm from prematurely reclaiming the defer nodes. In builds that use +`-tags nogc`, the helper still uses pthread TLS but simply skips the root +registration. From b7573e9f4818395dd97c82a8aed1366f05fd310a Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Oct 2025 13:35:57 +0800 Subject: [PATCH 2/5] bdwgc: expose root registration --- c/bdwgc/bdwgc.go | 6 ++++++ c/tls/tls.go | 2 +- c/tls/tls_nogc.go | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/c/bdwgc/bdwgc.go b/c/bdwgc/bdwgc.go index 5d90607..ecbad7f 100644 --- a/c/bdwgc/bdwgc.go +++ b/c/bdwgc/bdwgc.go @@ -40,6 +40,12 @@ func Realloc(ptr c.Pointer, size uintptr) c.Pointer //go:linkname Free C.GC_free func Free(ptr c.Pointer) +//go:linkname AddRoots C.GC_add_roots +func AddRoots(start, end c.Pointer) + +//go:linkname RemoveRoots C.GC_remove_roots +func RemoveRoots(start, end c.Pointer) + // ----------------------------------------------------------------------------- //go:linkname RegisterFinalizer C.GC_register_finalizer diff --git a/c/tls/tls.go b/c/tls/tls.go index c410cc9..f239f14 100644 --- a/c/tls/tls.go +++ b/c/tls/tls.go @@ -5,7 +5,7 @@ package tls import ( "unsafe" - c "github.com/goplus/lib/c" + "github.com/goplus/lib/c" "github.com/goplus/lib/c/bdwgc" "github.com/goplus/lib/c/pthread" ) diff --git a/c/tls/tls_nogc.go b/c/tls/tls_nogc.go index 7b5e124..950e99d 100644 --- a/c/tls/tls_nogc.go +++ b/c/tls/tls_nogc.go @@ -6,7 +6,7 @@ package tls import ( "unsafe" - c "github.com/goplus/lib/c" + "github.com/goplus/lib/c" "github.com/goplus/lib/c/pthread" ) From cb406499ab3cc1dd9a871b021879829af918f00a Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Oct 2025 13:38:45 +0800 Subject: [PATCH 3/5] docs: update tls example --- docs/gc-safe-tls.md | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/gc-safe-tls.md b/docs/gc-safe-tls.md index dc60838..5e597ea 100644 --- a/docs/gc-safe-tls.md +++ b/docs/gc-safe-tls.md @@ -19,22 +19,32 @@ pthread-specific area, which Boehm does not scan by default. ## Example ```go -var deferHeadTLS = tls.Alloc[*runtime.Defer](func(head **runtime.Defer) { - if head != nil { - *head = nil +type stats struct { + handled int +} + +var threadStats = tls.Alloc[*stats](func(s **stats) { + // Destructor runs when the thread exits. + if s != nil && *s != nil { + log.Printf("thread handled %d requests", (*s).handled) } }) -func pushDefer(head *runtime.Defer) { - deferHeadTLS.Set(head) +func recordRequest() { + cur := threadStats.Get() + if cur == nil { + cur = &stats{} + } + cur.handled++ + threadStats.Set(cur) } -func popDefer() *runtime.Defer { - return deferHeadTLS.Get() +func resetStats() { + threadStats.Clear() } ``` In GC builds the slot is automatically registered as a root, which prevents -Boehm from prematurely reclaiming the defer nodes. In builds that use -`-tags nogc`, the helper still uses pthread TLS but simply skips the root -registration. +Boehm from reclaiming any GC-managed memory reachable from the `stats` +instances. In builds that use `-tags nogc`, the helper still relies on pthread +TLS but simply skips the root registration step. From 4034713c979fb43fe08001f9ec45b4824791b246 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Oct 2025 15:14:01 +0800 Subject: [PATCH 4/5] tls: fix concurrency and error handling --- c/tls/tls.go | 53 ++++++++++++++++++++++++----------------------- c/tls/tls_nogc.go | 36 ++++++++++++++++---------------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/c/tls/tls.go b/c/tls/tls.go index f239f14..88d3539 100644 --- a/c/tls/tls.go +++ b/c/tls/tls.go @@ -3,6 +3,7 @@ package tls import ( + "fmt" "unsafe" "github.com/goplus/lib/c" @@ -12,27 +13,24 @@ import ( const slotRegistered = 1 << iota -type descriptor[T any] struct { - destructor func(*T) -} - type slot[T any] struct { - value T - state uintptr - desc *descriptor[T] + value T + state uintptr + destructor func(*T) } type Handle[T any] struct { - key pthread.Key - desc *descriptor[T] + key pthread.Key + destructor func(*T) } // Alloc creates a TLS handle whose storage is visible to the Boehm GC. func Alloc[T any](destructor func(*T)) Handle[T] { - d := &descriptor[T]{destructor: destructor} var key pthread.Key - key.Create(slotDestructor[T]) - return Handle[T]{key: key, desc: d} + if ret := key.Create(slotDestructor[T]); ret != 0 { + panic(fmt.Sprintf("tls: pthread_key_create failed (errno=%d)", ret)) + } + return Handle[T]{key: key, destructor: destructor} } // Get returns the value stored in the current thread's slot. @@ -64,20 +62,21 @@ func (h Handle[T]) ensureSlot() *slot[T] { return (*slot[T])(ptr) } size := unsafe.Sizeof(slot[T]{}) - mem := c.Malloc(size) + mem := c.Calloc(1, size) if mem == nil { panic("tls: failed to allocate thread slot") } - c.Memset(mem, 0, size) s := (*slot[T])(mem) - s.desc = h.desc - registerSlot(s) + s.destructor = h.destructor if existing := h.key.Get(); existing != nil { - deregisterSlot(s) c.Free(mem) return (*slot[T])(existing) } - h.key.Set(mem) + if ret := h.key.Set(mem); ret != 0 { + c.Free(mem) + panic(fmt.Sprintf("tls: pthread_setspecific failed (errno=%d)", ret)) + } + registerSlot(s) return s } @@ -86,13 +85,13 @@ func slotDestructor[T any](ptr c.Pointer) { if s == nil { return } - deregisterSlot(s) - if s.desc != nil && s.desc.destructor != nil { - s.desc.destructor(&s.value) + if s.destructor != nil { + s.destructor(&s.value) } + deregisterSlot(s) var zero T s.value = zero - s.desc = nil + s.destructor = nil c.Free(ptr) } @@ -101,7 +100,9 @@ func registerSlot[T any](s *slot[T]) { return } start, end := s.rootRange() - bdwgc.AddRoots(start, end) + if uintptr(end) > uintptr(start) { + bdwgc.AddRoots(start, end) + } s.state |= slotRegistered } @@ -115,7 +116,7 @@ func deregisterSlot[T any](s *slot[T]) { } func (s *slot[T]) rootRange() (start, end c.Pointer) { - head := unsafe.Pointer(&s.value) - endPtr := unsafe.Pointer(uintptr(head) + unsafe.Sizeof(s.value)) - return c.Pointer(head), c.Pointer(endPtr) + begin := unsafe.Pointer(s) + endPtr := unsafe.Pointer(uintptr(begin) + unsafe.Sizeof(*s)) + return c.Pointer(begin), c.Pointer(endPtr) } diff --git a/c/tls/tls_nogc.go b/c/tls/tls_nogc.go index 950e99d..cbe3e45 100644 --- a/c/tls/tls_nogc.go +++ b/c/tls/tls_nogc.go @@ -4,31 +4,29 @@ package tls import ( + "fmt" "unsafe" "github.com/goplus/lib/c" "github.com/goplus/lib/c/pthread" ) -type descriptor[T any] struct { - destructor func(*T) -} - type slot[T any] struct { - value T - desc *descriptor[T] + value T + destructor func(*T) } type Handle[T any] struct { - key pthread.Key - desc *descriptor[T] + key pthread.Key + destructor func(*T) } func Alloc[T any](destructor func(*T)) Handle[T] { - d := &descriptor[T]{destructor: destructor} var key pthread.Key - key.Create(slotDestructor[T]) - return Handle[T]{key: key, desc: d} + if ret := key.Create(slotDestructor[T]); ret != 0 { + panic(fmt.Sprintf("tls: pthread_key_create failed (errno=%d)", ret)) + } + return Handle[T]{key: key, destructor: destructor} } func (h Handle[T]) Get() T { @@ -57,18 +55,20 @@ func (h Handle[T]) ensureSlot() *slot[T] { return (*slot[T])(ptr) } size := unsafe.Sizeof(slot[T]{}) - mem := c.Malloc(size) + mem := c.Calloc(1, size) if mem == nil { panic("tls: failed to allocate thread slot") } - c.Memset(mem, 0, size) s := (*slot[T])(mem) - s.desc = h.desc + s.destructor = h.destructor if existing := h.key.Get(); existing != nil { c.Free(mem) return (*slot[T])(existing) } - h.key.Set(mem) + if ret := h.key.Set(mem); ret != 0 { + c.Free(mem) + panic(fmt.Sprintf("tls: pthread_setspecific failed (errno=%d)", ret)) + } return s } @@ -77,11 +77,11 @@ func slotDestructor[T any](ptr c.Pointer) { if s == nil { return } - if s.desc != nil && s.desc.destructor != nil { - s.desc.destructor(&s.value) + if s.destructor != nil { + s.destructor(&s.value) } var zero T s.value = zero - s.desc = nil + s.destructor = nil c.Free(ptr) } From 0e64df5c18ac9bc5f333db3c648a0101013603c1 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Oct 2025 15:18:33 +0800 Subject: [PATCH 5/5] docs: clarify tls example imports --- docs/gc-safe-tls.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/gc-safe-tls.md b/docs/gc-safe-tls.md index 5e597ea..aeadd38 100644 --- a/docs/gc-safe-tls.md +++ b/docs/gc-safe-tls.md @@ -19,13 +19,21 @@ pthread-specific area, which Boehm does not scan by default. ## Example ```go +package main + +import ( + "log" + + "github.com/goplus/lib/c/tls" +) + type stats struct { handled int } var threadStats = tls.Alloc[*stats](func(s **stats) { // Destructor runs when the thread exits. - if s != nil && *s != nil { + if *s != nil { log.Printf("thread handled %d requests", (*s).handled) } })