diff --git a/runtime/internal/runtime/defer_tls_baremetal.go b/runtime/internal/runtime/defer_tls_baremetal.go deleted file mode 100644 index d8c4938cca..0000000000 --- a/runtime/internal/runtime/defer_tls_baremetal.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build baremetal - -/* - * Copyright (c) 2025 The XGo Authors (xgo.dev). 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 runtime - -// globalDeferHead stores the current defer chain head. -// In baremetal single-threaded environment, a global variable -// replaces pthread TLS. -var globalDeferHead *Defer - -// SetThreadDefer associates the current thread with the given defer chain. -func SetThreadDefer(head *Defer) { - globalDeferHead = head -} - -// GetThreadDefer returns the current thread's defer chain head. -func GetThreadDefer() *Defer { - return globalDeferHead -} - -// ClearThreadDefer resets the current thread's defer chain to nil. -func ClearThreadDefer() { - globalDeferHead = nil -} diff --git a/runtime/internal/runtime/defer_tls.go b/runtime/internal/runtime/g.go similarity index 57% rename from runtime/internal/runtime/defer_tls.go rename to runtime/internal/runtime/g.go index 6a430e9924..4a1a3e5f04 100644 --- a/runtime/internal/runtime/defer_tls.go +++ b/runtime/internal/runtime/g.go @@ -1,7 +1,5 @@ -//go:build !baremetal - /* - * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. + * Copyright (c) 2026 The XGo Authors (xgo.dev). 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. @@ -18,25 +16,27 @@ package runtime -import "github.com/goplus/llgo/runtime/internal/clite/tls" +import "unsafe" -var deferTLS = tls.Alloc[*Defer](func(head **Defer) { - if head != nil { - *head = nil - } -}) +// g holds the runtime state owned by one LLGo goroutine. +type g struct { + defer_ *Defer + panic_ unsafe.Pointer + goexit bool + isMain bool +} -// SetThreadDefer associates the current thread with the given defer chain. +// SetThreadDefer associates the current goroutine with the given defer chain. func SetThreadDefer(head *Defer) { - deferTLS.Set(head) + getg().defer_ = head } -// GetThreadDefer returns the current thread's defer chain head. +// GetThreadDefer returns the current goroutine's defer chain head. func GetThreadDefer() *Defer { - return deferTLS.Get() + return getg().defer_ } -// ClearThreadDefer resets the current thread's defer chain to nil. +// ClearThreadDefer resets the current goroutine's defer chain to nil. func ClearThreadDefer() { - deferTLS.Clear() + getg().defer_ = nil } diff --git a/runtime/internal/runtime/g_global.go b/runtime/internal/runtime/g_global.go new file mode 100644 index 0000000000..255733ff01 --- /dev/null +++ b/runtime/internal/runtime/g_global.go @@ -0,0 +1,25 @@ +//go:build !llgo || baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). 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 runtime + +var globalG g + +func getg() *g { + return &globalG +} diff --git a/runtime/internal/runtime/g_pthread.go b/runtime/internal/runtime/g_pthread.go new file mode 100644 index 0000000000..048ecc1dc6 --- /dev/null +++ b/runtime/internal/runtime/g_pthread.go @@ -0,0 +1,66 @@ +//go:build llgo && !baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). 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 runtime + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + "github.com/goplus/llgo/runtime/internal/clite/pthread" +) + +var gKey = newGKey() + +func newGKey() pthread.Key { + var key pthread.Key + if ret := key.Create(pthread.KeyDestructor(destroyG)); ret != 0 { + c.Fprintf(c.Stderr, c.Str("runtime: pthread_key_create failed (errno=%d)\n"), ret) + panic("runtime: failed to create getg key") + } + return key +} + +func getg() *g { + if ptr := gKey.Get(); ptr != nil { + return (*g)(ptr) + } + ptr := AllocRoot(unsafe.Sizeof(g{})) + if ptr == nil { + panic("runtime: failed to allocate g") + } + c.Memset(ptr, 0, unsafe.Sizeof(g{})) + if ret := gKey.Set(ptr); ret != 0 { + FreeRoot(ptr) + c.Fprintf(c.Stderr, c.Str("runtime: pthread_setspecific failed (errno=%d)\n"), ret) + panic("runtime: failed to install g") + } + return (*g)(ptr) +} + +func destroyG(ptr c.Pointer) { + gp := (*g)(ptr) + if gp == nil { + return + } + if gp.panic_ != nil { + c.Free(gp.panic_) + } + *gp = g{} + FreeRoot(ptr) +} diff --git a/runtime/internal/runtime/z_default.go b/runtime/internal/runtime/z_default.go index d0757f9cb1..7f0b7601ba 100644 --- a/runtime/internal/runtime/z_default.go +++ b/runtime/internal/runtime/z_default.go @@ -16,7 +16,8 @@ var ( // Rethrow rethrows a panic. func Rethrow(link *Defer) { - if ptr := excepKey.Get(); ptr != nil { + gp := getg() + if ptr := gp.panic_; ptr != nil { if link == nil { TracePanic(*(*any)(ptr)) if PanicTraceback == nil || !PanicTraceback(2) { @@ -27,7 +28,7 @@ func Rethrow(link *Defer) { } else { c.Siglongjmp(link.Addr, 1) } - } else if ptr := goexitKey.Get(); ptr != nil { + } else if gp.goexit { // Goexit must run deferred functions before terminating the current // goroutine. Reuse the longjmp-based defer unwinding: // 1) If we have a defer frame, longjmp to it so it can execute defers. @@ -36,7 +37,7 @@ func Rethrow(link *Defer) { if link != nil { c.Siglongjmp(link.Addr, 1) } - if pthread.Equal(mainThread, pthread.Self()) != 0 { + if gp.isMain { fatal("no goroutines (main called runtime.Goexit) - deadlock!") c.Exit(2) } diff --git a/runtime/internal/runtime/z_rt.go b/runtime/internal/runtime/z_rt.go index 17c89f55da..d771ab94cf 100644 --- a/runtime/internal/runtime/z_rt.go +++ b/runtime/internal/runtime/z_rt.go @@ -20,7 +20,6 @@ import ( "unsafe" c "github.com/goplus/llgo/runtime/internal/clite" - "github.com/goplus/llgo/runtime/internal/clite/pthread" "github.com/goplus/llgo/runtime/internal/clite/setjmp" ) @@ -38,9 +37,10 @@ type Defer struct { // Recover recovers a panic. func Recover() (ret any) { - ptr := excepKey.Get() + gp := getg() + ptr := gp.panic_ if ptr != nil { - excepKey.Set(nil) + gp.panic_ = nil ret = *(*any)(ptr) c.Free(ptr) if PanicRecovered != nil { @@ -58,26 +58,20 @@ func Panic(v any) { SavePanicCallerFrames() ptr := c.Malloc(unsafe.Sizeof(v)) *(*any)(ptr) = v - excepKey.Set(ptr) + gp := getg() + gp.panic_ = ptr - Rethrow((*Defer)(c.GoDeferData())) + Rethrow(gp.defer_) } -var ( - excepKey pthread.Key - goexitKey pthread.Key - mainThread pthread.Thread -) - func Goexit() { - goexitKey.Set(unsafe.Pointer(&goexitKey)) - Rethrow((*Defer)(c.GoDeferData())) + gp := getg() + gp.goexit = true + Rethrow(gp.defer_) } func init() { - excepKey.Create(nil) - goexitKey.Create(nil) - mainThread = pthread.Self() + getg().isMain = true } // ----------------------------------------------------------------------------- diff --git a/test/go/runtime_g_state_test.go b/test/go/runtime_g_state_test.go new file mode 100644 index 0000000000..8d808ac5df --- /dev/null +++ b/test/go/runtime_g_state_test.go @@ -0,0 +1,80 @@ +package gotest + +import ( + "runtime" + "testing" +) + +type runtimeGStateResult struct { + id int + recovered any + order string +} + +func runtimeGStatePanic(id int, ready chan<- struct{}, start <-chan struct{}, results chan<- runtimeGStateResult) { + order := "" + defer func() { + result := runtimeGStateResult{id: id, recovered: recover(), order: order + "r"} + results <- result + }() + defer func() { + order += "d" + }() + ready <- struct{}{} + <-start + order += "p" + panic(id) +} + +func TestRuntimeGStateIsolation(t *testing.T) { + ready := make(chan struct{}, 2) + start := make(chan struct{}) + results := make(chan runtimeGStateResult, 2) + for id := 1; id <= 2; id++ { + go runtimeGStatePanic(id, ready, start, results) + } + <-ready + <-ready + close(start) + + seen := [3]bool{} + for i := 0; i < 2; i++ { + result := <-results + if result.id < 1 || result.id > 2 { + t.Fatalf("unexpected goroutine id %d", result.id) + } + if seen[result.id] { + t.Fatalf("duplicate result from goroutine %d", result.id) + } + seen[result.id] = true + if result.recovered != result.id { + t.Fatalf("goroutine %d recovered %v", result.id, result.recovered) + } + if result.order != "pdr" { + t.Fatalf("goroutine %d defer order = %q, want %q", result.id, result.order, "pdr") + } + } + + goexitDone := make(chan any, 1) + go func() { + defer func() { + goexitDone <- recover() + }() + runtime.Goexit() + goexitDone <- "Goexit returned" + }() + if recovered := <-goexitDone; recovered != nil { + t.Fatalf("recover during Goexit = %v, want nil", recovered) + } + + var recovered any + func() { + defer func() { + recovered = recover() + }() + panic("main panic") + }() + if recovered != "main panic" { + t.Fatalf("main goroutine recovered %v, want %q", recovered, "main panic") + } +} diff --git a/test/llgoext/runtime_g_test.go b/test/llgoext/runtime_g_test.go new file mode 100644 index 0000000000..52789908d7 --- /dev/null +++ b/test/llgoext/runtime_g_test.go @@ -0,0 +1,169 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). 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 llgoext + +import ( + "testing" + "unsafe" +) + +//go:linkname runtimeGetGForTest github.com/goplus/llgo/runtime/internal/runtime.getg +func runtimeGetGForTest() unsafe.Pointer + +//go:linkname runtimeGetThreadDeferForGTest github.com/goplus/llgo/runtime/internal/runtime.GetThreadDefer +func runtimeGetThreadDeferForGTest() unsafe.Pointer + +//go:linkname runtimeSetThreadDeferForGTest github.com/goplus/llgo/runtime/internal/runtime.SetThreadDefer +func runtimeSetThreadDeferForGTest(unsafe.Pointer) + +//go:linkname runtimeClearThreadDeferForGTest github.com/goplus/llgo/runtime/internal/runtime.ClearThreadDefer +func runtimeClearThreadDeferForGTest() + +type runtimeGPointerResult struct { + first unsafe.Pointer + second unsafe.Pointer +} + +func TestRuntimeGetGIsolation(t *testing.T) { + mainG := runtimeGetGForTest() + if mainG == nil || runtimeGetGForTest() != mainG { + t.Fatalf("main getg is not stable: %p", mainG) + } + + ready := make(chan unsafe.Pointer, 2) + start := make(chan struct{}) + results := make(chan runtimeGPointerResult, 2) + for i := 0; i < 2; i++ { + go func() { + first := runtimeGetGForTest() + ready <- first + <-start + results <- runtimeGPointerResult{first: first, second: runtimeGetGForTest()} + }() + } + firstReady := <-ready + secondReady := <-ready + close(start) + + first := <-results + second := <-results + for i, result := range []runtimeGPointerResult{first, second} { + if result.first == nil || result.first != result.second { + t.Fatalf("goroutine %d getg values = (%p, %p), want one stable non-nil pointer", i, result.first, result.second) + } + if result.first == mainG { + t.Fatalf("goroutine %d shared main g %p", i, mainG) + } + } + if firstReady == secondReady { + t.Fatalf("goroutines shared g %p", firstReady) + } +} + +type runtimeDeferProbeResult struct { + before unsafe.Pointer + inside unsafe.Pointer + after unsafe.Pointer +} + +func runtimeDeferProbe(ready chan<- unsafe.Pointer, start <-chan struct{}, results chan<- runtimeDeferProbeResult) { + result := runtimeDeferProbeResult{before: runtimeGetThreadDeferForGTest()} + func() { + defer func() {}() + result.inside = runtimeGetThreadDeferForGTest() + ready <- result.inside + <-start + }() + result.after = runtimeGetThreadDeferForGTest() + results <- result +} + +func TestRuntimeDeferHeadIsolation(t *testing.T) { + mainHead := runtimeGetThreadDeferForGTest() + ready := make(chan unsafe.Pointer, 2) + start := make(chan struct{}) + results := make(chan runtimeDeferProbeResult, 2) + + go runtimeDeferProbe(ready, start, results) + go runtimeDeferProbe(ready, start, results) + first := <-ready + second := <-ready + close(start) + + if first == nil || second == nil { + t.Fatalf("active defer heads = (%p, %p), want two non-nil heads", first, second) + } + if first == second { + t.Fatalf("concurrent goroutines shared defer head %p", first) + } + for i := 0; i < 2; i++ { + result := <-results + if result.inside == nil { + t.Fatal("active defer head is nil") + } + if result.after != result.before { + t.Fatalf("defer head after return = %p, want previous %p", result.after, result.before) + } + } + if got := runtimeGetThreadDeferForGTest(); got != mainHead { + t.Fatalf("main defer head changed to %p, want %p", got, mainHead) + } +} + +type runtimeDeferAccessorResult struct { + want unsafe.Pointer + got unsafe.Pointer + cleared unsafe.Pointer +} + +func runtimeDeferAccessorProbe(token unsafe.Pointer, results chan<- runtimeDeferAccessorResult) { + previous := runtimeGetThreadDeferForGTest() + runtimeSetThreadDeferForGTest(token) + got := runtimeGetThreadDeferForGTest() + runtimeClearThreadDeferForGTest() + cleared := runtimeGetThreadDeferForGTest() + runtimeSetThreadDeferForGTest(previous) + results <- runtimeDeferAccessorResult{want: token, got: got, cleared: cleared} +} + +func TestRuntimeDeferAccessors(t *testing.T) { + results := make(chan runtimeDeferAccessorResult, 2) + first := new(byte) + second := new(byte) + go runtimeDeferAccessorProbe(unsafe.Pointer(first), results) + go runtimeDeferAccessorProbe(unsafe.Pointer(second), results) + + for i := 0; i < 2; i++ { + result := <-results + if result.got != result.want { + t.Fatalf("defer head = %p, want %p", result.got, result.want) + } + if result.cleared != nil { + t.Fatalf("cleared defer head = %p, want nil", result.cleared) + } + } +} + +var runtimeGSink unsafe.Pointer + +func BenchmarkRuntimeGetG(b *testing.B) { + for i := 0; i < b.N; i++ { + runtimeGSink = runtimeGetGForTest() + } +}