Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions c/bdwgc/bdwgc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
122 changes: 122 additions & 0 deletions c/tls/tls.go
Original file line number Diff line number Diff line change
@@ -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)
}
87 changes: 87 additions & 0 deletions c/tls/tls_nogc.go
Original file line number Diff line number Diff line change
@@ -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)
}
89 changes: 89 additions & 0 deletions c/tls/tls_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
58 changes: 58 additions & 0 deletions docs/gc-safe-tls.md
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The example code uses log.Printf, but the log package is not imported in the code snippet. This makes the example incomplete and could be confusing for users. Please consider adding the import to the example to make it self-contained and runnable.

}
})

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.
Loading