diff --git a/_demo/go/export/export.go b/_demo/go/export/export.go index 2dd5392704..2f2bee1a41 100644 --- a/_demo/go/export/export.go +++ b/_demo/go/export/export.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "runtime" "unsafe" @@ -134,6 +135,11 @@ func RunGoroutine(value int) int { return <-ch } +//export FormatValue +func FormatValue(value string, number int) string { + return fmt.Sprintf("%s:%d", value, number) +} + // Functions with small struct parameters and return values //export CreateSmallStruct diff --git a/_demo/go/export/libexport.h.want b/_demo/go/export/libexport.h.want index 67deee32be..919f305869 100644 --- a/_demo/go/export/libexport.h.want +++ b/_demo/go/export/libexport.h.want @@ -121,6 +121,9 @@ mul(float a, float b); void github_com_goplus_llgo__demo_go_export_c_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/_demo/go/export/c.init") +intptr_t +AllThreadsSyscallStatus(void); + main_ComplexData CreateComplexData(void); @@ -163,6 +166,9 @@ CreateStringMap(void); C_XType CreateXType(int32_t id, GoString name, double value, _Bool flag); +GoString +FormatValue(GoString value, intptr_t number); + main_FuncInfoResult GetFuncInfo(void); diff --git a/_demo/go/export/runtime_hooks_linux.go b/_demo/go/export/runtime_hooks_linux.go new file mode 100644 index 0000000000..5ecf8aa427 --- /dev/null +++ b/_demo/go/export/runtime_hooks_linux.go @@ -0,0 +1,28 @@ +//go:build linux + +package main + +import "syscall" + +func gettimeofdayStatus() int { + var tv syscall.Timeval + if err := syscall.Gettimeofday(&tv); err != nil { + if errno, ok := err.(syscall.Errno); ok { + return int(errno) + } + return int(syscall.EINVAL) + } + if tv.Sec <= 0 { + return int(syscall.EINVAL) + } + return 0 +} + +//export AllThreadsSyscallStatus +func AllThreadsSyscallStatus() int { + if status := gettimeofdayStatus(); status != 0 { + return status + } + _, _, err := syscall.AllThreadsSyscall(syscall.SYS_GETPID, 0, 0, 0) + return int(err) +} diff --git a/_demo/go/export/runtime_hooks_other.go b/_demo/go/export/runtime_hooks_other.go new file mode 100644 index 0000000000..2d5176f5ea --- /dev/null +++ b/_demo/go/export/runtime_hooks_other.go @@ -0,0 +1,8 @@ +//go:build !linux + +package main + +//export AllThreadsSyscallStatus +func AllThreadsSyscallStatus() int { + return 0 +} diff --git a/_demo/go/export/use/Makefile b/_demo/go/export/use/Makefile index a7c75c2722..ee7dcee847 100644 --- a/_demo/go/export/use/Makefile +++ b/_demo/go/export/use/Makefile @@ -24,6 +24,7 @@ else SHARED_EXT = so PLATFORM_LIBS = $(shell pkg-config --libs libunwind 2>/dev/null || echo -lunwind) endif +STATIC_STDLIB_LIBS = $(shell pkg-config --libs libffi 2>/dev/null || echo -lffi) -lresolv # Library and flags based on link type ifeq ($(LINK_TYPE),shared) @@ -35,7 +36,7 @@ ifeq ($(LINK_TYPE),shared) else BUILDMODE = c-archive LIBRARY = ../libexport.a - LDFLAGS = $(LIBRARY) $(RUNTIME_LIBS) $(PLATFORM_LIBS) + LDFLAGS = $(LIBRARY) $(RUNTIME_LIBS) $(PLATFORM_LIBS) $(STATIC_STDLIB_LIBS) BUILD_MSG = "Building Go static library..." LINK_MSG = "Linking with static library..." endif diff --git a/_demo/go/export/use/main.c b/_demo/go/export/use/main.c index 4e1a911b45..4c6534a7c8 100644 --- a/_demo/go/export/use/main.c +++ b/_demo/go/export/use/main.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include "../libexport.h" @@ -59,6 +60,16 @@ int main() { HelloWorld(); printf("\n"); + // Verify that a C library can initialize and call standard-library paths + // that depend on the runtime hooks supplied by LLGo. + GoString formatted = FormatValue((GoString){"answer", 6}, 42); + assert(go_string_equals(formatted, "answer:42")); +#ifdef __linux__ + assert(AllThreadsSyscallStatus() == ENOTSUP); +#else + assert(AllThreadsSyscallStatus() == 0); +#endif + // Test small struct main_SmallStruct small = CreateSmallStruct(5, 1); // 1 for true assert(small.ID == 5); diff --git a/internal/build/main_module.go b/internal/build/main_module.go index 5dbf0ad81f..1b46299007 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -76,10 +76,27 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g }, LPkg: mainPkg, } + var rtInit llssa.Function + if cfg.rtInit { + rtInit = declareNoArgFunc(mainPkg, rtPkgPath+".init") + } + var abiInit llssa.Function + if cfg.abiInit != 0 { + abiInit = mainPkg.InitAbiTypesFor("init$abitypes", func(sym *llssa.AbiSymbol) bool { + if _, ok := cfg.abiSymbols[sym.Name]; !ok { + return false + } + return filterAbiSymbol(cfg.abiInit, sym) + }) + } if ctx.buildConf.BuildMode != BuildModeExe { - if cfg.rtInit { - defineLibraryRuntimeInit(mainPkg, declareNoArgFunc(mainPkg, rtPkgPath+".init")) + if rtInit != nil || abiInit != nil { + initArraySection := "" + if ctx.buildConf.BuildMode == BuildModeCShared && ctx.buildConf.Goos == "linux" { + initArraySection = ".init_array" + } + defineLibraryRuntimeInit(mainPkg, rtInit, abiInit, initArraySection) } return mainAPkg } @@ -95,21 +112,6 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g pyFinalize = declareNoArgFunc(mainPkg, "Py_Finalize") } - var rtInit llssa.Function - if cfg.rtInit { - rtInit = declareNoArgFunc(mainPkg, rtPkgPath+".init") - } - - var abiInit llssa.Function - if cfg.abiInit != 0 { - abiInit = mainPkg.InitAbiTypesFor("init$abitypes", func(sym *llssa.AbiSymbol) bool { - if _, ok := cfg.abiSymbols[sym.Name]; !ok { - return false - } - return filterAbiSymbol(cfg.abiInit, sym) - }) - } - mainInit := declareNoArgFunc(mainPkg, pkg.PkgPath+".init") mainMain := declareNoArgFunc(mainPkg, pkg.PkgPath+".main") @@ -130,20 +132,34 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g return mainAPkg } -// defineLibraryRuntimeInit arranges for the LLGo runtime to be initialized -// before a C program calls an exported Go function. llvm.global_ctors is -// lowered to the platform's native constructor mechanism for both shared -// libraries and archive members. -func defineLibraryRuntimeInit(pkg llssa.Package, rtInit llssa.Function) { +// defineLibraryRuntimeInit arranges for the LLGo runtime and reflection ABI +// tables to be initialized before a C program calls an exported Go function. +// Linux shared libraries use .init_array explicitly because the raw LLVM +// target machine lowers llvm.global_ctors to legacy .ctors; other library +// formats use LLVM's platform-specific constructor lowering. +func defineLibraryRuntimeInit(pkg llssa.Package, rtInit, abiInit llssa.Function, initArraySection string) { const ctorName = "__llgo_runtime_ctor" ctor := pkg.NewFunc(ctorName, llssa.NoArgsNoRet, llssa.InC) ctorValue := pkg.Module().NamedFunction(ctorName) ctorValue.SetLinkage(llvm.InternalLinkage) b := ctor.MakeBody(1) - b.Call(rtInit.Expr) + if rtInit != nil { + b.Call(rtInit.Expr) + } + if abiInit != nil { + b.Call(abiInit.Expr) + } b.Return() mod := pkg.Module() + if initArraySection != "" { + initEntry := llvm.AddGlobal(mod, ctorValue.Type(), "__llgo_runtime_ctor_init") + initEntry.SetInitializer(ctorValue) + initEntry.SetGlobalConstant(true) + initEntry.SetSection(initArraySection) + initEntry.SetVisibility(llvm.HiddenVisibility) + return + } llvmCtx := mod.Context() priority := llvm.ConstInt(llvmCtx.Int32Type(), 65535, false) entry := llvmCtx.ConstStruct([]llvm.Value{ diff --git a/internal/build/main_module_test.go b/internal/build/main_module_test.go index f803182482..b2b9bf6aa6 100644 --- a/internal/build/main_module_test.go +++ b/internal/build/main_module_test.go @@ -97,10 +97,14 @@ func TestGenMainModuleLibraryInitializesRuntime(t *testing.T) { mod := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{rtInit: true}) ir := mod.LPkg.String() checks := []string{ - "@llvm.global_ctors = appending global", "define internal void @__llgo_runtime_ctor()", "call void @\"github.com/goplus/llgo/runtime/internal/runtime.init\"()", } + if mode == BuildModeCShared { + checks = append(checks, `@__llgo_runtime_ctor_init = hidden constant ptr @__llgo_runtime_ctor, section ".init_array"`) + } else { + checks = append(checks, "@llvm.global_ctors = appending global") + } for _, want := range checks { if !strings.Contains(ir, want) { t.Fatalf("library module IR missing %q:\n%s", want, ir) @@ -113,6 +117,21 @@ func TestGenMainModuleLibraryInitializesRuntime(t *testing.T) { } } +func TestDefineLibraryRuntimeInitInitializesAbiTypes(t *testing.T) { + prog := llssa.NewProgram(nil) + pkg := prog.NewPackage("", "example.com/foo.main") + rtInit := declareNoArgFunc(pkg, llssa.PkgRuntime+".init") + abiInit := declareNoArgFunc(pkg, "init$abitypes") + + defineLibraryRuntimeInit(pkg, rtInit, abiInit, "") + + ir := pkg.String() + assertInOrder(t, ir, + "call void @\"github.com/goplus/llgo/runtime/internal/runtime.init\"()", + "call void @\"init$abitypes\"()", + ) +} + func assertInOrder(t *testing.T, s string, wants ...string) { t.Helper() offset := 0 diff --git a/runtime/internal/lib/runtime/gettimeofday_linux_amd64_llgo.go b/runtime/internal/lib/runtime/gettimeofday_linux_amd64_llgo.go new file mode 100644 index 0000000000..41c075b0a5 --- /dev/null +++ b/runtime/internal/lib/runtime/gettimeofday_linux_amd64_llgo.go @@ -0,0 +1,26 @@ +//go:build linux && amd64 && !baremetal + +package runtime + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + cliteos "github.com/goplus/llgo/runtime/internal/clite/os" + clitesyscall "github.com/goplus/llgo/runtime/internal/clite/syscall" +) + +//go:linkname c_gettimeofday C.gettimeofday +func c_gettimeofday(tv *clitesyscall.Timeval, tz unsafe.Pointer) c.Int + +// syscall.Gettimeofday and syscall.Time call an amd64 assembly helper. LLGo's +// replacement syscall package does not include that symbol, so forward it to +// libc while preserving syscall's errno result. +// +//go:linkname syscall_gettimeofday syscall.gettimeofday +func syscall_gettimeofday(tv *clitesyscall.Timeval) clitesyscall.Errno { + if c_gettimeofday(tv, nil) != 0 { + return clitesyscall.Errno(cliteos.Errno()) + } + return 0 +} diff --git a/runtime/internal/lib/runtime/os_darwin.go b/runtime/internal/lib/runtime/os_darwin.go index e0f3fa16bb..d4a8cc0f97 100644 --- a/runtime/internal/lib/runtime/os_darwin.go +++ b/runtime/internal/lib/runtime/os_darwin.go @@ -9,11 +9,27 @@ func sysctlbynameInt32(name []byte) (int32, int32) { return ret, out } +func sysctlbynameBytes(name, out []byte) int32 { + nout := uintptr(len(out)) + return sysctlbyname(&name[0], &out[0], &nout, nil, 0) +} + //go:linkname internal_cpu_getsysctlbyname internal/cpu.getsysctlbyname func internal_cpu_getsysctlbyname(name []byte) (int32, int32) { return sysctlbynameInt32(name) } +//go:linkname internal_cpu_sysctlbynameInt32 internal/cpu.sysctlbynameInt32 +func internal_cpu_sysctlbynameInt32(name []byte) (int32, int32) { + return sysctlbynameInt32(name) +} + +//go:linkname internal_cpu_sysctlbynameBytes internal/cpu.sysctlbynameBytes +func internal_cpu_sysctlbynameBytes(name, out []byte) int32 { + return sysctlbynameBytes(name, out) +} + +//go:cgo_import_dynamic libc_sysctlbyname sysctlbyname "/usr/lib/libSystem.B.dylib" var libc_sysctlbyname_trampoline_addr uintptr // adapted from runtime/sys_darwin.go in the pattern of sysctl() above, as defined in x/sys/unix diff --git a/runtime/internal/lib/runtime/syscall_linux_llgo.go b/runtime/internal/lib/runtime/syscall_linux_llgo.go new file mode 100644 index 0000000000..44c07816d4 --- /dev/null +++ b/runtime/internal/lib/runtime/syscall_linux_llgo.go @@ -0,0 +1,25 @@ +//go:build linux + +package runtime + +import ( + "unsafe" + + clitesyscall "github.com/goplus/llgo/runtime/internal/clite/syscall" +) + +// LLGo may run alongside threads created by C libraries, which the Go runtime +// cannot enumerate. Match Go's cgo behavior and report AllThreadsSyscall as +// unsupported rather than changing process credentials on only one thread. +// +//go:linkname syscall_runtime_doAllThreadsSyscall syscall.runtime_doAllThreadsSyscall +//go:uintptrescapes +func syscall_runtime_doAllThreadsSyscall(_, _, _, _, _, _, _ uintptr) (r1, r2, err uintptr) { + return ^uintptr(0), ^uintptr(0), uintptr(clitesyscall.ENOTSUP) +} + +//go:linkname syscall_cgocaller syscall.cgocaller +//go:uintptrescapes +func syscall_cgocaller(_ unsafe.Pointer, _ ...uintptr) uintptr { + return uintptr(clitesyscall.ENOTSUP) +} 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() + } +}