From d887d9752863ceab6cc37ca7ca5a47da7f0f486a Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 23 Jul 2026 17:01:26 +0800 Subject: [PATCH 1/3] runtime: introduce G/M/P pthread backend --- cl/_testgo/goroutine/in.go | 10 +- cl/_testgo/selects/in.go | 100 +++++----- runtime/internal/runtime/g.go | 11 -- runtime/internal/runtime/g_global.go | 12 +- runtime/internal/runtime/g_pthread.go | 29 ++- .../runtime/{z_thread.go => os_pthread.go} | 50 +++-- runtime/internal/runtime/proc.go | 186 ++++++++++++++++++ runtime/internal/runtime/proc_atomic.go | 43 ++++ runtime/internal/runtime/proc_baremetal.go | 49 +++++ runtime/internal/runtime/runtime2.go | 76 +++++++ runtime/internal/runtime/z_default.go | 3 +- ssa/goroutine.go | 29 +-- ssa/goroutine_patch_test.go | 28 ++- 13 files changed, 480 insertions(+), 146 deletions(-) rename runtime/internal/runtime/{z_thread.go => os_pthread.go} (57%) create mode 100644 runtime/internal/runtime/proc.go create mode 100644 runtime/internal/runtime/proc_atomic.go create mode 100644 runtime/internal/runtime/proc_baremetal.go create mode 100644 runtime/internal/runtime/runtime2.go diff --git a/cl/_testgo/goroutine/in.go b/cl/_testgo/goroutine/in.go index 84d3a6c39c..e2312bfa1e 100644 --- a/cl/_testgo/goroutine/in.go +++ b/cl/_testgo/goroutine/in.go @@ -6,20 +6,14 @@ func main() { // CHECK: call ptr @"{{.*}}AllocZ"(i64 1) // CHECK: store i1 false, ptr %0, align 1 // CHECK: call ptr @"{{.*}}AllocRoot"(i64 16) - // CHECK: alloca %"{{.*}}pthread.Attr", align 8 - // CHECK: call i32 @"{{.*}}InitThreadAttr" - // CHECK: call i32 @"{{.*}}CreateThread"(ptr {{%[0-9]+}}, ptr {{%[0-9]+}}, ptr @"{{.*}}goroutine._llgo_routine$1", ptr %1) - // CHECK: call i32 @"{{.*}}DestroyThreadAttr" + // CHECK: call void @"{{.*}}NewProc"(ptr @"{{.*}}goroutine._llgo_routine$1", ptr %1, i64 0) done := false go println("hello") go func(s string) { // CHECK: call ptr @"{{.*}}AllocU"(i64 8) // CHECK: { ptr @"{{.*}}goroutine.main$1", ptr undef } // CHECK: call ptr @"{{.*}}AllocRoot"(i64 32) - // CHECK: alloca %"{{.*}}pthread.Attr", align 8 - // CHECK: call i32 @"{{.*}}InitThreadAttr" - // CHECK: call i32 @"{{.*}}CreateThread"(ptr {{%[0-9]+}}, ptr {{%[0-9]+}}, ptr @"{{.*}}goroutine._llgo_routine$2", ptr {{%[0-9]+}}) - // CHECK: call i32 @"{{.*}}DestroyThreadAttr" + // CHECK: call void @"{{.*}}NewProc"(ptr @"{{.*}}goroutine._llgo_routine$2", ptr {{%[0-9]+}}, i64 0) // CHECK: call void @"{{.*}}PrintString"(%"{{.*}}String" { ptr @2, i64 1 }) // CHECK: ret void // CHECK-LABEL: define void @"{{.*}}goroutine.main$1"(ptr %0, %"{{.*}}String" %1){{.*}} { diff --git a/cl/_testgo/selects/in.go b/cl/_testgo/selects/in.go index 5f69de4d6a..f1ba4145ca 100644 --- a/cl/_testgo/selects/in.go +++ b/cl/_testgo/selects/in.go @@ -74,59 +74,55 @@ func main() { // CHECK-NEXT: %12 = call ptr @"{{.*}}/runtime/internal/runtime.AllocRoot"(i64 16) // CHECK-NEXT: %13 = getelementptr inbounds { { ptr, ptr } }, ptr %12, i32 0, i32 0 // CHECK-NEXT: store { ptr, ptr } %11, ptr %13, align 8 -// CHECK-NEXT: %14 = alloca i8, i64 8, align 1 -// CHECK-NEXT: %15 = alloca %"{{.*}}/runtime/internal/clite/pthread.Attr", align 8 -// CHECK-NEXT: %16 = call i32 @"{{.*}}/runtime/internal/runtime.InitThreadAttr"(ptr %15) -// CHECK-NEXT: %17 = call i32 @"{{.*}}/runtime/internal/runtime.CreateThread"(ptr %14, ptr %15, ptr @"{{.*}}/cl/_testgo/selects._llgo_routine$1", ptr %12) -// CHECK-NEXT: %18 = call i32 @"{{.*}}/runtime/internal/runtime.DestroyThreadAttr"(ptr %15) -// CHECK-NEXT: %19 = load ptr, ptr %0, align 8 -// CHECK-NEXT: %20 = call ptr @llvm.stacksave.p0() -// CHECK-NEXT: %21 = alloca {}, align 8 -// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %21, i8 0, i64 0, i1 false) -// CHECK-NEXT: store {} zeroinitializer, ptr %21, align 1 -// CHECK-NEXT: %22 = call i1 @"{{.*}}/runtime/internal/runtime.ChanSend"(ptr %19, ptr %21, i64 0) -// CHECK-NEXT: call void @llvm.stackrestore.p0(ptr %20) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.NewProc"(ptr @"{{.*}}/cl/_testgo/selects._llgo_routine$1", ptr %12, i64 0) +// CHECK-NEXT: %14 = load ptr, ptr %0, align 8 +// CHECK-NEXT: %15 = call ptr @llvm.stacksave.p0() +// CHECK-NEXT: %16 = alloca {}, align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %16, i8 0, i64 0, i1 false) +// CHECK-NEXT: store {} zeroinitializer, ptr %16, align 1 +// CHECK-NEXT: %17 = call i1 @"{{.*}}/runtime/internal/runtime.ChanSend"(ptr %14, ptr %16, i64 0) +// CHECK-NEXT: call void @llvm.stackrestore.p0(ptr %15) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %23 = load ptr, ptr %2, align 8 -// CHECK-NEXT: %24 = call ptr @llvm.stacksave.p0() +// CHECK-NEXT: %18 = load ptr, ptr %2, align 8 +// CHECK-NEXT: %19 = call ptr @llvm.stacksave.p0() +// CHECK-NEXT: %20 = alloca {}, align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %20, i8 0, i64 0, i1 false) +// CHECK-NEXT: %21 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" undef, ptr %18, 0 +// CHECK-NEXT: %22 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %21, ptr %20, 1 +// CHECK-NEXT: %23 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %22, i32 0, 2 +// CHECK-NEXT: %24 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %23, i1 false, 3 // CHECK-NEXT: %25 = alloca {}, align 8 // CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %25, i8 0, i64 0, i1 false) -// CHECK-NEXT: %26 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" undef, ptr %23, 0 +// CHECK-NEXT: %26 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" undef, ptr %6, 0 // CHECK-NEXT: %27 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %26, ptr %25, 1 // CHECK-NEXT: %28 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %27, i32 0, 2 // CHECK-NEXT: %29 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %28, i1 false, 3 -// CHECK-NEXT: %30 = alloca {}, align 8 -// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %30, i8 0, i64 0, i1 false) -// CHECK-NEXT: %31 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" undef, ptr %6, 0 -// CHECK-NEXT: %32 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %31, ptr %30, 1 -// CHECK-NEXT: %33 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %32, i32 0, 2 -// CHECK-NEXT: %34 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %33, i1 false, 3 -// CHECK-NEXT: %35 = alloca i8, i64 48, align 1 -// CHECK-NEXT: %36 = getelementptr %"{{.*}}/runtime/internal/runtime.ChanOp", ptr %35, i64 0 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.ChanOp" %29, ptr %36, align 8 -// CHECK-NEXT: %37 = getelementptr %"{{.*}}/runtime/internal/runtime.ChanOp", ptr %35, i64 1 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.ChanOp" %34, ptr %37, align 8 -// CHECK-NEXT: %38 = insertvalue %"{{.*}}/runtime/internal/runtime.Slice" undef, ptr %35, 0 -// CHECK-NEXT: %39 = insertvalue %"{{.*}}/runtime/internal/runtime.Slice" %38, i64 2, 1 -// CHECK-NEXT: %40 = insertvalue %"{{.*}}/runtime/internal/runtime.Slice" %39, i64 2, 2 -// CHECK-NEXT: %41 = call { i64, i1 } @"{{.*}}/runtime/internal/runtime.Select"(%"{{.*}}/runtime/internal/runtime.Slice" %40) -// CHECK-NEXT: %42 = extractvalue { i64, i1 } %41, 0 -// CHECK-NEXT: %43 = extractvalue { i64, i1 } %41, 1 -// CHECK-NEXT: %44 = extractvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %29, 1 -// CHECK-NEXT: %45 = icmp eq ptr %44, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.AssertNilDeref"(i1 %45) -// CHECK-NEXT: %46 = extractvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %34, 1 -// CHECK-NEXT: %47 = icmp eq ptr %46, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.AssertNilDeref"(i1 %47) -// CHECK-NEXT: call void @llvm.stackrestore.p0(ptr %24) -// CHECK-NEXT: %48 = insertvalue { i64, i1, {}, {} } undef, i64 %42, 0 -// CHECK-NEXT: %49 = insertvalue { i64, i1, {}, {} } %48, i1 %43, 1 -// CHECK-NEXT: %50 = insertvalue { i64, i1, {}, {} } %49, {} zeroinitializer, 2 -// CHECK-NEXT: %51 = insertvalue { i64, i1, {}, {} } %50, {} zeroinitializer, 3 -// CHECK-NEXT: %52 = extractvalue { i64, i1, {}, {} } %51, 0 -// CHECK-NEXT: %53 = icmp eq i64 %52, 0 -// CHECK-NEXT: br i1 %53, label %_llgo_2, label %_llgo_3 +// CHECK-NEXT: %30 = alloca i8, i64 48, align 1 +// CHECK-NEXT: %31 = getelementptr %"{{.*}}/runtime/internal/runtime.ChanOp", ptr %30, i64 0 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.ChanOp" %24, ptr %31, align 8 +// CHECK-NEXT: %32 = getelementptr %"{{.*}}/runtime/internal/runtime.ChanOp", ptr %30, i64 1 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.ChanOp" %29, ptr %32, align 8 +// CHECK-NEXT: %33 = insertvalue %"{{.*}}/runtime/internal/runtime.Slice" undef, ptr %30, 0 +// CHECK-NEXT: %34 = insertvalue %"{{.*}}/runtime/internal/runtime.Slice" %33, i64 2, 1 +// CHECK-NEXT: %35 = insertvalue %"{{.*}}/runtime/internal/runtime.Slice" %34, i64 2, 2 +// CHECK-NEXT: %36 = call { i64, i1 } @"{{.*}}/runtime/internal/runtime.Select"(%"{{.*}}/runtime/internal/runtime.Slice" %35) +// CHECK-NEXT: %37 = extractvalue { i64, i1 } %36, 0 +// CHECK-NEXT: %38 = extractvalue { i64, i1 } %36, 1 +// CHECK-NEXT: %39 = extractvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %24, 1 +// CHECK-NEXT: %40 = icmp eq ptr %39, null +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.AssertNilDeref"(i1 %40) +// CHECK-NEXT: %41 = extractvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %29, 1 +// CHECK-NEXT: %42 = icmp eq ptr %41, null +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.AssertNilDeref"(i1 %42) +// CHECK-NEXT: call void @llvm.stackrestore.p0(ptr %19) +// CHECK-NEXT: %43 = insertvalue { i64, i1, {}, {} } undef, i64 %37, 0 +// CHECK-NEXT: %44 = insertvalue { i64, i1, {}, {} } %43, i1 %38, 1 +// CHECK-NEXT: %45 = insertvalue { i64, i1, {}, {} } %44, {} zeroinitializer, 2 +// CHECK-NEXT: %46 = insertvalue { i64, i1, {}, {} } %45, {} zeroinitializer, 3 +// CHECK-NEXT: %47 = extractvalue { i64, i1, {}, {} } %46, 0 +// CHECK-NEXT: %48 = icmp eq i64 %47, 0 +// CHECK-NEXT: br i1 %48, label %_llgo_2, label %_llgo_3 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_4, %_llgo_2 // CHECK-NEXT: ret void @@ -137,8 +133,8 @@ func main() { // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_0 -// CHECK-NEXT: %54 = icmp eq i64 %52, 1 -// CHECK-NEXT: br i1 %54, label %_llgo_4, label %_llgo_5 +// CHECK-NEXT: %49 = icmp eq i64 %47, 1 +// CHECK-NEXT: br i1 %49, label %_llgo_4, label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_3 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 4 }) @@ -146,10 +142,10 @@ func main() { // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_3 -// CHECK-NEXT: %55 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @3, i64 31 }, ptr %55, align 8 -// CHECK-NEXT: %56 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %55, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %56) +// CHECK-NEXT: %50 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @3, i64 31 }, ptr %50, align 8 +// CHECK-NEXT: %51 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %50, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %51) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/runtime/internal/runtime/g.go b/runtime/internal/runtime/g.go index dc249a33ba..b21fe3e418 100644 --- a/runtime/internal/runtime/g.go +++ b/runtime/internal/runtime/g.go @@ -16,17 +16,6 @@ package runtime -import "unsafe" - -// g holds the runtime state owned by one LLGo goroutine. -type g struct { - defer_ *Defer - panic_ unsafe.Pointer - goexit bool - isMain bool - paniconfault bool -} - // SetPanicOnFault updates the fault behavior requested by the current // goroutine and reports its previous value. func SetPanicOnFault(in bool) (out bool) { diff --git a/runtime/internal/runtime/g_global.go b/runtime/internal/runtime/g_global.go index 255733ff01..d373278a9c 100644 --- a/runtime/internal/runtime/g_global.go +++ b/runtime/internal/runtime/g_global.go @@ -18,8 +18,16 @@ package runtime -var globalG g +// Host builds and single-context targets share one process-wide current G. +var currentG *g func getg() *g { - return &globalG + if currentG == nil { + currentG = initRuntimeContext(new(runtimeContext), nil, _Grunning) + } + return currentG +} + +func setg(gp *g) { + currentG = gp } diff --git a/runtime/internal/runtime/g_pthread.go b/runtime/internal/runtime/g_pthread.go index 048ecc1dc6..83aebf0127 100644 --- a/runtime/internal/runtime/g_pthread.go +++ b/runtime/internal/runtime/g_pthread.go @@ -40,17 +40,24 @@ 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") + gp := initRuntimeContext(allocRuntimeContext(), nil, _Grunning) + if ret := setgRaw(gp); ret != 0 { + destroyG(c.Pointer(unsafe.Pointer(gp))) + c.Fprintf(c.Stderr, c.Str("runtime: pthread_setspecific failed (errno=%d)\n"), ret) + panic("runtime: failed to install g") } - c.Memset(ptr, 0, unsafe.Sizeof(g{})) - if ret := gKey.Set(ptr); ret != 0 { - FreeRoot(ptr) + return gp +} + +func setg(gp *g) { + if ret := setgRaw(gp); ret != 0 { c.Fprintf(c.Stderr, c.Str("runtime: pthread_setspecific failed (errno=%d)\n"), ret) panic("runtime: failed to install g") } - return (*g)(ptr) +} + +func setgRaw(gp *g) c.Int { + return gKey.Set(c.Pointer(unsafe.Pointer(gp))) } func destroyG(ptr c.Pointer) { @@ -61,6 +68,10 @@ func destroyG(ptr c.Pointer) { if gp.panic_ != nil { c.Free(gp.panic_) } - *gp = g{} - FreeRoot(ptr) + ctx := gp.context + if ctx != nil && ctx.root != nil { + root := ctx.root + ctx.root = nil + FreeRoot(root) + } } diff --git a/runtime/internal/runtime/z_thread.go b/runtime/internal/runtime/os_pthread.go similarity index 57% rename from runtime/internal/runtime/z_thread.go rename to runtime/internal/runtime/os_pthread.go index d53ac121c8..f972a4510b 100644 --- a/runtime/internal/runtime/z_thread.go +++ b/runtime/internal/runtime/os_pthread.go @@ -17,38 +17,52 @@ package runtime import ( - _ "unsafe" + "unsafe" c "github.com/goplus/llgo/runtime/internal/clite" "github.com/goplus/llgo/runtime/internal/clite/pthread" ) -func InitThreadAttr(attr *pthread.Attr) c.Int { - if ret := attr.Init(); ret != 0 { - return ret - } - if ret := attr.SetDetached(pthread.CreateDetached); ret != 0 { - attr.Destroy() - return ret +// mOS is the pthread-specific part of an M. +type mOS struct { + thread pthread.Thread +} + +// newosproc provides the current host-thread backend for newm. +func newosproc(mp *m, stackSize uintptr) int { + var attr pthread.Attr + if ret := initThreadAttr(&attr, stackSize); ret != 0 { + return int(ret) } - return 0 + ret := pthread.Create( + &mp.os.thread, + &attr, + pthread.RoutineFunc(mstart), + c.Pointer(unsafe.Pointer(mp)), + ) + attr.Destroy() + return int(ret) } -func InitThreadAttrWithStack(attr *pthread.Attr, stackSize uintptr) c.Int { - if ret := InitThreadAttr(attr); ret != 0 { +func initThreadAttr(attr *pthread.Attr, stackSize uintptr) c.Int { + if ret := attr.Init(); ret != 0 { return ret } - if ret := attr.SetStackSize(stackSize); ret != 0 { + if ret := attr.SetDetached(pthread.CreateDetached); ret != 0 { attr.Destroy() return ret } + if stackSize != 0 { + if ret := attr.SetStackSize(stackSize); ret != 0 { + attr.Destroy() + return ret + } + } return 0 } -func DestroyThreadAttr(attr *pthread.Attr) c.Int { - return attr.Destroy() -} - -func CreateThread(th *pthread.Thread, attr *pthread.Attr, routine pthread.RoutineFunc, arg c.Pointer) c.Int { - return pthread.Create(th, attr, routine, arg) +func exitCurrentM() { + mp := getg().m + mexit(mp) + pthread.Exit(nil) } diff --git a/runtime/internal/runtime/proc.go b/runtime/internal/runtime/proc.go new file mode 100644 index 0000000000..14b6a3ed58 --- /dev/null +++ b/runtime/internal/runtime/proc.go @@ -0,0 +1,186 @@ +/* + * 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" +) + +// goroutineFunc is the target-independent entry ABI between compiler-generated +// goroutine wrappers and the runtime scheduler. +// +//llgo:type C +type goroutineFunc func(unsafe.Pointer) unsafe.Pointer + +// runtimeContext keeps the G, M, and P for the current 1:1 backend in one +// allocation. Keeping their ownership together makes mexit deterministic while +// leaving the individual objects and links compatible with a later M:N backend. +type runtimeContext struct { + g g + m m + p p + + // root is non-nil for contexts passed through a host-thread API. Such + // contexts must remain visible to the collector until mexit. + root unsafe.Pointer +} + +var sched struct { + goidgen uint64 + midgen int64 + pidgen int32 +} + +// NewProc creates a new G running fn. +// +// The compiler turns a go statement into a call to NewProc. Unlike the old +// lowering, this ABI contains no pthread types: the selected runtime backend +// decides how to provide an M and execute the G. +func NewProc(fn goroutineFunc, arg unsafe.Pointer, stackSize uintptr) { + gp := newproc1(fn, arg, getg()) + if errno := newm(gp.m, stackSize); errno != 0 { + ctx := gp.context + FreeRoot(arg) + FreeRoot(ctx.root) + panic("runtime: failed to create new OS thread") + } +} + +// newproc1 creates a runnable G and its initial M/P ownership. The pthread +// backend starts that G immediately; a future scheduler can enqueue the same G +// without changing the compiler ABI. +func newproc1(fn goroutineFunc, arg unsafe.Pointer, callergp *g) *g { + if fn == nil { + panic("go of nil func value") + } + + ctx := allocRuntimeContext() + gp := initRuntimeContext(ctx, callergp, _Grunnable) + gp.startfn = fn + gp.startarg = arg + return gp +} + +func allocRuntimeContext() *runtimeContext { + size := unsafe.Sizeof(runtimeContext{}) + root := AllocRoot(size) + if root == nil { + panic("runtime: failed to allocate goroutine context") + } + c.Memset(root, 0, size) + ctx := (*runtimeContext)(root) + ctx.root = root + return ctx +} + +// newm starts the platform execution resource for mp. +func newm(mp *m, stackSize uintptr) int { + return newosproc(mp, stackSize) +} + +// mstart is the first LLGo runtime function executed on a new M. +func mstart(arg unsafe.Pointer) unsafe.Pointer { + mp := (*m)(arg) + if mp == nil || mp.curg == nil || mp.p == nil { + fatal("runtime: invalid mstart context") + return nil + } + gp := mp.curg + pp := mp.p + + setg(gp) + casgstatus(gp, _Grunnable, _Grunning) + pp.status = _Prunning + + fn, arg := gp.startfn, gp.startarg + gp.startfn = nil + gp.startarg = nil + ret := fn(arg) + mexit(mp) + return ret +} + +// mexit tears down the current 1:1 G/M/P context. It does not terminate the +// host thread so both a returning start routine and runtime.Goexit can share +// the same ownership cleanup. +func mexit(mp *m) { + if mp == nil || mp.curg == nil || mp.p == nil { + fatal("runtime: invalid mexit context") + return + } + gp := mp.curg + pp := mp.p + ctx := gp.context + root := ctx.root + + casgstatus(gp, _Grunning, _Gdead) + pp.status = _Pdead + + pp.m = nil + mp.p = nil + mp.curg = nil + gp.m = nil + + setg(nil) + if root != nil { + ctx.root = nil + FreeRoot(root) + } +} + +func initRuntimeContext(ctx *runtimeContext, callergp *g, status uint32) *g { + gp := &ctx.g + mp := &ctx.m + pp := &ctx.p + + gp.m = mp + gp.atomicstatus = status + gp.goid = nextGoid(gp) + if callergp != nil { + gp.parentGoid = callergp.goid + } + gp.context = ctx + + mp.curg = gp + mp.p = pp + mp.id = nextMid(mp) + + pp.id = nextPid(pp) + pp.status = _Pidle + if status == _Grunning { + pp.status = _Prunning + } + pp.m = mp + return gp +} + +// GMPForTesting reports the current runtime ownership graph. It is kept +// internal to the compiler runtime and linked only by LLGo execution tests. +func GMPForTesting() (goid, parentGoid uint64, mid int64, pid int32, gstatus, pstatus uint32, linked bool) { + gp := getg() + if gp == nil || gp.m == nil || gp.m.p == nil { + return + } + mp := gp.m + pp := mp.p + ctx := gp.context + return gp.goid, gp.parentGoid, mp.id, pp.id, readgstatus(gp), pp.status, + mp.curg == gp && pp.m == mp && ctx != nil && + &ctx.g == gp && &ctx.m == mp && &ctx.p == pp +} diff --git a/runtime/internal/runtime/proc_atomic.go b/runtime/internal/runtime/proc_atomic.go new file mode 100644 index 0000000000..45e802bd65 --- /dev/null +++ b/runtime/internal/runtime/proc_atomic.go @@ -0,0 +1,43 @@ +//go:build !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 "github.com/goplus/llgo/runtime/internal/clite/sync/atomic" + +func nextGoid(gp *g) uint64 { + return atomic.Add(&sched.goidgen, uint64(1)) +} + +func nextMid(mp *m) int64 { + return atomic.Add(&sched.midgen, int64(1)) +} + +func nextPid(pp *p) int32 { + return atomic.Add(&sched.pidgen, int32(1)) - 1 +} + +func readgstatus(gp *g) uint32 { + return atomic.Load(&gp.atomicstatus) +} + +func casgstatus(gp *g, oldval, newval uint32) { + if _, ok := atomic.CompareAndExchange(&gp.atomicstatus, oldval, newval); !ok { + fatal("runtime: invalid goroutine status transition") + } +} diff --git a/runtime/internal/runtime/proc_baremetal.go b/runtime/internal/runtime/proc_baremetal.go new file mode 100644 index 0000000000..fdf0268225 --- /dev/null +++ b/runtime/internal/runtime/proc_baremetal.go @@ -0,0 +1,49 @@ +//go:build 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" + +// Bare-metal targets do not necessarily provide the compiler-rt __atomic +// helpers. Object addresses are sufficient as live G/M/P identifiers for the +// current task backend and avoid adding libatomic solely for diagnostics. +func nextGoid(gp *g) uint64 { + return uint64(uintptr(unsafe.Pointer(gp))) +} + +func nextMid(mp *m) int64 { + return int64(uintptr(unsafe.Pointer(mp))) +} + +func nextPid(pp *p) int32 { + return int32((uintptr(unsafe.Pointer(pp)) >> 2) & 0x7fffffff) +} + +// Each bare-metal G owns its status transitions in the current backend. +func readgstatus(gp *g) uint32 { + return gp.atomicstatus +} + +func casgstatus(gp *g, oldval, newval uint32) { + if gp.atomicstatus != oldval { + fatal("runtime: invalid goroutine status transition") + return + } + gp.atomicstatus = newval +} diff --git a/runtime/internal/runtime/runtime2.go b/runtime/internal/runtime/runtime2.go new file mode 100644 index 0000000000..8e0fe9dc7a --- /dev/null +++ b/runtime/internal/runtime/runtime2.go @@ -0,0 +1,76 @@ +/* + * 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" + +// These G and P states intentionally keep the values used by the Go runtime. +// Only states reachable by the current 1:1 backend are defined here. +const ( + _Grunnable = 1 + _Grunning = 2 + _Gdead = 6 +) + +const ( + _Pidle = 0 + _Prunning = 1 + _Pdead = 4 +) + +// g holds state owned by one LLGo goroutine. +// +// The current pthread backend gives every G its own M and P. Fields that only +// make sense once LLGo can suspend and resume a G (saved registers, wait state, +// and stack roots) belong here when those facilities are added. +type g struct { + defer_ *Defer + panic_ unsafe.Pointer + m *m + + atomicstatus uint32 + goid uint64 + parentGoid uint64 + + startfn goroutineFunc + startarg unsafe.Pointer + + context *runtimeContext + + goexit bool + isMain bool + paniconfault bool +} + +// m represents the host execution resource running Go code. The platform +// thread handle is deliberately confined to mOS so other backends do not leak +// pthread types into the scheduler core. +type m struct { + curg *g + p *p + id int64 + os mOS +} + +// p represents the scheduling resources attached to an M. The pthread backend +// currently binds one P to one M; a later M:N scheduler can retain this object +// while replacing that fixed binding with a P pool and run queues. +type p struct { + id int32 + status uint32 + m *m +} diff --git a/runtime/internal/runtime/z_default.go b/runtime/internal/runtime/z_default.go index 7f0b7601ba..6ab61c0d66 100644 --- a/runtime/internal/runtime/z_default.go +++ b/runtime/internal/runtime/z_default.go @@ -5,7 +5,6 @@ package runtime import ( c "github.com/goplus/llgo/runtime/internal/clite" "github.com/goplus/llgo/runtime/internal/clite/debug" - "github.com/goplus/llgo/runtime/internal/clite/pthread" ) var ( @@ -41,6 +40,6 @@ func Rethrow(link *Defer) { fatal("no goroutines (main called runtime.Goexit) - deadlock!") c.Exit(2) } - pthread.Exit(nil) + exitCurrentM() } } diff --git a/ssa/goroutine.go b/ssa/goroutine.go index 1c9f5708c3..278f4e7ec8 100644 --- a/ssa/goroutine.go +++ b/ssa/goroutine.go @@ -36,22 +36,6 @@ func (p Program) tyRoutine() *types.Signature { return p.routineTy } -func (b Builder) pthreadCreate(pp, attr, routine, arg Expr) Expr { - fn := b.Pkg.rtFunc("CreateThread") - return b.Call(fn, pp, attr, routine, arg) -} - -func (b Builder) pthreadAttr() Expr { - prog := b.Prog - fn := b.Pkg.rtFunc("CreateThread") - params := fn.raw.Type.(*types.Signature).Params() - attr := prog.rawType(params.At(1).Type()) - elem := prog.rawType(attr.RawType().(*types.Pointer).Elem()) - return b.AllocaT(elem) -} - -// ----------------------------------------------------------------------------- - // The Go instruction creates a new goroutine and calls the specified // function within it. // @@ -88,17 +72,8 @@ func (b Builder) Go(fn Expr, buildCall func(Builder, Expr, ...Expr) Expr, args . dataPtr := b.Call(pkg.rtFunc("AllocRoot"), prog.IntVal(prog.SizeOf(t), prog.Uintptr())).impl aggregateInit(b.impl, dataPtr, t.ll, flds...) data := Expr{dataPtr, voidPtr} - size := prog.SizeOf(voidPtr) - pthd := b.Alloca(prog.IntVal(uint64(size), prog.Uintptr())) - attr := b.pthreadAttr() - if prog.pthreadStackSize > 0 { - stackSize := prog.IntVal(prog.pthreadStackSize, prog.Uintptr()) - b.Call(pkg.rtFunc("InitThreadAttrWithStack"), attr, stackSize) - } else { - b.Call(pkg.rtFunc("InitThreadAttr"), attr) - } - b.pthreadCreate(pthd, attr, pkg.routine(t, fn, buildCall, len(args)), data) - b.Call(pkg.rtFunc("DestroyThreadAttr"), attr) + stackSize := prog.IntVal(prog.pthreadStackSize, prog.Uintptr()) + b.Call(pkg.rtFunc("NewProc"), pkg.routine(t, fn, buildCall, len(args)), data, stackSize) } func (p Package) routineName() string { diff --git a/ssa/goroutine_patch_test.go b/ssa/goroutine_patch_test.go index 16af8643c9..b6b245cd45 100644 --- a/ssa/goroutine_patch_test.go +++ b/ssa/goroutine_patch_test.go @@ -82,7 +82,7 @@ func TestGoPanicRoutineDoesNotReturnAfterUnreachable(t *testing.T) { } } -func TestGoUsesPthreadAttrForDetachedStack(t *testing.T) { +func TestGoPassesConfiguredStackSizeToRuntime(t *testing.T) { prog := ssatest.NewProgram(t, nil) prog.SetPthreadStackSize(32 << 20) pkg := prog.NewPackage("bar", "foo/bar") @@ -95,24 +95,18 @@ func TestGoUsesPthreadAttrForDetachedStack(t *testing.T) { ob.Return() ir := pkg.String() - initAttr := strings.Index(ir, `"github.com/goplus/llgo/runtime/internal/runtime.InitThreadAttrWithStack"`) - createThread := strings.Index(ir, `"github.com/goplus/llgo/runtime/internal/runtime.CreateThread"`) - destroyAttr := strings.Index(ir, `"github.com/goplus/llgo/runtime/internal/runtime.DestroyThreadAttr"`) - if initAttr < 0 || createThread < 0 || destroyAttr < 0 { - t.Fatalf("goroutine should initialize, use, and destroy pthread attrs:\n%s", ir) - } - if !(initAttr < createThread && createThread < destroyAttr) { - t.Fatalf("pthread attr calls should wrap CreateThread:\n%s", ir) + if !strings.Contains(ir, `"github.com/goplus/llgo/runtime/internal/runtime.NewProc"`) { + t.Fatalf("goroutine should delegate startup to the runtime:\n%s", ir) } if !strings.Contains(ir, "33554432") { - t.Fatalf("goroutine should pass configured pthread stack size:\n%s", ir) + t.Fatalf("goroutine should pass the configured stack size:\n%s", ir) } - if strings.Contains(ir, "llgo_pthread_create_detached") { - t.Fatalf("goroutine should not call detached C wrapper:\n%s", ir) + if strings.Contains(ir, "pthread") { + t.Fatalf("compiler IR should not depend on the pthread backend:\n%s", ir) } } -func TestGoUsesPthreadAttrWithoutStackByDefault(t *testing.T) { +func TestGoPassesZeroStackSizeToRuntimeByDefault(t *testing.T) { prog := ssatest.NewProgram(t, nil) pkg := prog.NewPackage("bar", "foo/bar") @@ -124,10 +118,10 @@ func TestGoUsesPthreadAttrWithoutStackByDefault(t *testing.T) { ob.Return() ir := pkg.String() - if !strings.Contains(ir, `"github.com/goplus/llgo/runtime/internal/runtime.InitThreadAttr"`) { - t.Fatalf("goroutine should initialize pthread attrs:\n%s", ir) + if !strings.Contains(ir, `"github.com/goplus/llgo/runtime/internal/runtime.NewProc"`) { + t.Fatalf("goroutine should delegate startup to the runtime:\n%s", ir) } - if strings.Contains(ir, `"github.com/goplus/llgo/runtime/internal/runtime.InitThreadAttrWithStack"`) { - t.Fatalf("default goroutine should not request custom pthread stack size:\n%s", ir) + if strings.Contains(ir, "pthread") { + t.Fatalf("compiler IR should not depend on the pthread backend:\n%s", ir) } } From 433ad8b5267fea053588c6b1ec90145f3e1afc4f Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 23 Jul 2026 17:01:27 +0800 Subject: [PATCH 2/3] test(runtime): verify G/M/P ownership --- test/llgoext/runtime_g_test.go | 88 ++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/test/llgoext/runtime_g_test.go b/test/llgoext/runtime_g_test.go index 52789908d7..03fe1b2bc5 100644 --- a/test/llgoext/runtime_g_test.go +++ b/test/llgoext/runtime_g_test.go @@ -76,6 +76,94 @@ func TestRuntimeGetGIsolation(t *testing.T) { } } +//go:linkname runtimeGMPForTesting github.com/goplus/llgo/runtime/internal/runtime.GMPForTesting +func runtimeGMPForTesting() (goid, parentGoid uint64, mid int64, pid int32, gstatus, pstatus uint32, linked bool) + +const ( + runtimeGRunning = 2 + runtimePRunning = 1 +) + +type runtimeGMPState struct { + goid uint64 + parentGoid uint64 + mid int64 + pid int32 + gstatus uint32 + pstatus uint32 + linked bool +} + +func currentRuntimeGMPState() runtimeGMPState { + goid, parentGoid, mid, pid, gstatus, pstatus, linked := runtimeGMPForTesting() + return runtimeGMPState{ + goid: goid, + parentGoid: parentGoid, + mid: mid, + pid: pid, + gstatus: gstatus, + pstatus: pstatus, + linked: linked, + } +} + +func checkRunningRuntimeGMP(t *testing.T, state runtimeGMPState) { + t.Helper() + if state.goid == 0 { + t.Fatal("current G has no ID") + } + if state.mid == 0 { + t.Fatal("current G has no M") + } + if state.pid < 0 { + t.Fatalf("current G has invalid P id %d", state.pid) + } + if state.gstatus != runtimeGRunning { + t.Fatalf("G status = %d, want running (%d)", state.gstatus, runtimeGRunning) + } + if state.pstatus != runtimePRunning { + t.Fatalf("P status = %d, want running (%d)", state.pstatus, runtimePRunning) + } + if !state.linked { + t.Fatal("G, M, and P links are inconsistent") + } +} + +func TestRuntimeGMPLinks(t *testing.T) { + parent := currentRuntimeGMPState() + checkRunningRuntimeGMP(t, parent) + + results := make(chan runtimeGMPState, 2) + for i := 0; i < cap(results); i++ { + go func() { + results <- currentRuntimeGMPState() + }() + } + + seenG := map[uint64]bool{parent.goid: true} + seenM := map[int64]bool{parent.mid: true} + seenP := map[int32]bool{parent.pid: true} + for i := 0; i < cap(results); i++ { + state := <-results + checkRunningRuntimeGMP(t, state) + if state.parentGoid != parent.goid { + t.Fatalf("G %d parent = %d, want %d", state.goid, state.parentGoid, parent.goid) + } + if seenG[state.goid] { + t.Fatalf("duplicate G id %d", state.goid) + } + if seenM[state.mid] { + t.Fatalf("duplicate M id %d", state.mid) + } + if seenP[state.pid] { + t.Fatalf("duplicate P id %d", state.pid) + } + seenG[state.goid] = true + seenM[state.mid] = true + seenP[state.pid] = true + } +} + type runtimeDeferProbeResult struct { before unsafe.Pointer inside unsafe.Pointer From a0c47820a7d67df6984fe725c1153ceb8d6a6227 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 23 Jul 2026 18:34:05 +0800 Subject: [PATCH 3/3] runtime: address G/M/P backend review feedback --- runtime/internal/runtime/os_pthread.go | 10 ++++++---- runtime/internal/runtime/proc.go | 11 ++++++----- runtime/internal/runtime/proc_atomic.go | 8 ++++++++ runtime/internal/runtime/proc_baremetal.go | 8 ++++++++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/runtime/internal/runtime/os_pthread.go b/runtime/internal/runtime/os_pthread.go index f972a4510b..4a7447fda4 100644 --- a/runtime/internal/runtime/os_pthread.go +++ b/runtime/internal/runtime/os_pthread.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 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. @@ -40,7 +40,9 @@ func newosproc(mp *m, stackSize uintptr) int { pthread.RoutineFunc(mstart), c.Pointer(unsafe.Pointer(mp)), ) - attr.Destroy() + // Once Create succeeds, mp belongs to the detached thread. A destroy + // failure cannot be reported as creation failure without freeing live data. + _ = attr.Destroy() return int(ret) } @@ -49,12 +51,12 @@ func initThreadAttr(attr *pthread.Attr, stackSize uintptr) c.Int { return ret } if ret := attr.SetDetached(pthread.CreateDetached); ret != 0 { - attr.Destroy() + _ = attr.Destroy() return ret } if stackSize != 0 { if ret := attr.SetStackSize(stackSize); ret != 0 { - attr.Destroy() + _ = attr.Destroy() return ret } } diff --git a/runtime/internal/runtime/proc.go b/runtime/internal/runtime/proc.go index 14b6a3ed58..b9e35ef954 100644 --- a/runtime/internal/runtime/proc.go +++ b/runtime/internal/runtime/proc.go @@ -106,7 +106,7 @@ func mstart(arg unsafe.Pointer) unsafe.Pointer { setg(gp) casgstatus(gp, _Grunnable, _Grunning) - pp.status = _Prunning + setpstatus(pp, _Prunning) fn, arg := gp.startfn, gp.startarg gp.startfn = nil @@ -130,7 +130,7 @@ func mexit(mp *m) { root := ctx.root casgstatus(gp, _Grunning, _Gdead) - pp.status = _Pdead + setpstatus(pp, _Pdead) pp.m = nil mp.p = nil @@ -162,10 +162,11 @@ func initRuntimeContext(ctx *runtimeContext, callergp *g, status uint32) *g { mp.id = nextMid(mp) pp.id = nextPid(pp) - pp.status = _Pidle + pstatus := uint32(_Pidle) if status == _Grunning { - pp.status = _Prunning + pstatus = _Prunning } + setpstatus(pp, pstatus) pp.m = mp return gp } @@ -180,7 +181,7 @@ func GMPForTesting() (goid, parentGoid uint64, mid int64, pid int32, gstatus, ps mp := gp.m pp := mp.p ctx := gp.context - return gp.goid, gp.parentGoid, mp.id, pp.id, readgstatus(gp), pp.status, + return gp.goid, gp.parentGoid, mp.id, pp.id, readgstatus(gp), readpstatus(pp), mp.curg == gp && pp.m == mp && ctx != nil && &ctx.g == gp && &ctx.m == mp && &ctx.p == pp } diff --git a/runtime/internal/runtime/proc_atomic.go b/runtime/internal/runtime/proc_atomic.go index 45e802bd65..eaec33b38f 100644 --- a/runtime/internal/runtime/proc_atomic.go +++ b/runtime/internal/runtime/proc_atomic.go @@ -41,3 +41,11 @@ func casgstatus(gp *g, oldval, newval uint32) { fatal("runtime: invalid goroutine status transition") } } + +func readpstatus(pp *p) uint32 { + return atomic.Load(&pp.status) +} + +func setpstatus(pp *p, status uint32) { + atomic.Store(&pp.status, status) +} diff --git a/runtime/internal/runtime/proc_baremetal.go b/runtime/internal/runtime/proc_baremetal.go index fdf0268225..e203ec44f1 100644 --- a/runtime/internal/runtime/proc_baremetal.go +++ b/runtime/internal/runtime/proc_baremetal.go @@ -47,3 +47,11 @@ func casgstatus(gp *g, oldval, newval uint32) { } gp.atomicstatus = newval } + +func readpstatus(pp *p) uint32 { + return pp.status +} + +func setpstatus(pp *p, status uint32) { + pp.status = status +}