From 7872f2c698c5e20a1b5b08068bf1a652122be17e Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 22 May 2026 17:26:10 +0800 Subject: [PATCH 1/3] fix stack object finalizer liveness --- cl/compile.go | 647 ++++++++++++++++++- cl/instr.go | 6 + runtime/internal/clite/bdwgc/bdwgc.go | 3 + runtime/internal/lib/runtime/_wrap/runtime.c | 64 ++ runtime/internal/lib/runtime/mfinal.go | 168 ++++- runtime/internal/lib/runtime/runtime.go | 16 + runtime/internal/lib/runtime/runtime_gc.go | 4 + ssa/memory.go | 28 + test/go/finalizer_test.go | 216 +++++++ test/goroot/xfail.yaml | 16 - 10 files changed, 1133 insertions(+), 35 deletions(-) create mode 100644 test/go/finalizer_test.go diff --git a/cl/compile.go b/cl/compile.go index 5b6d7c9fbe..5d5eb6015a 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -152,23 +152,29 @@ type pkgInfo struct { type none = struct{} type context struct { - prog llssa.Program - pkg llssa.Package - fn llssa.Function - goFn *ssa.Function - fset *token.FileSet - goProg *ssa.Program - goTyps *types.Package - goPkg *ssa.Package - pyMod string - skips map[string]none - loaded map[*types.Package]*pkgInfo // loaded packages - bvals map[ssa.Value]llssa.Expr // block values - vargs map[*ssa.Alloc][]llssa.Expr // varargs - funcs map[*ssa.Function]llssa.Function - stackDefers map[*ssa.Function]bool - anonDefers map[*ssa.Function]bool - paramDIVars map[*types.Var]llssa.DIVar + prog llssa.Program + pkg llssa.Package + fn llssa.Function + goFn *ssa.Function + fset *token.FileSet + goProg *ssa.Program + goTyps *types.Package + goPkg *ssa.Package + pyMod string + skips map[string]none + loaded map[*types.Package]*pkgInfo // loaded packages + bvals map[ssa.Value]llssa.Expr // block values + vargs map[*ssa.Alloc][]llssa.Expr // varargs + funcs map[*ssa.Function]llssa.Function + stackDefers map[*ssa.Function]bool + anonDefers map[*ssa.Function]bool + stackClears map[ssa.Instruction][]*ssa.Alloc + entryClears map[*ssa.BasicBlock][]*ssa.Alloc + loadClears map[ssa.Instruction]bool + callClobbers map[ssa.Instruction]bool + paramClobbers map[ssa.Instruction]bool + paramScans map[ssa.Instruction][]*ssa.Parameter + paramDIVars map[*types.Var]llssa.DIVar patches Patches blkInfos []blocks.Info @@ -455,6 +461,21 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun b.DebugFunction(fn, pos, bodyPos) } p.bvals = make(map[ssa.Value]llssa.Expr) + if p.enableConservativeLivenessClears(f) { + p.stackClears = p.collectStackClearPlans(f) + p.entryClears = p.collectEntryClearPlans(f) + p.loadClears = make(map[ssa.Instruction]bool) + p.callClobbers = p.collectCallClobberPlans(f) + p.paramClobbers = p.collectParamClobberPlans(f) + p.paramScans = p.collectParamScanPlans(f) + } else { + p.stackClears = nil + p.entryClears = nil + p.loadClears = nil + p.callClobbers = nil + p.paramClobbers = nil + p.paramScans = nil + } off := make([]int, len(f.Blocks)) if isCgo { p.cgoArgs = make([]llssa.Expr, len(f.Params)) @@ -559,6 +580,7 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do var instrs = block.Instrs[n:] var ret = fn.Block(block.Index) b.SetBlock(ret) + p.clearEntryAllocs(b, block) if block.Index == 0 && enableCallTracing && !strings.HasPrefix(fn.Name(), "github.com/goplus/llgo/runtime/internal/runtime.Print") { b.Printf("call " + fn.Name() + "\n\x00") } @@ -593,6 +615,9 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do fnOld := pkg.NewFunc(initFnNameOld, llssa.NoArgsNoRet, llssa.InC) b.Call(fnOld.Expr) } + if !(isCgoCfunc || isCgoC2 || isCgoCmacro) && p.shouldSkipLateSetFinalizerValue(instr) { + continue + } if isCgoCfunc || isCgoC2 || isCgoCmacro { switch instr := instr.(type) { case *ssa.Alloc: @@ -631,6 +656,17 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do } else { p.compileInstr(b, instr) } + if isTerminatingInstruction(instr) { + continue + } + p.clearDeadAllocs(b, instr) + if p.callClobbers[instr] { + p.clobberPointerRegs(b) + } + p.scanParamPointers(b, instr) + if p.paramClobbers[instr] { + p.clobberPointerRegs(b) + } } // is cgo cfunc but not return yet, some funcs has multiple blocks if (isCgoCfunc || isCgoC2 || isCgoCmacro) && !cgoReturned { @@ -798,6 +834,575 @@ func isAllocVargs(ctx *context, v *ssa.Alloc) bool { return false } +func (p *context) enableConservativeLivenessClears(fn *ssa.Function) bool { + if fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + path := fn.Pkg.Pkg.Path() + if path == "command-line-arguments" { + return true + } + return false +} + +func hasConservativeGCPointers(t types.Type, seen map[types.Type]bool) bool { + if t == nil { + return false + } + t = types.Unalias(t) + if seen[t] { + return false + } + seen[t] = true + switch t := t.Underlying().(type) { + case *types.Pointer, *types.Slice, *types.Map, *types.Chan, *types.Signature, *types.Interface: + return true + case *types.Basic: + return t.Kind() == types.String || t.Kind() == types.UnsafePointer + case *types.Array: + return hasConservativeGCPointers(t.Elem(), seen) + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + if hasConservativeGCPointers(t.Field(i).Type(), seen) { + return true + } + } + } + return false +} + +func (p *context) shouldClearAlloc(v *ssa.Alloc) bool { + if v == nil || v.Comment == "varargs" || v.Comment == "makeslice" { + return false + } + ptr, ok := v.Type().Underlying().(*types.Pointer) + return ok && hasConservativeGCPointers(ptr.Elem(), map[types.Type]bool{}) +} + +func blockCanReach(from, to *ssa.BasicBlock, seen map[*ssa.BasicBlock]bool) bool { + if from == nil || to == nil { + return false + } + if from == to { + return true + } + if seen[from] { + return false + } + seen[from] = true + for _, succ := range from.Succs { + if blockCanReach(succ, to, seen) { + return true + } + } + return false +} + +func refBlock(ref ssa.Instruction) *ssa.BasicBlock { + if ref == nil { + return nil + } + return ref.Block() +} + +func instructionUsesValue(instr ssa.Instruction, v ssa.Value) bool { + if instr == nil || v == nil { + return false + } + for _, operand := range instr.Operands(nil) { + if operand != nil && *operand == v { + return true + } + } + return false +} + +func isCallLikeInstruction(instr ssa.Instruction) bool { + switch instr.(type) { + case *ssa.Call, *ssa.Defer, *ssa.Go: + return true + } + return false +} + +func isTerminatingInstruction(instr ssa.Instruction) bool { + switch instr.(type) { + case *ssa.Jump, *ssa.Return, *ssa.If, *ssa.Panic: + return true + } + return false +} + +func (p *context) isRuntimeSetFinalizerCall(call *ssa.CallCommon) bool { + if call == nil { + return false + } + fn, ok := call.Value.(*ssa.Function) + if !ok || fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + return fn.Name() == "SetFinalizer" && + fn.Pkg.Pkg.Path() == "github.com/goplus/llgo/runtime/internal/lib/runtime" +} + +func (p *context) isOnlyRuntimeSetFinalizerArg(v ssa.Value) bool { + refs := v.Referrers() + if refs == nil || len(*refs) != 1 { + return false + } + call, ok := (*refs)[0].(*ssa.Call) + return ok && p.isRuntimeSetFinalizerCall(&call.Call) +} + +func (p *context) shouldSkipLateSetFinalizerValue(instr ssa.Instruction) bool { + switch instr := instr.(type) { + case *ssa.MakeInterface: + return p.isOnlyRuntimeSetFinalizerArg(instr) + case *ssa.UnOp: + if instr.Op != token.MUL { + return false + } + refs := instr.Referrers() + if refs == nil || len(*refs) != 1 { + return false + } + mi, ok := (*refs)[0].(*ssa.MakeInterface) + return ok && p.isOnlyRuntimeSetFinalizerArg(mi) + } + return false +} + +func (p *context) collectValueUseBlocks(v ssa.Value, blocks map[*ssa.BasicBlock]bool, seen map[ssa.Value]bool, followPhi bool) bool { + if v == nil || seen[v] { + return true + } + seen[v] = true + refs := v.Referrers() + if refs == nil { + return true + } + for _, ref := range *refs { + switch ref := ref.(type) { + case *ssa.DebugRef: + continue + case *ssa.FieldAddr, *ssa.IndexAddr, *ssa.ChangeType, *ssa.Convert, *ssa.MakeInterface: + refVal, ok := ref.(ssa.Value) + if !ok { + return false + } + if !p.collectValueUseBlocks(refVal, blocks, seen, followPhi) { + return false + } + case *ssa.UnOp: + if ref.Op != token.MUL || ref.X != v { + blk := refBlock(ref) + if blk == nil { + return false + } + blocks[blk] = true + continue + } + blk := refBlock(ref) + if blk == nil { + return false + } + blocks[blk] = true + if !p.collectValueUseBlocks(ref, blocks, seen, followPhi) { + return false + } + case *ssa.Phi: + if followPhi { + if !p.collectValueUseBlocks(ref, blocks, seen, followPhi) { + return false + } + continue + } + for i, edge := range ref.Edges { + if edge == v && i < len(ref.Block().Preds) { + blocks[ref.Block().Preds[i]] = true + } + } + default: + instr, ok := ref.(ssa.Instruction) + if !ok || !instructionUsesValue(instr, v) { + return false + } + blk := refBlock(instr) + if blk == nil { + return false + } + blocks[blk] = true + } + } + return true +} + +func (p *context) valueLastUseBlock(v ssa.Value) (*ssa.BasicBlock, bool) { + blocks := make(map[*ssa.BasicBlock]bool) + if !p.collectValueUseBlocks(v, blocks, map[ssa.Value]bool{}, true) { + return nil, false + } + if len(blocks) == 0 { + return nil, true + } + for candidate := range blocks { + ok := true + for blk := range blocks { + if blk != candidate && !blockCanReach(blk, candidate, map[*ssa.BasicBlock]bool{}) { + ok = false + break + } + } + if ok { + return candidate, true + } + } + return nil, false +} + +func (p *context) lastUseInBlock(v ssa.Value, blk *ssa.BasicBlock, order map[ssa.Instruction]int, seen map[ssa.Value]bool) (ssa.Instruction, bool) { + if v == nil || seen[v] { + return nil, true + } + seen[v] = true + refs := v.Referrers() + if refs == nil { + return nil, true + } + var last ssa.Instruction + updateLast := func(instr ssa.Instruction) { + if instr == nil { + return + } + if last == nil || order[instr] > order[last] { + last = instr + } + } + refBeforeBlock := func(refBlk *ssa.BasicBlock) bool { + return refBlk != nil && blk != nil && refBlk != blk && blockCanReach(refBlk, blk, map[*ssa.BasicBlock]bool{}) + } + for _, ref := range *refs { + switch ref := ref.(type) { + case *ssa.DebugRef: + continue + case *ssa.FieldAddr, *ssa.IndexAddr, *ssa.ChangeType, *ssa.Convert, *ssa.MakeInterface: + refVal := ref.(ssa.Value) + refInstr := ref.(ssa.Instruction) + if refInstr.Block() != blk { + if refBeforeBlock(refInstr.Block()) { + continue + } + return nil, false + } + use, ok := p.lastUseInBlock(refVal, blk, order, seen) + if !ok { + return nil, false + } + updateLast(use) + case *ssa.UnOp: + if ref.Op != token.MUL || ref.X != v { + if ref.Block() != blk { + if refBeforeBlock(ref.Block()) { + continue + } + return nil, false + } + updateLast(ref) + continue + } + if ref.Block() != blk { + if refBeforeBlock(ref.Block()) { + continue + } + return nil, false + } + use, ok := p.lastUseInBlock(ref, blk, order, seen) + if !ok { + return nil, false + } + if use != nil { + if isCallLikeInstruction(use) { + updateLast(ref) + continue + } + updateLast(use) + } else { + updateLast(ref) + } + case *ssa.Phi: + use, ok := p.lastUseInBlock(ref, blk, order, seen) + if !ok { + return nil, false + } + updateLast(use) + default: + instr, ok := ref.(ssa.Instruction) + if !ok || !instructionUsesValue(instr, v) { + return nil, false + } + if instr.Block() != blk { + if refBeforeBlock(instr.Block()) { + continue + } + return nil, false + } + updateLast(instr) + } + } + return last, true +} + +func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][]*ssa.Alloc { + plans := make(map[ssa.Instruction][]*ssa.Alloc) + for _, blk := range fn.Blocks { + for _, instr := range blk.Instrs { + alloc, ok := instr.(*ssa.Alloc) + if !ok || !p.shouldClearAlloc(alloc) { + continue + } + useBlk, ok := p.valueLastUseBlock(alloc) + if !ok || useBlk == nil { + continue + } + if useBlk != alloc.Block() && alloc.Block().Index != 0 { + continue + } + order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) + for i, useInstr := range useBlk.Instrs { + order[useInstr] = i + } + last, ok := p.lastUseInBlock(alloc, useBlk, order, map[ssa.Value]bool{}) + if ok && last != nil { + plans[last] = append(plans[last], alloc) + } + } + } + return plans +} + +func (p *context) collectEntryClearPlans(fn *ssa.Function) map[*ssa.BasicBlock][]*ssa.Alloc { + plans := make(map[*ssa.BasicBlock][]*ssa.Alloc) + for _, blk := range fn.Blocks { + if blk == nil || len(blk.Succs) < 2 { + continue + } + for _, instr := range blk.Instrs { + alloc, ok := instr.(*ssa.Alloc) + if !ok || !p.shouldClearAlloc(alloc) { + continue + } + useBlocks := make(map[*ssa.BasicBlock]bool) + if !p.collectValueUseBlocks(alloc, useBlocks, map[ssa.Value]bool{}, false) { + continue + } + liveSucc := make(map[*ssa.BasicBlock]bool, len(blk.Succs)) + for _, succ := range blk.Succs { + for useBlk := range useBlocks { + if useBlk == nil { + continue + } + if succ == useBlk || blockCanReach(succ, useBlk, map[*ssa.BasicBlock]bool{}) { + liveSucc[succ] = true + break + } + } + } + if len(liveSucc) == 0 || len(liveSucc) == len(blk.Succs) { + continue + } + for _, succ := range blk.Succs { + if !liveSucc[succ] && len(succ.Preds) == 1 { + plans[succ] = append(plans[succ], alloc) + } + } + } + } + return plans +} + +func (p *context) collectParamClobberPlans(fn *ssa.Function) map[ssa.Instruction]bool { + plans := make(map[ssa.Instruction]bool) + for _, param := range fn.Params { + if !hasConservativeGCPointers(param.Type(), map[types.Type]bool{}) { + continue + } + useBlk, ok := p.valueLastUseBlock(param) + if !ok || useBlk == nil { + continue + } + order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) + for i, useInstr := range useBlk.Instrs { + order[useInstr] = i + } + last, ok := p.lastUseInBlock(param, useBlk, order, map[ssa.Value]bool{}) + if ok && last != nil { + plans[last] = true + } + } + return plans +} + +func (p *context) collectParamScanPlans(fn *ssa.Function) map[ssa.Instruction][]*ssa.Parameter { + plans := make(map[ssa.Instruction][]*ssa.Parameter) + for _, param := range fn.Params { + if !hasConservativeGCPointers(param.Type(), map[types.Type]bool{}) { + continue + } + useBlk, ok := p.valueLastUseBlock(param) + if !ok || useBlk == nil { + continue + } + order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) + for i, useInstr := range useBlk.Instrs { + order[useInstr] = i + } + last, ok := p.lastUseInBlock(param, useBlk, order, map[ssa.Value]bool{}) + if ok && last != nil { + plans[last] = append(plans[last], param) + } + } + return plans +} + +func (p *context) collectCallClobberPlans(fn *ssa.Function) map[ssa.Instruction]bool { + plans := make(map[ssa.Instruction]bool) + for _, blk := range fn.Blocks { + for _, instr := range blk.Instrs { + call, ok := instr.(*ssa.Call) + if !ok { + continue + } + for _, arg := range call.Common().Args { + if hasConservativeGCPointers(arg.Type(), map[types.Type]bool{}) { + plans[instr] = true + break + } + } + } + } + return plans +} + +func (p *context) compileLateValue(b llssa.Builder, v ssa.Value) llssa.Expr { + switch v := v.(type) { + case *ssa.MakeInterface: + t := p.type_(v.Type(), llssa.InGo) + x := p.compileLateValue(b, v.X) + return b.MakeInterface(t, x) + case *ssa.UnOp: + if v.Op != token.MUL { + return p.compileValue(b, v) + } + x := p.compileLateValue(b, v.X) + return b.UnOp(v.Op, x) + case *ssa.FieldAddr: + x := p.compileLateValue(b, v.X) + return b.FieldAddr(x, v.Field) + case *ssa.IndexAddr: + x := p.compileLateValue(b, v.X) + idx := p.compileLateValue(b, v.Index) + return b.IndexAddr(x, idx) + case *ssa.ChangeType: + t := p.type_(v.Type(), llssa.InGo) + x := p.compileLateValue(b, v.X) + return b.ChangeType(t, x) + case *ssa.Convert: + t := p.type_(v.Type(), llssa.InGo) + x := p.compileLateValue(b, v.X) + return b.Convert(t, x) + } + return p.compileValue(b, v) +} + +func (p *context) scanStackPointer(b llssa.Builder, val llssa.Expr) { + b.Pkg.NeedRuntime = true + t := p.type_(types.Typ[types.UnsafePointer], llssa.InGo) + if !types.Identical(val.RawType(), t.RawType()) { + val = b.Convert(t, val) + } + fn := b.Pkg.NewFunc("runtime.ClearStackPointer", + types.NewSignatureType(nil, nil, nil, types.NewTuple(types.NewParam(token.NoPos, nil, "target", types.Typ[types.UnsafePointer])), nil, false), llssa.InGo) + b.Call(fn.Expr, val) +} + +func (p *context) scanPointerExpr(b llssa.Builder, val llssa.Expr) { + switch t := types.Unalias(val.RawType()).Underlying().(type) { + case *types.Pointer: + p.scanStackPointer(b, val) + case *types.Struct: + if t.NumFields() == 1 { + if _, ok := types.Unalias(t.Field(0).Type()).Underlying().(*types.Pointer); ok { + p.scanStackPointer(b, b.Field(val, 0)) + } + } + } +} + +func (p *context) scanAllocPointer(b llssa.Builder, ptr llssa.Expr) { + elem := b.Prog.Elem(ptr.Type) + switch t := types.Unalias(elem.RawType()).Underlying().(type) { + case *types.Pointer: + p.scanStackPointer(b, b.Load(ptr)) + case *types.Struct: + if t.NumFields() == 1 { + if _, ok := types.Unalias(t.Field(0).Type()).Underlying().(*types.Pointer); ok { + p.scanStackPointer(b, b.Load(b.FieldAddr(ptr, 0))) + } + } + } +} + +func (p *context) scanParamPointers(b llssa.Builder, instr ssa.Instruction) { + params := p.paramScans[instr] + for _, param := range params { + p.scanPointerExpr(b, p.compileValue(b, param)) + } +} + +func (p *context) clearAlloc(b llssa.Builder, alloc *ssa.Alloc) { + ptr := p.compileValue(b, alloc) + b.IfThen(b.BinOp(token.NEQ, ptr, p.prog.Zero(ptr.Type)), func() { + p.scanAllocPointer(b, ptr) + elem := b.Prog.Elem(ptr.Type) + b.Store(ptr, p.prog.Zero(elem)) + }) +} + +func (p *context) clearDeadAllocs(b llssa.Builder, instr ssa.Instruction) { + if p.loadClears[instr] { + return + } + allocs := p.stackClears[instr] + if len(allocs) == 0 { + return + } + for _, alloc := range allocs { + p.clearAlloc(b, alloc) + } + if unop, ok := instr.(*ssa.UnOp); ok && unop.Op == token.MUL { + return + } + p.clobberPointerRegs(b) +} + +func (p *context) clearEntryAllocs(b llssa.Builder, block *ssa.BasicBlock) { + allocs := p.entryClears[block] + if len(allocs) == 0 { + return + } + for _, alloc := range allocs { + p.clearAlloc(b, alloc) + } + p.clobberPointerRegs(b) +} + +func (p *context) clobberPointerRegs(b llssa.Builder) { + b.Pkg.NeedRuntime = true + fn := b.Pkg.NewFunc("runtime.ClobberPointerRegs", + types.NewSignatureType(nil, nil, nil, nil, nil, false), llssa.InGo) + b.Call(fn.Expr) +} + func isPhi(i ssa.Instruction) bool { _, ok := i.(*ssa.Phi) return ok @@ -901,6 +1506,14 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue return ret } } + if len(p.stackClears[v]) > 0 { + x := p.compileValue(b, v.X) + if ret, ok := b.LoadAndClearSinglePointer(x); ok { + p.loadClears[v] = true + p.bvals[iv] = ret + return ret + } + } } x := p.compileValue(b, v.X) if v.Op == token.ARROW { diff --git a/cl/instr.go b/cl/instr.go index 31ac30788e..5d9fa68e77 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -874,6 +874,12 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm ret = p.emitDo(b, act, ds, llssa.Builtin(fn), llssa.Builder.Call, args...) case *ssa.Function: aFn, pyFn, ftype := p.compileFunction(cv) + if p.isRuntimeSetFinalizerCall(call) && len(args) == 2 && act == llssa.Call && ds == nil { + finalizer := p.compileLateValue(b, args[1]) + obj := p.compileLateValue(b, args[0]) + ret = p.emitDo(b, act, nil, aFn.Expr, llssa.Builder.Call, obj, finalizer) + return + } // TODO(xsw): check ca != llssa.Call switch ftype { case cFunc: diff --git a/runtime/internal/clite/bdwgc/bdwgc.go b/runtime/internal/clite/bdwgc/bdwgc.go index e7d2a3e347..ada31837d1 100644 --- a/runtime/internal/clite/bdwgc/bdwgc.go +++ b/runtime/internal/clite/bdwgc/bdwgc.go @@ -97,6 +97,9 @@ func IsDisabled() c.Int //go:linkname Gcollect C.GC_gcollect func Gcollect() +//go:linkname ClearStack C.GC_clear_stack +func ClearStack(arg c.Pointer) c.Pointer + //go:linkname GetMemoryUse C.GC_get_memory_use func GetMemoryUse() uintptr diff --git a/runtime/internal/lib/runtime/_wrap/runtime.c b/runtime/internal/lib/runtime/_wrap/runtime.c index 4dc23cfd53..9cf1feef8c 100644 --- a/runtime/internal/lib/runtime/_wrap/runtime.c +++ b/runtime/internal/lib/runtime/_wrap/runtime.c @@ -1,3 +1,5 @@ +#include +#include #include int llgo_maxprocs() @@ -8,3 +10,65 @@ int llgo_maxprocs() return 1; #endif } + +void llgo_clobber_pointer_regs(uintptr_t a0, uintptr_t a1, uintptr_t a2, uintptr_t a3, + uintptr_t a4, uintptr_t a5, uintptr_t a6, uintptr_t a7) +{ + volatile uintptr_t sink = a0 | a1 | a2 | a3 | a4 | a5 | a6 | a7; + (void)sink; +} + +void llgo_clear_stack_ptr(uintptr_t target) +{ + if (target == 0) { + return; + } + + volatile uintptr_t marker = 0; + uintptr_t *cur = 0; + uintptr_t *end = 0; + +#if defined(__APPLE__) + void *stackaddr = pthread_get_stackaddr_np(pthread_self()); + size_t stacksize = pthread_get_stacksize_np(pthread_self()); + if (stackaddr != 0 && stacksize != 0) { + uintptr_t *mark = (uintptr_t *)▮ + uintptr_t *lo = (uintptr_t *)((char *)stackaddr - stacksize); + uintptr_t *hi = (uintptr_t *)stackaddr; + if (mark >= lo && mark < hi) { + cur = lo; + end = hi; + } else { + lo = (uintptr_t *)stackaddr; + hi = (uintptr_t *)((char *)stackaddr + stacksize); + if (mark >= lo && mark < hi) { + cur = lo; + end = hi; + } + } + } +#elif defined(__linux__) + pthread_attr_t attr; + void *stackaddr = 0; + size_t stacksize = 0; + if (pthread_getattr_np(pthread_self(), &attr) == 0) { + if (pthread_attr_getstack(&attr, &stackaddr, &stacksize) == 0) { + cur = (uintptr_t *)stackaddr; + end = (uintptr_t *)((char *)stackaddr + stacksize); + } + pthread_attr_destroy(&attr); + } +#endif + + if (cur == 0 || end == 0 || end <= cur) { + return; + } + if ((uintptr_t *)target >= cur && (uintptr_t *)target < end) { + return; + } + for (; cur < end; cur++) { + if (*cur == target) { + *cur = 0; + } + } +} diff --git a/runtime/internal/lib/runtime/mfinal.go b/runtime/internal/lib/runtime/mfinal.go index c3498f98f5..b2015bf3c5 100644 --- a/runtime/internal/lib/runtime/mfinal.go +++ b/runtime/internal/lib/runtime/mfinal.go @@ -6,7 +6,171 @@ package runtime +import ( + "unsafe" + + "github.com/goplus/llgo/runtime/abi" + "github.com/goplus/llgo/runtime/internal/clite/bdwgc" + psync "github.com/goplus/llgo/runtime/internal/clite/pthread/sync" + "github.com/goplus/llgo/runtime/internal/clite/sync/atomic" +) + +type finalizerClosure struct { + fn unsafe.Pointer + env unsafe.Pointer +} + +type finalizerEntry struct { + fn any + obj unsafe.Pointer + key uintptr + next *finalizerEntry + prevFn bdwgc.FinalizerFunc + prevCb unsafe.Pointer + stop int32 +} + +var finalizerState struct { + once psync.Once + mu psync.Mutex + m map[uintptr]*finalizerEntry + head *finalizerEntry + tail *finalizerEntry +} + +func initFinalizerState() { + finalizerState.mu.Init(nil) + finalizerState.m = make(map[uintptr]*finalizerEntry) +} + func SetFinalizer(obj any, finalizer any) { - // TODO(xsw): - // panic("todo: runtime.SetFinalizer") + objFace := *(*eface)(unsafe.Pointer(&obj)) + if objFace._type == nil { + throw("runtime.SetFinalizer: first argument is nil") + } + if objFace._type.Kind() != abi.Pointer { + throw("runtime.SetFinalizer: first argument is " + objFace._type.String() + ", not pointer") + } + objPtr := ifacePointerData(&objFace) + if objPtr == nil { + throw("runtime.SetFinalizer: first argument is nil") + } + + finalizerState.once.Do(initFinalizerState) + key := hideFinalizerPtr(objPtr) + + finalizerState.mu.Lock() + if old := finalizerState.m[key]; old != nil { + atomic.Store(&old.stop, 1) + delete(finalizerState.m, key) + restoreFinalizer(objPtr, old) + } + finalizerState.mu.Unlock() + + finalizerFace := *(*eface)(unsafe.Pointer(&finalizer)) + if finalizerFace._type == nil { + return + } + ft := finalizerFuncType(finalizerFace._type) + if ft == nil { + throw("runtime.SetFinalizer: second argument is " + finalizerFace._type.String() + ", not a function") + } + if len(ft.In) != 1 || ft.In[0] != objFace._type { + throw("runtime.SetFinalizer: cannot pass " + objFace._type.String() + " to finalizer " + finalizerFace._type.String()) + } + entry := &finalizerEntry{fn: finalizer, key: key} + var oldFn bdwgc.FinalizerFunc + var oldCb unsafe.Pointer + bdwgc.RegisterFinalizer(objPtr, setFinalizerCallback, unsafe.Pointer(entry), &oldFn, &oldCb) + entry.prevFn = oldFn + entry.prevCb = oldCb + + finalizerState.mu.Lock() + finalizerState.m[key] = entry + finalizerState.mu.Unlock() +} + +func ifacePointerData(e *eface) unsafe.Pointer { + if e._type.IsDirectIface() { + return e.data + } + return *(*unsafe.Pointer)(e.data) +} + +func finalizerFuncType(t *abi.Type) *abi.FuncType { + if t.IsClosure() { + st := t.StructType() + if st == nil || len(st.Fields) == 0 { + return nil + } + return st.Fields[0].Typ.FuncType() + } + return t.FuncType() +} + +func callFinalizer(fn any, ptr unsafe.Pointer) { + c := (*finalizerClosure)((*eface)(unsafe.Pointer(&fn)).data) + f := *(*func(unsafe.Pointer))(unsafe.Pointer(c)) + f(ptr) +} + +func setFinalizerCallback(ptr unsafe.Pointer, cb unsafe.Pointer) { + entry := (*finalizerEntry)(cb) + if entry.prevFn != nil { + entry.prevFn(ptr, entry.prevCb) + } + if atomic.Load(&entry.stop) == 1 { + return + } + + // Keep the object alive until runFinalizers invokes the Go finalizer. + // Do not allocate or lock here; BDWGC calls this while collecting. + entry.obj = ptr + entry.next = nil + if finalizerState.tail == nil { + finalizerState.head = entry + finalizerState.tail = entry + } else { + finalizerState.tail.next = entry + finalizerState.tail = entry + } +} + +func restoreFinalizer(ptr unsafe.Pointer, entry *finalizerEntry) { + var oldFn bdwgc.FinalizerFunc + var oldCb unsafe.Pointer + if entry.prevFn != nil { + bdwgc.RegisterFinalizer(ptr, entry.prevFn, entry.prevCb, &oldFn, &oldCb) + return + } + bdwgc.RegisterFinalizer(ptr, nil, nil, &oldFn, &oldCb) +} + +func runFinalizers() { + finalizerState.once.Do(initFinalizerState) + for { + entry := finalizerState.head + if entry == nil { + return + } + finalizerState.head = entry.next + if finalizerState.head == nil { + finalizerState.tail = nil + } + entry.next = nil + finalizerState.mu.Lock() + if finalizerState.m[entry.key] == entry { + delete(finalizerState.m, entry.key) + } + finalizerState.mu.Unlock() + + if atomic.Load(&entry.stop) != 1 { + callFinalizer(entry.fn, entry.obj) + } + entry.obj = nil + } +} + +func hideFinalizerPtr(ptr unsafe.Pointer) uintptr { + return ^uintptr(ptr) } diff --git a/runtime/internal/lib/runtime/runtime.go b/runtime/internal/lib/runtime/runtime.go index e2a77b8477..9d30d1285f 100644 --- a/runtime/internal/lib/runtime/runtime.go +++ b/runtime/internal/lib/runtime/runtime.go @@ -49,6 +49,22 @@ func Goexit() { func KeepAlive(x any) { } +//go:linkname c_clobber_pointer_regs C.llgo_clobber_pointer_regs +func c_clobber_pointer_regs(a0, a1, a2, a3, a4, a5, a6, a7 uintptr) + +//go:linkname c_clear_stack_ptr C.llgo_clear_stack_ptr +func c_clear_stack_ptr(target uintptr) + +//go:noinline +func ClobberPointerRegs() { + c_clobber_pointer_regs(0, 0, 0, 0, 0, 0, 0, 0) +} + +//go:noinline +func ClearStackPointer(target unsafe.Pointer) { + c_clear_stack_ptr(uintptr(target)) +} + //go:linkname c_write C.write func c_write(fd c.Int, p unsafe.Pointer, n c.SizeT) c.SsizeT diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index be4612921a..ddeec1d002 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -18,11 +18,15 @@ func ReadMemStats(m *runtime.MemStats) { } func GC() { + bdwgc.ClearStack(nil) bdwgc.Gcollect() + runFinalizers() // BDW finalizers are observed on a subsequent collection cycle. // Run one extra cycle so weak-pointer cleanup hooks (unique/weak) see // finalized state before we trigger map cleanup callbacks. + bdwgc.ClearStack(nil) bdwgc.Gcollect() + runFinalizers() unique_runtime_notifyMapCleanup() if poolCleanup != nil { poolCleanup() diff --git a/ssa/memory.go b/ssa/memory.go index 4bb3e7ce98..a5b0983436 100644 --- a/ssa/memory.go +++ b/ssa/memory.go @@ -63,6 +63,34 @@ func (b Builder) aggregateValue(t Type, flds ...llvm.Value) Expr { return Expr{aggregateValue(b.impl, t.ll, flds...), t} } +// LoadAndClearSinglePointer atomically copies a pointer-sized value out of ptr +// and clears the source slot. It handles either *P or *struct{P}. +func (b Builder) LoadAndClearSinglePointer(ptr Expr) (Expr, bool) { + elem := b.Prog.Elem(ptr.Type) + if elem.ll.TypeKind() == llvm.PointerTypeKind { + old := b.loadAndClearPointerWord(ptr.impl, elem.ll) + return Expr{old, elem}, true + } + + st, ok := types.Unalias(elem.RawType()).Underlying().(*types.Struct) + if !ok || st.NumFields() != 1 { + return Nil, false + } + field := b.Prog.rawType(st.Field(0).Type()) + if field.ll.TypeKind() != llvm.PointerTypeKind { + return Nil, false + } + fieldPtr := llvm.CreateStructGEP(b.impl, elem.ll, ptr.impl, 0) + old := b.loadAndClearPointerWord(fieldPtr, field.ll) + return Expr{old, elem}, true +} + +func (b Builder) loadAndClearPointerWord(ptr llvm.Value, typ llvm.Type) llvm.Value { + old := llvm.CreateLoad(b.impl, typ, ptr) + b.impl.CreateStore(llvm.ConstNull(typ), ptr) + return old +} + func aggregateValue(b llvm.Builder, tll llvm.Type, flds ...llvm.Value) llvm.Value { agg := llvm.Undef(tll) for i, fld := range flds { diff --git a/test/go/finalizer_test.go b/test/go/finalizer_test.go new file mode 100644 index 0000000000..6c3f6ffea6 --- /dev/null +++ b/test/go/finalizer_test.go @@ -0,0 +1,216 @@ +/* + * 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 gotest + +import ( + "os" + "path/filepath" + "runtime" + "testing" + "time" +) + +func TestRuntimeSetFinalizerTinyObjects(t *testing.T) { + const n = 32 + finalized := make(chan int32, n) + makeFinalizerTinyObjects(n, finalized) + + done := make([]bool, n) + count := 0 + deadline := time.After(3 * time.Second) + for count <= n/2 { + runGCWithTimeout(t) + for { + select { + case v := <-finalized: + if v < 0 || v >= n { + t.Fatalf("finalizer got %d, want [0,%d)", v, n) + } + if done[v] { + t.Fatalf("finalizer got duplicate value %d", v) + } + done[v] = true + count++ + if count > n/2 { + return + } + default: + goto wait + } + } + wait: + select { + case <-deadline: + t.Fatalf("only %d/%d finalizers ran", count, n) + case <-time.After(10 * time.Millisecond): + } + } +} + +func makeFinalizerTinyObjects(n int, finalized chan<- int32) { + for i := 0; i < n; i++ { + x := new(int32) + *x = int32(i) + runtime.SetFinalizer(x, func(p *int32) { + finalized <- *p + }) + } +} + +func TestRuntimeSetFinalizerCancel(t *testing.T) { + finalized := make(chan struct{}, 1) + func() { + x := new(int) + runtime.SetFinalizer(x, func(*int) { + finalized <- struct{}{} + }) + runtime.SetFinalizer(x, nil) + }() + + for i := 0; i < 3; i++ { + runGCWithTimeout(t) + } + select { + case <-finalized: + t.Fatal("canceled finalizer ran") + case <-time.After(50 * time.Millisecond): + } +} + +const finalizerStackLivenessProbe = `package main + +import ( + "fmt" + "runtime" +) + +type HeapObj [8]int64 + +type StkObj struct { + h *HeapObj +} + +var n int +var c int = -1 +var null StkObj +var sink *HeapObj + +func gc() { + runtime.GC() + runtime.GC() + runtime.GC() + n++ +} + +func keepAliveCase() { + c = -1 + n = 0 + f() + gc() + if c != 1 { + panic(fmt.Sprintf("keepalive collection phase = %d, want 1", c)) + } +} + +func f() { + var s StkObj + s.h = new(HeapObj) + runtime.SetFinalizer(s.h, func(h *HeapObj) { + c = n + }) + g(&s) + gc() +} + +func g(s *StkObj) { + gc() + runtime.KeepAlive(s) + gc() +} + +//go:noinline +func use(p *StkObj) { +} + +//go:noinline +func ambiguousArgCase(s StkObj, b bool) { + var p *StkObj + if b { + p = &s + } else { + p = &null + } + use(p) + gc() + sink = p.h + gc() + sink = nil + gc() +} + +func runAmbiguousArgCase(b bool, want int) { + var s StkObj + s.h = new(HeapObj) + c = -1 + n = 0 + runtime.SetFinalizer(s.h, func(h *HeapObj) { + c = n + }) + ambiguousArgCase(s, b) + if c != want { + panic(fmt.Sprintf("ambiguous arg b=%v collection phase = %d, want %d", b, c, want)) + } +} + +func main() { + keepAliveCase() + runAmbiguousArgCase(true, 2) + runAmbiguousArgCase(false, 0) +} +` + +func TestRuntimeSetFinalizerStackObjectLiveness(t *testing.T) { + dir, err := os.MkdirTemp("", "llgo-finalizer-stack-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + mainFile := filepath.Join(dir, "main.go") + if err := os.WriteFile(mainFile, []byte(finalizerStackLivenessProbe), 0644); err != nil { + t.Fatal(err) + } + + runGoCmd(t, dir, "run", mainFile) + + root := findLLGoRoot(t) + t.Setenv("LLGO_ROOT", root) + runGoCmd(t, root, "run", "./cmd/llgo", "run", mainFile) +} + +func runGCWithTimeout(t *testing.T) { + t.Helper() + done := make(chan struct{}) + go func() { + runtime.GC() + close(done) + }() + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("runtime.GC did not return") + } +} diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index a5a66d10b4..a6ad3a16f3 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2327,10 +2327,6 @@ xfails: directive: runoutput case: fixedbugs/bug449.go reason: bug449 runoutput emits an empty llgo_tmp__.go on linux/amd64 - - platform: darwin/arm64 - directive: run - case: deferfin.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: finprofiled.go @@ -2383,18 +2379,6 @@ xfails: directive: run case: reflectmethod6.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: stackobj.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: stackobj3.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: tinyfin.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/bug347.go From ed0fa41e38416ef5ea859fd1ee7ec6c043f1fae8 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 22 May 2026 22:50:48 +0800 Subject: [PATCH 2/3] runtime: expose Linux pthread stack APIs --- runtime/internal/lib/runtime/_wrap/runtime.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/runtime/internal/lib/runtime/_wrap/runtime.c b/runtime/internal/lib/runtime/_wrap/runtime.c index 9cf1feef8c..2cdda12d1a 100644 --- a/runtime/internal/lib/runtime/_wrap/runtime.c +++ b/runtime/internal/lib/runtime/_wrap/runtime.c @@ -1,3 +1,7 @@ +#if defined(__linux__) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE +#endif + #include #include #include From 36044708da36eeaf85754d70a864941d98635982 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 22 May 2026 23:16:06 +0800 Subject: [PATCH 3/3] runtime: limit stack liveness helpers to finalizer programs --- cl/compile.go | 62 ++++++++++++++++++++++--- runtime/internal/lib/runtime/runtime.go | 16 ------- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 5d5eb6015a..c61156d026 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -840,7 +840,48 @@ func (p *context) enableConservativeLivenessClears(fn *ssa.Function) bool { } path := fn.Pkg.Pkg.Path() if path == "command-line-arguments" { - return true + return p.packageUsesRuntimeSetFinalizer(fn.Pkg) + } + return false +} + +func (p *context) packageUsesRuntimeSetFinalizer(pkg *ssa.Package) bool { + for _, member := range pkg.Members { + fn, ok := member.(*ssa.Function) + if ok && p.functionUsesRuntimeSetFinalizer(fn, map[*ssa.Function]bool{}) { + return true + } + } + return false +} + +func (p *context) functionUsesRuntimeSetFinalizer(fn *ssa.Function, seen map[*ssa.Function]bool) bool { + if fn == nil || seen[fn] { + return false + } + seen[fn] = true + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + switch instr := instr.(type) { + case *ssa.Call: + if p.isRuntimeSetFinalizerCall(&instr.Call) { + return true + } + case *ssa.Defer: + if p.isRuntimeSetFinalizerCall(&instr.Call) { + return true + } + case *ssa.Go: + if p.isRuntimeSetFinalizerCall(&instr.Call) { + return true + } + } + } + } + for _, anon := range fn.AnonFuncs { + if p.functionUsesRuntimeSetFinalizer(anon, seen) { + return true + } } return false } @@ -1316,12 +1357,12 @@ func (p *context) compileLateValue(b llssa.Builder, v ssa.Value) llssa.Expr { func (p *context) scanStackPointer(b llssa.Builder, val llssa.Expr) { b.Pkg.NeedRuntime = true - t := p.type_(types.Typ[types.UnsafePointer], llssa.InGo) + t := p.type_(types.Typ[types.Uintptr], llssa.InGo) if !types.Identical(val.RawType(), t.RawType()) { val = b.Convert(t, val) } - fn := b.Pkg.NewFunc("runtime.ClearStackPointer", - types.NewSignatureType(nil, nil, nil, types.NewTuple(types.NewParam(token.NoPos, nil, "target", types.Typ[types.UnsafePointer])), nil, false), llssa.InGo) + fn := b.Pkg.NewFunc("llgo_clear_stack_ptr", + types.NewSignatureType(nil, nil, nil, types.NewTuple(types.NewParam(token.NoPos, nil, "target", types.Typ[types.Uintptr])), nil, false), llssa.InC) b.Call(fn.Expr, val) } @@ -1398,9 +1439,16 @@ func (p *context) clearEntryAllocs(b llssa.Builder, block *ssa.BasicBlock) { func (p *context) clobberPointerRegs(b llssa.Builder) { b.Pkg.NeedRuntime = true - fn := b.Pkg.NewFunc("runtime.ClobberPointerRegs", - types.NewSignatureType(nil, nil, nil, nil, nil, false), llssa.InGo) - b.Call(fn.Expr) + uintptrParam := func(name string) *types.Var { + return types.NewParam(token.NoPos, nil, name, types.Typ[types.Uintptr]) + } + fn := b.Pkg.NewFunc("llgo_clobber_pointer_regs", + types.NewSignatureType(nil, nil, nil, types.NewTuple( + uintptrParam("a0"), uintptrParam("a1"), uintptrParam("a2"), uintptrParam("a3"), + uintptrParam("a4"), uintptrParam("a5"), uintptrParam("a6"), uintptrParam("a7"), + ), nil, false), llssa.InC) + zero := b.Prog.IntVal(0, b.Prog.Uintptr()) + b.Call(fn.Expr, zero, zero, zero, zero, zero, zero, zero, zero) } func isPhi(i ssa.Instruction) bool { diff --git a/runtime/internal/lib/runtime/runtime.go b/runtime/internal/lib/runtime/runtime.go index 9d30d1285f..e2a77b8477 100644 --- a/runtime/internal/lib/runtime/runtime.go +++ b/runtime/internal/lib/runtime/runtime.go @@ -49,22 +49,6 @@ func Goexit() { func KeepAlive(x any) { } -//go:linkname c_clobber_pointer_regs C.llgo_clobber_pointer_regs -func c_clobber_pointer_regs(a0, a1, a2, a3, a4, a5, a6, a7 uintptr) - -//go:linkname c_clear_stack_ptr C.llgo_clear_stack_ptr -func c_clear_stack_ptr(target uintptr) - -//go:noinline -func ClobberPointerRegs() { - c_clobber_pointer_regs(0, 0, 0, 0, 0, 0, 0, 0) -} - -//go:noinline -func ClearStackPointer(target unsafe.Pointer) { - c_clear_stack_ptr(uintptr(target)) -} - //go:linkname c_write C.write func c_write(fd c.Int, p unsafe.Pointer, n c.SizeT) c.SsizeT