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 new file mode 100644 index 0000000..88d3539 --- /dev/null +++ b/c/tls/tls.go @@ -0,0 +1,122 @@ +//go:build !nogc + +package tls + +import ( + "fmt" + "unsafe" + + "github.com/goplus/lib/c" + "github.com/goplus/lib/c/bdwgc" + "github.com/goplus/lib/c/pthread" +) + +const slotRegistered = 1 << iota + +type slot[T any] struct { + value T + state uintptr + destructor func(*T) +} + +type Handle[T any] struct { + 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] { + var key pthread.Key + 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. +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.Calloc(1, size) + if mem == nil { + panic("tls: failed to allocate thread slot") + } + s := (*slot[T])(mem) + s.destructor = h.destructor + if existing := h.key.Get(); existing != nil { + c.Free(mem) + return (*slot[T])(existing) + } + 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 +} + +func slotDestructor[T any](ptr c.Pointer) { + s := (*slot[T])(ptr) + if s == nil { + return + } + if s.destructor != nil { + s.destructor(&s.value) + } + deregisterSlot(s) + var zero T + s.value = zero + s.destructor = nil + c.Free(ptr) +} + +func registerSlot[T any](s *slot[T]) { + if s.state&slotRegistered != 0 { + return + } + start, end := s.rootRange() + if uintptr(end) > uintptr(start) { + 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) { + 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 new file mode 100644 index 0000000..cbe3e45 --- /dev/null +++ b/c/tls/tls_nogc.go @@ -0,0 +1,87 @@ +//go:build nogc +// +build nogc + +package tls + +import ( + "fmt" + "unsafe" + + "github.com/goplus/lib/c" + "github.com/goplus/lib/c/pthread" +) + +type slot[T any] struct { + value T + destructor func(*T) +} + +type Handle[T any] struct { + key pthread.Key + destructor func(*T) +} + +func Alloc[T any](destructor func(*T)) Handle[T] { + var key pthread.Key + 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 { + 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.Calloc(1, size) + if mem == nil { + panic("tls: failed to allocate thread slot") + } + s := (*slot[T])(mem) + s.destructor = h.destructor + if existing := h.key.Get(); existing != nil { + c.Free(mem) + return (*slot[T])(existing) + } + if ret := h.key.Set(mem); ret != 0 { + c.Free(mem) + panic(fmt.Sprintf("tls: pthread_setspecific failed (errno=%d)", ret)) + } + return s +} + +func slotDestructor[T any](ptr c.Pointer) { + s := (*slot[T])(ptr) + if s == nil { + return + } + if s.destructor != nil { + s.destructor(&s.value) + } + var zero T + s.value = zero + s.destructor = 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..aeadd38 --- /dev/null +++ b/docs/gc-safe-tls.md @@ -0,0 +1,58 @@ +# 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 +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 { + log.Printf("thread handled %d requests", (*s).handled) + } +}) + +func recordRequest() { + cur := threadStats.Get() + if cur == nil { + cur = &stats{} + } + cur.handled++ + threadStats.Set(cur) +} + +func resetStats() { + threadStats.Clear() +} +``` + +In GC builds the slot is automatically registered as a root, which prevents +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.