Skip to content
Merged
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
14 changes: 14 additions & 0 deletions runtime/internal/clite/bdwgc/bdwgc.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ func Realloc(ptr c.Pointer, size uintptr) c.Pointer
//go:linkname Free C.GC_free
func Free(ptr c.Pointer)

// AddRoots registers a memory region [start, end) as a GC root. The caller
// must ensure that the range remains valid until RemoveRoots is invoked with
// the same boundaries. This is typically used for TLS slots that store Go
// pointers.
//
//go:linkname AddRoots C.GC_add_roots
func AddRoots(start, end c.Pointer)

// RemoveRoots unregisters a region previously registered with AddRoots. The
// start and end pointers must exactly match the earlier AddRoots call.
//
//go:linkname RemoveRoots C.GC_remove_roots
func RemoveRoots(start, end c.Pointer)

// -----------------------------------------------------------------------------

//go:linkname RegisterFinalizer C.GC_register_finalizer
Expand Down
128 changes: 128 additions & 0 deletions runtime/internal/clite/tls/tls_common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//go:build llgo

/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// Package tls provides generic thread-local storage backed by POSIX pthread
// TLS. When built with the GC-enabled configuration (llgo && !nogc), TLS slots
// are automatically registered with the BDWGC garbage collector so pointers
// stored in thread-local state remain visible to the collector. Builds without
// GC integration (llgo && nogc) simply fall back to pthread TLS without root
// registration.
//
// Basic usage:
//
// h := tls.Alloc[int](nil)
// h.Set(42)
// val := h.Get() // returns 42
//
// With destructor:
//
// h := tls.Alloc[*Resource](func(r **Resource) {
// if r != nil && *r != nil {
// (*r).Close()
// }
// })
//
// Build tags:
// - llgo && !nogc: Enables GC-aware slot registration via BDWGC
// - llgo && nogc: Disables GC integration; TLS acts as plain pthread TLS
package tls

import (
"unsafe"

c "github.com/goplus/llgo/runtime/internal/clite"
"github.com/goplus/llgo/runtime/internal/clite/pthread"
)

type Handle[T any] struct {
key pthread.Key
destructor func(*T)
}

// Alloc creates a TLS handle backed by pthread TLS.
func Alloc[T any](destructor func(*T)) Handle[T] {
var key pthread.Key
if ret := key.Create(slotDestructor[T]); ret != 0 {
c.Fprintf(c.Stderr, c.Str("tls: pthread_key_create failed (errno=%d)\n"), ret)
panic("tls: failed to create thread local storage key")
}
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)
c.Fprintf(c.Stderr, c.Str("tls: pthread_setspecific failed (errno=%d)\n"), ret)
panic("tls: failed to set thread local storage value")
}
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)
}
74 changes: 74 additions & 0 deletions runtime/internal/clite/tls/tls_gc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//go:build llgo && !nogc

/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package tls

import (
"unsafe"

c "github.com/goplus/llgo/runtime/internal/clite"
"github.com/goplus/llgo/runtime/internal/clite/bdwgc"
)

const slotRegistered = 1 << iota

const maxSlotSize = 1 << 20 // 1 MiB sanity cap

type slot[T any] struct {
value T
state uintptr
destructor func(*T)
}

func registerSlot[T any](s *slot[T]) {
if s.state&slotRegistered != 0 {
return
}
start, end := s.rootRange()
size := uintptr(end) - uintptr(start)
if size == 0 {
return
}
if size > maxSlotSize {
panic("tls: slot size exceeds maximum")
}
bdwgc.AddRoots(start, end)
s.state |= slotRegistered
}

func deregisterSlot[T any](s *slot[T]) {
if s == nil || s.state&slotRegistered == 0 {
return
}
s.state &^= slotRegistered
start, end := s.rootRange()
if uintptr(end) > uintptr(start) {
bdwgc.RemoveRoots(start, end)
}
}

func (s *slot[T]) rootRange() (start, end c.Pointer) {
begin := unsafe.Pointer(s)
size := unsafe.Sizeof(*s)
beginAddr := uintptr(begin)
if beginAddr > ^uintptr(0)-size {
panic("tls: pointer arithmetic overflow in rootRange")
}
endPtr := unsafe.Pointer(beginAddr + size)
return c.Pointer(begin), c.Pointer(endPtr)
}
28 changes: 28 additions & 0 deletions runtime/internal/clite/tls/tls_nogc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//go:build llgo && nogc

/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package tls

type slot[T any] struct {
value T
destructor func(*T)
}

func registerSlot[T any](s *slot[T]) {}

func deregisterSlot[T any](s *slot[T]) {}
141 changes: 141 additions & 0 deletions runtime/internal/clite/tls/tls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//go:build llgo

/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package tls_test

import (
"fmt"
"sync"
"testing"

"github.com/goplus/llgo/runtime/internal/clite/tls"
)

func TestAllocReadWrite(t *testing.T) {
h := tls.Alloc[int](nil)
if got := h.Get(); got != 0 {
t.Fatalf("zero slot = %d, want 0", got)
}
h.Set(42)
if got := h.Get(); got != 42 {
t.Fatalf("Set/Get mismatch: got %d", got)
}
h.Clear()
if got := h.Get(); got != 0 {
t.Fatalf("Clear() did not reset slot, got %d", got)
}
}

func TestAllocThreadLocalIsolation(t *testing.T) {
h := tls.Alloc[int](nil)
h.Set(7)

const want = 99
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
if got := h.Get(); got != 0 {
t.Errorf("new goroutine initial value = %d, want 0", got)
}
h.Set(want)
if got := h.Get(); got != want {
t.Errorf("goroutine value = %d, want %d", got, want)
}
}()
wg.Wait()

if got := h.Get(); got != 7 {
t.Fatalf("main goroutine value changed to %d", got)
}
}

func TestDestructorRuns(t *testing.T) {
var mu sync.Mutex
var calls int
values := make([]int, 0, 1)

h := tls.Alloc[*int](func(p **int) {
mu.Lock()
defer mu.Unlock()
if p != nil && *p != nil {
calls++
values = append(values, **p)
}
})

var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
val := new(int)
*val = 123
h.Set(val)
}()
wg.Wait()

mu.Lock()
defer mu.Unlock()
if calls == 0 {
t.Fatalf("expected destructor to be invoked")
}
if len(values) != 1 || values[0] != 123 {
t.Fatalf("destructor saw unexpected values: %v", values)
}
}

func TestAllocStress(t *testing.T) {
const sequentialIterations = 200_000

h := tls.Alloc[int](nil)
for i := 0; i < sequentialIterations; i++ {
h.Set(i)
if got := h.Get(); got != i {
t.Fatalf("stress iteration %d: got %d want %d", i, got, i)
}
}

var wg sync.WaitGroup
const (
goroutines = 32
iterationsPerGoroutine = 1_000
)
errs := make(chan error, goroutines)
wg.Add(goroutines)
for g := 0; g < goroutines; g++ {
go func(offset int) {
defer wg.Done()
local := tls.Alloc[int](nil)
for i := 0; i < iterationsPerGoroutine; i++ {
v := offset*iterationsPerGoroutine + i
local.Set(v)
if got := local.Get(); got != v {
errs <- fmt.Errorf("goroutine %d iteration %d: got %d want %d", offset, i, got, v)
return
}
}
}(g)
}
wg.Wait()
close(errs)
for err := range errs {
if err != nil {
t.Fatal(err)
}
}
}
Loading
Loading