From 9b76be9e9e45354ca45969e8b80a388772d718df Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 14 Nov 2025 11:56:34 +0800 Subject: [PATCH] support ldflags rewrites for initialized globals --- cl/_testgo/rewrite/dep/dep.go | 11 +++ cl/_testgo/rewrite/main.go | 23 +++++ cl/_testgo/rewrite/out.ll | 1 + cl/compile.go | 58 ++++++++++++- internal/build/build.go | 154 ++++++++++++++++------------------ internal/build/build_test.go | 66 ++++++++++++++- ssa/globals.go | 10 +++ 7 files changed, 239 insertions(+), 84 deletions(-) create mode 100644 cl/_testgo/rewrite/dep/dep.go create mode 100644 cl/_testgo/rewrite/main.go create mode 100644 cl/_testgo/rewrite/out.ll diff --git a/cl/_testgo/rewrite/dep/dep.go b/cl/_testgo/rewrite/dep/dep.go new file mode 100644 index 00000000..6dc2e879 --- /dev/null +++ b/cl/_testgo/rewrite/dep/dep.go @@ -0,0 +1,11 @@ +package dep + +import "fmt" + +var VarName = "dep-default" +var VarPlain string + +func PrintVar() { + fmt.Printf("dep.VarName: %s\n", VarName) + fmt.Printf("dep.VarPlain: %s\n", VarPlain) +} diff --git a/cl/_testgo/rewrite/main.go b/cl/_testgo/rewrite/main.go new file mode 100644 index 00000000..4bd616e4 --- /dev/null +++ b/cl/_testgo/rewrite/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "runtime" + + dep "github.com/goplus/llgo/cl/_testgo/rewrite/dep" +) + +var VarName = "main-default" +var VarPlain string + +func printLine(label, value string) { + fmt.Printf("%s: %s\n", label, value) +} + +func main() { + printLine("main.VarName", VarName) + printLine("main.VarPlain", VarPlain) + dep.PrintVar() + printLine("runtime.GOROOT()", runtime.GOROOT()) + printLine("runtime.Version()", runtime.Version()) +} diff --git a/cl/_testgo/rewrite/out.ll b/cl/_testgo/rewrite/out.ll new file mode 100644 index 00000000..1c8a0e79 --- /dev/null +++ b/cl/_testgo/rewrite/out.ll @@ -0,0 +1 @@ +; \ No newline at end of file diff --git a/cl/compile.go b/cl/compile.go index 082b34b5..d239bb07 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -127,6 +127,50 @@ type context struct { cgoArgs []llssa.Expr cgoRet llssa.Expr cgoSymbols []string + rewrites map[string]string +} + +func (p *context) rewriteValue(name string) (string, bool) { + if p.rewrites == nil { + return "", false + } + val, ok := p.rewrites[name] + return val, ok +} + +func (p *context) isStringType(typ types.Type) bool { + for typ != nil { + switch t := typ.Underlying().(type) { + case *types.Basic: + return t.Kind() == types.String + case *types.Pointer: + typ = t.Elem() + continue + default: + return false + } + } + return false +} + +func (p *context) globalFullName(g *ssa.Global) string { + name, _, _ := p.varName(g.Pkg.Pkg, g) + return name +} + +func (p *context) rewriteInitStore(store *ssa.Store, g *ssa.Global) (string, bool) { + fn := store.Block().Parent() + if fn == nil || fn.Synthetic != "package initializer" { + return "", false + } + value, ok := p.rewriteValue(p.globalFullName(g)) + if !ok || !p.isStringType(g.Type()) { + return "", false + } + if _, ok := store.Val.(*ssa.Const); !ok { + return "", false + } + return value, true } type pkgState byte @@ -176,7 +220,9 @@ func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) { log.Println("==> NewVar", name, typ) } g := pkg.NewVar(name, typ, llssa.Background(vtype)) - if define { + if value, ok := p.rewriteValue(name); ok && p.isStringType(typ) { + g.Init(pkg.ConstString(value)) + } else if define { g.InitNil() } } @@ -816,6 +862,11 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { return } } + if g, ok := va.(*ssa.Global); ok { + if _, ok := p.rewriteInitStore(v, g); ok { + return + } + } ptr := p.compileValue(b, va) val := p.compileValue(b, v.Val) b.Store(ptr, val) @@ -980,12 +1031,12 @@ type Patches = map[string]Patch // NewPackage compiles a Go package to LLVM IR package. func NewPackage(prog llssa.Program, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, err error) { - ret, _, err = NewPackageEx(prog, nil, pkg, files) + ret, _, err = NewPackageEx(prog, nil, nil, pkg, files) return } // NewPackageEx compiles a Go package to LLVM IR package. -func NewPackageEx(prog llssa.Program, patches Patches, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, externs []string, err error) { +func NewPackageEx(prog llssa.Program, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, externs []string, err error) { pkgProg := pkg.Prog pkgTypes := pkg.Pkg oldTypes := pkgTypes @@ -1018,6 +1069,7 @@ func NewPackageEx(prog llssa.Program, patches Patches, pkg *ssa.Package, files [ types.Unsafe: {kind: PkgDeclOnly}, // TODO(xsw): PkgNoInit or PkgDeclOnly? }, cgoSymbols: make([]string, 0, 128), + rewrites: rewrites, } ctx.initPyModule() ctx.initFiles(pkgPath, files, pkgName == "C") diff --git a/internal/build/build.go b/internal/build/build.go index 1147d495..0de41c43 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -111,29 +111,28 @@ type OutFmtDetails struct { } type Config struct { - Goos string - Goarch string - Target string // target name (e.g., "rp2040", "wasi") - takes precedence over Goos/Goarch - BinPath string - AppExt string // ".exe" on Windows, empty on Unix - OutFile string // only valid for ModeBuild when len(pkgs) == 1 - OutFmts OutFmts // Output format specifications (only for Target != "") - Emulator bool // run in emulator mode - Port string // target port for flashing - BaudRate int // baudrate for serial communication - RunArgs []string - Mode Mode - BuildMode BuildMode // Build mode: exe, c-archive, c-shared - AbiMode AbiMode - GenExpect bool // only valid for ModeCmpTest - Verbose bool - GenLL bool // generate pkg .ll files - CheckLLFiles bool // check .ll files valid - CheckLinkArgs bool // check linkargs valid - ForceEspClang bool // force to use esp-clang - Tags string - GlobalNames map[string][]string // pkg => names - GlobalDatas map[string]string // pkg.name => data + Goos string + Goarch string + Target string // target name (e.g., "rp2040", "wasi") - takes precedence over Goos/Goarch + BinPath string + AppExt string // ".exe" on Windows, empty on Unix + OutFile string // only valid for ModeBuild when len(pkgs) == 1 + OutFmts OutFmts // Output format specifications (only for Target != "") + Emulator bool // run in emulator mode + Port string // target port for flashing + BaudRate int // baudrate for serial communication + RunArgs []string + Mode Mode + BuildMode BuildMode // Build mode: exe, c-archive, c-shared + AbiMode AbiMode + GenExpect bool // only valid for ModeCmpTest + Verbose bool + GenLL bool // generate pkg .ll files + CheckLLFiles bool // check .ll files valid + CheckLinkArgs bool // check linkargs valid + ForceEspClang bool // force to use esp-clang + Tags string + GlobalRewrites map[string]string // pkg.name => data } func NewDefaultConf(mode Mode) *Config { @@ -335,6 +334,10 @@ func Do(args []string, conf *Config) ([]Package, error) { crossCompile: export, cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), } + + // default runtime globals must be registered before packages are built + addDefaultGlobalString(conf, "runtime.defaultGOROOT="+runtime.GOROOT(), nil) + addDefaultGlobalString(conf, "runtime.buildVersion="+runtime.Version(), nil) pkgs, err := buildAllPkgs(ctx, initial, verbose) check(err) if mode == ModeGen { @@ -351,13 +354,6 @@ func Do(args []string, conf *Config) ([]Package, error) { allPkgs := append([]*aPackage{}, pkgs...) allPkgs = append(allPkgs, dpkg...) - // update globals importpath.name=value - addGlobalString(conf, "runtime.defaultGOROOT="+runtime.GOROOT(), nil) - addGlobalString(conf, "runtime.buildVersion="+runtime.Version(), nil) - - global, err := createGlobals(ctx, ctx.prog, pkgs) - check(err) - for _, pkg := range initial { if needLink(pkg, mode) { name := path.Base(pkg.PkgPath) @@ -369,7 +365,7 @@ func Do(args []string, conf *Config) ([]Package, error) { } // Link main package using the output path from buildOutFmts - err = linkMainPkg(ctx, pkg, allPkgs, global, outFmts.Out, verbose) + err = linkMainPkg(ctx, pkg, allPkgs, outFmts.Out, verbose) if err != nil { return nil, err } @@ -630,6 +626,14 @@ var ( ) func addGlobalString(conf *Config, arg string, mainPkgs []string) { + addGlobalStringWith(conf, arg, mainPkgs, false) +} + +func addDefaultGlobalString(conf *Config, arg string, mainPkgs []string) { + addGlobalStringWith(conf, arg, mainPkgs, true) +} + +func addGlobalStringWith(conf *Config, arg string, mainPkgs []string, skipIfExists bool) { eq := strings.Index(arg, "=") dot := strings.LastIndex(arg[:eq+1], ".") if eq < 0 || dot < 0 { @@ -640,47 +644,23 @@ func addGlobalString(conf *Config, arg string, mainPkgs []string) { if pkg == "main" { pkgs = mainPkgs } - if conf.GlobalNames == nil { - conf.GlobalNames = make(map[string][]string) + if len(pkgs) == 0 { + return } - if conf.GlobalDatas == nil { - conf.GlobalDatas = make(map[string]string) + if conf.GlobalRewrites == nil { + conf.GlobalRewrites = make(map[string]string) } - for _, pkg := range pkgs { - name := pkg + arg[dot:eq] - value := arg[eq+1:] - if _, ok := conf.GlobalDatas[name]; !ok { - conf.GlobalNames[pkg] = append(conf.GlobalNames[pkg], name) - } - conf.GlobalDatas[name] = value - } -} - -func createGlobals(ctx *context, prog llssa.Program, pkgs []*aPackage) (llssa.Package, error) { - if len(ctx.buildConf.GlobalDatas) == 0 { - return nil, nil - } - for _, pkg := range pkgs { - if pkg.ExportFile == "" { - continue - } - if names, ok := ctx.buildConf.GlobalNames[pkg.PkgPath]; ok { - err := pkg.LPkg.Undefined(names...) - if err != nil { - return nil, err - } - pkg.ExportFile += "-global" - pkg.ExportFile, err = exportObject(ctx, pkg.PkgPath+".global", pkg.ExportFile, []byte(pkg.LPkg.String())) - if err != nil { - return nil, err + suffix := arg[dot:eq] + value := arg[eq+1:] + for _, realPkg := range pkgs { + name := realPkg + suffix + if skipIfExists { + if _, exists := conf.GlobalRewrites[name]; exists { + continue } } + conf.GlobalRewrites[name] = value } - global := prog.NewPackage("", "global") - for name, value := range ctx.buildConf.GlobalDatas { - global.AddGlobalString(name, value) - } - return global, nil } // compileExtraFiles compiles extra files (.s/.c) from target configuration and returns object files @@ -752,7 +732,7 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { return objFiles, nil } -func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global llssa.Package, outputPath string, verbose bool) error { +func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPath string, verbose bool) error { needRuntime := false needPyInit := false @@ -794,14 +774,6 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l } objFiles = append(objFiles, extraObjFiles...) - if global != nil { - export, err := exportObject(ctx, pkg.PkgPath+".global", pkg.ExportFile+"-global", []byte(global.String())) - if err != nil { - return err - } - objFiles = append(objFiles, export) - } - if IsFullRpathEnabled() { // Treat every link-time library search path, specified by the -L parameter, as a runtime search path as well. // This is to ensure the final executable can locate libraries with a relocatable install_name @@ -1050,7 +1022,7 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error { cl.SetDebug(cl.DbgFlagAll) } - ret, externs, err := cl.NewPackageEx(ctx.prog, ctx.patches, aPkg.SSA, syntax) + ret, externs, err := cl.NewPackageEx(ctx.prog, ctx.patches, aPkg.rewriteVars, aPkg.SSA, syntax) if showDetail { llssa.SetDebug(0) cl.SetDebug(0) @@ -1171,8 +1143,9 @@ type aPackage struct { AltPkg *packages.Cached LPkg llssa.Package - LinkArgs []string - LLFiles []string + LinkArgs []string + LLFiles []string + rewriteVars map[string]string } type Package = *aPackage @@ -1193,7 +1166,8 @@ func allPkgs(ctx *context, initial []*packages.Package, verbose bool) (all []*aP return } } - all = append(all, &aPackage{p, ssaPkg, altPkg, nil, nil, nil}) + rewrites := collectRewriteVars(ctx, pkgPath) + all = append(all, &aPackage{p, ssaPkg, altPkg, nil, nil, nil, rewrites}) } else { errs = append(errs, p) } @@ -1201,6 +1175,26 @@ func allPkgs(ctx *context, initial []*packages.Package, verbose bool) (all []*aP return } +func collectRewriteVars(ctx *context, pkgPath string) map[string]string { + var rewrites map[string]string + add := func(name, value string) { + if rewrites == nil { + rewrites = make(map[string]string) + } + rewrites[name] = value + } + if data := ctx.buildConf.GlobalRewrites; len(data) != 0 { + prefix := pkgPath + "." + for name, value := range data { + if strings.HasPrefix(name, prefix) { + add(name, value) + delete(data, name) + } + } + } + return rewrites +} + func createSSAPkg(prog *ssa.Program, p *packages.Package, verbose bool) *ssa.Package { pkgSSA := prog.ImportedPackage(p.ID) if pkgSSA == nil { diff --git a/internal/build/build_test.go b/internal/build/build_test.go index ead07db0..c71f0d74 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -8,6 +8,9 @@ import ( "fmt" "io" "os" + "os/exec" + "path/filepath" + "runtime" "testing" "github.com/goplus/llgo/internal/mockable" @@ -94,4 +97,65 @@ func TestCmpTest(t *testing.T) { mockRun([]string{"../../cl/_testgo/runtest"}, &Config{Mode: ModeCmpTest}) } -// TestGenerateOutputFilenames removed - functionality moved to filename_test.go +const ( + rewriteMainPkg = "github.com/goplus/llgo/cl/_testgo/rewrite" + rewriteDepPkg = rewriteMainPkg + "/dep" + rewriteDirPath = "../../cl/_testgo/rewrite" +) + +func TestLdFlagsRewriteVars(t *testing.T) { + buildRewriteBinary(t, false, "build-main", "build-pkg") + buildRewriteBinary(t, false, "rerun-main", "rerun-pkg") +} + +func TestLdFlagsRewriteVarsMainAlias(t *testing.T) { + buildRewriteBinary(t, true, "alias-main", "alias-pkg") +} + +func buildRewriteBinary(t *testing.T, useMainAlias bool, mainVal, depVal string) { + t.Helper() + binPath := filepath.Join(t.TempDir(), "rewrite") + if runtime.GOOS == "windows" { + binPath += ".exe" + } + + cfg := &Config{Mode: ModeBuild, OutFile: binPath} + mainKey := rewriteMainPkg + var mainPkgs []string + if useMainAlias { + mainKey = "main" + mainPkgs = []string{rewriteMainPkg} + } + mainPlain := mainVal + "-plain" + depPlain := depVal + "-plain" + gorootVal := "goroot-" + mainVal + versionVal := "version-" + mainVal + addGlobalString(cfg, mainKey+".VarName="+mainVal, mainPkgs) + addGlobalString(cfg, mainKey+".VarPlain="+mainPlain, mainPkgs) + addGlobalString(cfg, rewriteDepPkg+".VarName="+depVal, nil) + addGlobalString(cfg, rewriteDepPkg+".VarPlain="+depPlain, nil) + addGlobalString(cfg, "runtime.defaultGOROOT="+gorootVal, nil) + addGlobalString(cfg, "runtime.buildVersion="+versionVal, nil) + + if _, err := Do([]string{rewriteDirPath}, cfg); err != nil { + t.Fatalf("ModeBuild failed: %v", err) + } + got := runBinary(t, binPath) + want := fmt.Sprintf( + "main.VarName: %s\nmain.VarPlain: %s\ndep.VarName: %s\ndep.VarPlain: %s\nruntime.GOROOT(): %s\nruntime.Version(): %s\n", + mainVal, mainPlain, depVal, depPlain, gorootVal, versionVal, + ) + if got != want { + t.Fatalf("unexpected binary output:\nwant %q\ngot %q", want, got) + } +} + +func runBinary(t *testing.T, path string, args ...string) string { + t.Helper() + cmd := exec.Command(path, args...) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("failed to run %s: %v\n%s", path, err, output) + } + return string(output) +} diff --git a/ssa/globals.go b/ssa/globals.go index fc15fb59..27413096 100644 --- a/ssa/globals.go +++ b/ssa/globals.go @@ -31,6 +31,16 @@ func (pkg Package) AddGlobalString(name string, value string) { pkg.NewVarEx(name, prog.Pointer(styp)).Init(Expr{cv, styp}) } +// ConstString returns an SSA string constant expression within this package. +func (pkg Package) ConstString(value string) Expr { + prog := pkg.Prog + styp := prog.String() + data := pkg.createGlobalStr(value) + length := prog.IntVal(uint64(len(value)), prog.Uintptr()) + cv := llvm.ConstNamedStruct(styp.ll, []llvm.Value{data, length.impl}) + return Expr{cv, styp} +} + // Undefined global string var by names func (pkg Package) Undefined(names ...string) error { prog := pkg.Prog