Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae690476ba | ||
|
|
dc39b84187 | ||
|
|
faa5330b69 | ||
|
|
8e5b34057e | ||
|
|
cee22db053 | ||
|
|
b25ae1b4e7 | ||
|
|
a6516de181 | ||
|
|
4a268650fc | ||
|
|
7e4e53eb1c | ||
|
|
1473ee98f7 | ||
|
|
8c7e8b6290 | ||
|
|
2a4b2ef023 | ||
|
|
7abb468592 | ||
|
|
bf9c6abb23 | ||
|
|
2d80951e7d | ||
|
|
034b05c53c | ||
|
|
1ba7d1e561 | ||
|
|
d17ff2592a | ||
|
|
4b26cccc90 | ||
|
|
3a1d8693e9 | ||
|
|
2a52d422c5 | ||
|
|
b0f5d34b39 | ||
|
|
8ba8ec71b5 | ||
|
|
1e4616a758 | ||
|
|
22a43622a0 | ||
|
|
065126e270 | ||
|
|
552156ff40 | ||
|
|
36e84196c6 | ||
|
|
7f1e07755a | ||
|
|
af27d0475d | ||
|
|
bb29e8c768 | ||
|
|
eec2c271bd | ||
|
|
1ed924ed50 | ||
|
|
c46ca84122 | ||
|
|
b36be05c1e | ||
|
|
66a537ad29 | ||
|
|
33a00dff1b | ||
|
|
e4a69ce413 | ||
|
|
531f69ae6a | ||
|
|
812dfd45c9 | ||
|
|
e2bb68489d | ||
|
|
9b76be9e9e | ||
|
|
e11ae0e21b | ||
|
|
dac3365c73 |
15
.github/workflows/llgo.yml
vendored
15
.github/workflows/llgo.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
if ${{ startsWith(matrix.os, 'macos') }}; then
|
||||
DEMO_PKG="cargs_darwin_arm64.zip"
|
||||
else
|
||||
DEMO_PKG="cargs_linux_amd64.zip"
|
||||
DEMO_PKG="cargs_linux_amd64.zip"
|
||||
fi
|
||||
|
||||
mkdir -p ./_demo/c/cargs/libs
|
||||
@@ -133,6 +133,13 @@ jobs:
|
||||
chmod +x test.sh
|
||||
./test.sh
|
||||
|
||||
- name: Test export with different symbol names on embedded targets
|
||||
run: |
|
||||
echo "Testing //export with different symbol names on embedded targets..."
|
||||
cd _demo/embed/export
|
||||
chmod +x verify_export.sh
|
||||
./verify_export.sh
|
||||
|
||||
- name: _xtool build tests
|
||||
run: |
|
||||
cd _xtool
|
||||
@@ -186,11 +193,15 @@ jobs:
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{matrix.go}}
|
||||
|
||||
- name: Test Baremetal GC
|
||||
if: ${{!startsWith(matrix.os, 'macos')}}
|
||||
working-directory: runtime/internal/runtime/tinygogc
|
||||
run: llgo test -tags testGC .
|
||||
- name: run llgo test
|
||||
run: |
|
||||
llgo test ./...
|
||||
|
||||
|
||||
hello:
|
||||
continue-on-error: true
|
||||
timeout-minutes: 30
|
||||
|
||||
67
_demo/embed/export/main.go
Normal file
67
_demo/embed/export/main.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/goplus/lib/c"
|
||||
)
|
||||
|
||||
// This demo shows how to use //export with different symbol names on embedded targets.
|
||||
//
|
||||
// On embedded targets, you can export Go functions with different C symbol names.
|
||||
// This is useful for hardware interrupt handlers that require specific names.
|
||||
|
||||
// Standard Go export - same name
|
||||
//
|
||||
//export HelloWorld
|
||||
func HelloWorld() {
|
||||
c.Printf(c.Str("Hello from "))
|
||||
c.Printf(c.Str("HelloWorld\n"))
|
||||
}
|
||||
|
||||
// Embedded target export - different name
|
||||
// Go function name: interruptLPSPI2
|
||||
// Exported C symbol: LPSPI2_IRQHandler
|
||||
//
|
||||
//export LPSPI2_IRQHandler
|
||||
func interruptLPSPI2() {
|
||||
c.Printf(c.Str("LPSPI2 interrupt "))
|
||||
c.Printf(c.Str("handler called\n"))
|
||||
}
|
||||
|
||||
// Embedded target export - different name
|
||||
// Go function name: systemTickHandler
|
||||
// Exported C symbol: SysTick_Handler
|
||||
//
|
||||
//export SysTick_Handler
|
||||
func systemTickHandler() {
|
||||
c.Printf(c.Str("SysTick "))
|
||||
c.Printf(c.Str("handler called\n"))
|
||||
}
|
||||
|
||||
// Embedded target export - different name
|
||||
// Go function name: Add
|
||||
// Exported C symbol: AddFunc
|
||||
//
|
||||
//export AddFunc
|
||||
func Add(a, b int) int {
|
||||
result := a + b
|
||||
c.Printf(c.Str("AddFunc(%d, %d) = %d\n"), a, b, result)
|
||||
return result
|
||||
}
|
||||
|
||||
func main() {
|
||||
c.Printf(c.Str("=== Export Demo ===\n\n"))
|
||||
|
||||
// Call exported functions directly from Go
|
||||
c.Printf(c.Str("Calling HelloWorld:\n"))
|
||||
HelloWorld()
|
||||
|
||||
c.Printf(c.Str("\nSimulating hardware interrupts:\n"))
|
||||
interruptLPSPI2()
|
||||
systemTickHandler()
|
||||
|
||||
c.Printf(c.Str("\nTesting function with return value:\n"))
|
||||
result := Add(10, 20)
|
||||
c.Printf(c.Str("Result: %d\n"), result)
|
||||
|
||||
c.Printf(c.Str("\n=== Demo Complete ===\n"))
|
||||
}
|
||||
54
_demo/embed/export/verify_export.sh
Executable file
54
_demo/embed/export/verify_export.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building for embedded target..."
|
||||
|
||||
# Build for embedded target as executable
|
||||
# Use llgo directly instead of llgo.sh to avoid go.mod version check
|
||||
llgo build -o test-verify --target=esp32 .
|
||||
|
||||
echo "Checking exported symbols..."
|
||||
|
||||
# Get exported symbols
|
||||
exported_symbols=$(nm -gU ./test-verify.elf | grep -E "(HelloWorld|LPSPI2_IRQHandler|SysTick_Handler|AddFunc)" | awk '{print $NF}')
|
||||
|
||||
echo ""
|
||||
echo "Exported symbols:"
|
||||
echo "$exported_symbols" | awk '{print " " $0}'
|
||||
echo ""
|
||||
|
||||
# Check expected symbols
|
||||
expected=("HelloWorld" "LPSPI2_IRQHandler" "SysTick_Handler" "AddFunc")
|
||||
missing=""
|
||||
|
||||
for symbol in "${expected[@]}"; do
|
||||
if ! echo "$exported_symbols" | grep -q "^$symbol$"; then
|
||||
missing="$missing $symbol"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$missing" ]; then
|
||||
echo "❌ Missing symbols:$missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Symbol name mapping verification:"
|
||||
echo " HelloWorld -> HelloWorld"
|
||||
echo " interruptLPSPI2 -> LPSPI2_IRQHandler"
|
||||
echo " systemTickHandler -> SysTick_Handler"
|
||||
echo " Add -> AddFunc"
|
||||
echo ""
|
||||
echo "🎉 All export symbols verified successfully!"
|
||||
echo ""
|
||||
|
||||
echo "Testing that non-embedded target rejects different export names..."
|
||||
# Build without --target should fail with panic
|
||||
if llgo build -o test-notarget . 2>&1 | grep -q 'export comment has wrong name "LPSPI2_IRQHandler"'; then
|
||||
echo "✅ Correctly rejected different export name on non-embedded target"
|
||||
else
|
||||
echo "❌ Should have panicked with 'export comment has wrong name' error"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
echo "Note: Different symbol names are only supported on embedded targets."
|
||||
11
cl/_testgo/rewrite/dep/dep.go
Normal file
11
cl/_testgo/rewrite/dep/dep.go
Normal file
@@ -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)
|
||||
}
|
||||
23
cl/_testgo/rewrite/main.go
Normal file
23
cl/_testgo/rewrite/main.go
Normal file
@@ -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())
|
||||
}
|
||||
1
cl/_testgo/rewrite/out.ll
Normal file
1
cl/_testgo/rewrite/out.ll
Normal file
@@ -0,0 +1 @@
|
||||
;
|
||||
@@ -393,10 +393,10 @@ func TestErrImport(t *testing.T) {
|
||||
|
||||
func TestErrInitLinkname(t *testing.T) {
|
||||
var ctx context
|
||||
ctx.initLinkname("//llgo:link abc", func(name string) (string, bool, bool) {
|
||||
ctx.initLinkname("//llgo:link abc", func(name string, isExport bool) (string, bool, bool) {
|
||||
return "", false, false
|
||||
})
|
||||
ctx.initLinkname("//go:linkname Printf printf", func(name string) (string, bool, bool) {
|
||||
ctx.initLinkname("//go:linkname Printf printf", func(name string, isExport bool) (string, bool, bool) {
|
||||
return "", false, false
|
||||
})
|
||||
defer func() {
|
||||
@@ -404,7 +404,7 @@ func TestErrInitLinkname(t *testing.T) {
|
||||
t.Fatal("initLinkname: no error?")
|
||||
}
|
||||
}()
|
||||
ctx.initLinkname("//go:linkname Printf printf", func(name string) (string, bool, bool) {
|
||||
ctx.initLinkname("//go:linkname Printf printf", func(name string, isExport bool) (string, bool, bool) {
|
||||
return "foo.Printf", false, name == "Printf"
|
||||
})
|
||||
}
|
||||
@@ -506,3 +506,238 @@ func TestInstantiate(t *testing.T) {
|
||||
t.Fatal("error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleExportDiffName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
enableExportRename bool
|
||||
line string
|
||||
fullName string
|
||||
inPkgName string
|
||||
wantHasLinkname bool
|
||||
wantLinkname string
|
||||
wantExport string
|
||||
}{
|
||||
{
|
||||
name: "ExportDiffNames_DifferentName",
|
||||
enableExportRename: true,
|
||||
line: "//export IRQ_Handler",
|
||||
fullName: "pkg.HandleInterrupt",
|
||||
inPkgName: "HandleInterrupt",
|
||||
wantHasLinkname: true,
|
||||
wantLinkname: "IRQ_Handler",
|
||||
wantExport: "IRQ_Handler",
|
||||
},
|
||||
{
|
||||
name: "ExportDiffNames_SameName",
|
||||
enableExportRename: true,
|
||||
line: "//export SameName",
|
||||
fullName: "pkg.SameName",
|
||||
inPkgName: "SameName",
|
||||
wantHasLinkname: true,
|
||||
wantLinkname: "SameName",
|
||||
wantExport: "SameName",
|
||||
},
|
||||
{
|
||||
name: "ExportDiffNames_WithSpaces",
|
||||
enableExportRename: true,
|
||||
line: "//export Timer_Callback ",
|
||||
fullName: "pkg.OnTimerTick",
|
||||
inPkgName: "OnTimerTick",
|
||||
wantHasLinkname: true,
|
||||
wantLinkname: "Timer_Callback",
|
||||
wantExport: "Timer_Callback",
|
||||
},
|
||||
{
|
||||
name: "ExportDiffNames_Disabled_MatchingName",
|
||||
enableExportRename: false,
|
||||
line: "//export Func",
|
||||
fullName: "pkg.Func",
|
||||
inPkgName: "Func",
|
||||
wantHasLinkname: true,
|
||||
wantLinkname: "Func",
|
||||
wantExport: "Func",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Save and restore global state
|
||||
oldEnableExportRename := enableExportRename
|
||||
defer func() {
|
||||
EnableExportRename(oldEnableExportRename)
|
||||
}()
|
||||
EnableExportRename(tt.enableExportRename)
|
||||
|
||||
// Setup context
|
||||
prog := llssa.NewProgram(nil)
|
||||
pkg := prog.NewPackage("test", "test")
|
||||
ctx := &context{
|
||||
prog: prog,
|
||||
pkg: pkg,
|
||||
}
|
||||
|
||||
// Call initLinkname with closure that mimics initLinknameByDoc behavior
|
||||
ret := ctx.initLinkname(tt.line, func(name string, isExport bool) (string, bool, bool) {
|
||||
return tt.fullName, false, name == tt.inPkgName || (isExport && enableExportRename)
|
||||
})
|
||||
|
||||
// Verify result
|
||||
hasLinkname := (ret == hasLinkname)
|
||||
if hasLinkname != tt.wantHasLinkname {
|
||||
t.Errorf("hasLinkname = %v, want %v", hasLinkname, tt.wantHasLinkname)
|
||||
}
|
||||
|
||||
if tt.wantHasLinkname {
|
||||
// Check linkname was set
|
||||
if link, ok := prog.Linkname(tt.fullName); !ok || link != tt.wantLinkname {
|
||||
t.Errorf("linkname = %q (ok=%v), want %q", link, ok, tt.wantLinkname)
|
||||
}
|
||||
|
||||
// Check export was set
|
||||
exports := pkg.ExportFuncs()
|
||||
if export, ok := exports[tt.fullName]; !ok || export != tt.wantExport {
|
||||
t.Errorf("export = %q (ok=%v), want %q", export, ok, tt.wantExport)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitLinknameByDocExportDiffNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
enableExportRename bool
|
||||
doc *ast.CommentGroup
|
||||
fullName string
|
||||
inPkgName string
|
||||
wantExported bool // Whether the symbol should be exported with different name
|
||||
wantLinkname string
|
||||
wantExport string
|
||||
}{
|
||||
{
|
||||
name: "WithExportDiffNames_DifferentNameExported",
|
||||
enableExportRename: true,
|
||||
doc: &ast.CommentGroup{
|
||||
List: []*ast.Comment{
|
||||
{Text: "//export IRQ_Handler"},
|
||||
},
|
||||
},
|
||||
fullName: "pkg.HandleInterrupt",
|
||||
inPkgName: "HandleInterrupt",
|
||||
wantExported: true,
|
||||
wantLinkname: "IRQ_Handler",
|
||||
wantExport: "IRQ_Handler",
|
||||
},
|
||||
{
|
||||
name: "WithoutExportDiffNames_NotExported",
|
||||
enableExportRename: false,
|
||||
doc: &ast.CommentGroup{
|
||||
List: []*ast.Comment{
|
||||
{Text: "//export DifferentName"},
|
||||
},
|
||||
},
|
||||
fullName: "pkg.HandleInterrupt",
|
||||
inPkgName: "HandleInterrupt",
|
||||
wantExported: false,
|
||||
// Without enableExportRename, it goes through normal flow which expects same name
|
||||
// The symbol "DifferentName" won't be found, so no export happens
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Without enableExportRename, export with different name will panic
|
||||
if !tt.wantExported && !tt.enableExportRename {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic for export with different name when enableExportRename=false")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Save and restore global state
|
||||
oldEnableExportRename := enableExportRename
|
||||
defer func() {
|
||||
EnableExportRename(oldEnableExportRename)
|
||||
}()
|
||||
EnableExportRename(tt.enableExportRename)
|
||||
|
||||
// Setup context
|
||||
prog := llssa.NewProgram(nil)
|
||||
pkg := prog.NewPackage("test", "test")
|
||||
ctx := &context{
|
||||
prog: prog,
|
||||
pkg: pkg,
|
||||
}
|
||||
|
||||
// Call initLinknameByDoc
|
||||
ctx.initLinknameByDoc(tt.doc, tt.fullName, tt.inPkgName, false)
|
||||
|
||||
// Verify export behavior
|
||||
exports := pkg.ExportFuncs()
|
||||
if tt.wantExported {
|
||||
// Should have exported the symbol with different name
|
||||
if export, ok := exports[tt.fullName]; !ok || export != tt.wantExport {
|
||||
t.Errorf("export = %q (ok=%v), want %q", export, ok, tt.wantExport)
|
||||
}
|
||||
// Check linkname was also set
|
||||
if link, ok := prog.Linkname(tt.fullName); !ok || link != tt.wantLinkname {
|
||||
t.Errorf("linkname = %q (ok=%v), want %q", link, ok, tt.wantLinkname)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitLinkExportDiffNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
enableExportRename bool
|
||||
line string
|
||||
wantPanic bool
|
||||
}{
|
||||
{
|
||||
name: "ExportDiffNames_Enabled_NoError",
|
||||
enableExportRename: true,
|
||||
line: "//export IRQ_Handler",
|
||||
wantPanic: false,
|
||||
},
|
||||
{
|
||||
name: "ExportDiffNames_Disabled_Panic",
|
||||
enableExportRename: false,
|
||||
line: "//export IRQ_Handler",
|
||||
wantPanic: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantPanic {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic but didn't panic")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
oldEnableExportRename := enableExportRename
|
||||
defer func() {
|
||||
EnableExportRename(oldEnableExportRename)
|
||||
}()
|
||||
EnableExportRename(tt.enableExportRename)
|
||||
|
||||
prog := llssa.NewProgram(nil)
|
||||
pkg := prog.NewPackage("test", "test")
|
||||
ctx := &context{
|
||||
prog: prog,
|
||||
pkg: pkg,
|
||||
}
|
||||
|
||||
ctx.initLinkname(tt.line, func(inPkgName string, isExport bool) (fullName string, isVar, ok bool) {
|
||||
// Simulate initLinknames scenario: symbol not found (like in decl packages)
|
||||
return "", false, false
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,11 @@ var (
|
||||
enableDbg bool
|
||||
enableDbgSyms bool
|
||||
disableInline bool
|
||||
|
||||
// enableExportRename enables //export to use different C symbol names than Go function names.
|
||||
// This is for TinyGo compatibility when using -target flag for embedded targets.
|
||||
// Currently, using -target implies TinyGo embedded target mode.
|
||||
enableExportRename bool
|
||||
)
|
||||
|
||||
// SetDebug sets debug flags.
|
||||
@@ -73,6 +78,12 @@ func EnableTrace(b bool) {
|
||||
enableCallTracing = b
|
||||
}
|
||||
|
||||
// EnableExportRename enables or disables //export with different C symbol names.
|
||||
// This is enabled when using -target flag for TinyGo compatibility.
|
||||
func EnableExportRename(b bool) {
|
||||
enableExportRename = b
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
type instrOrValue interface {
|
||||
@@ -127,6 +138,58 @@ 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
|
||||
}
|
||||
dot := strings.LastIndex(name, ".")
|
||||
if dot < 0 || dot == len(name)-1 {
|
||||
return "", false
|
||||
}
|
||||
varName := name[dot+1:]
|
||||
val, ok := p.rewrites[varName]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// isStringPtrType checks if typ is a pointer to the basic string type (*string).
|
||||
// This is used to validate that -ldflags -X can only rewrite variables of type *string,
|
||||
// not derived string types like "type T string".
|
||||
func (p *context) isStringPtrType(typ types.Type) bool {
|
||||
ptr, ok := typ.(*types.Pointer)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
basic, ok := ptr.Elem().(*types.Basic)
|
||||
return ok && basic.Kind() == types.String
|
||||
}
|
||||
|
||||
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) {
|
||||
if p.rewrites == nil {
|
||||
return "", false
|
||||
}
|
||||
fn := store.Block().Parent()
|
||||
if fn == nil || fn.Synthetic != "package initializer" {
|
||||
return "", false
|
||||
}
|
||||
if _, ok := store.Val.(*ssa.Const); !ok {
|
||||
return "", false
|
||||
}
|
||||
if !p.isStringPtrType(g.Type()) {
|
||||
return "", false
|
||||
}
|
||||
value, ok := p.rewriteValue(p.globalFullName(g))
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
type pkgState byte
|
||||
@@ -176,7 +239,16 @@ 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 {
|
||||
if p.isStringPtrType(gbl.Type()) {
|
||||
g.Init(pkg.ConstString(value))
|
||||
} else {
|
||||
log.Printf("warning: ignoring rewrite for non-string variable %s (type: %v)", name, gbl.Type())
|
||||
if define {
|
||||
g.InitNil()
|
||||
}
|
||||
}
|
||||
} else if define {
|
||||
g.InitNil()
|
||||
}
|
||||
}
|
||||
@@ -816,6 +888,13 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if p.rewrites != nil {
|
||||
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 +1059,22 @@ 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) {
|
||||
//
|
||||
// Parameters:
|
||||
// - prog: target LLVM SSA program context
|
||||
// - patches: optional package patches applied during compilation
|
||||
// - rewrites: per-package string initializers rewritten at compile time
|
||||
// - pkg: SSA package to compile
|
||||
// - files: parsed AST files that belong to the package
|
||||
//
|
||||
// The rewrites map uses short variable names (without package qualifier) and
|
||||
// only affects string-typed globals defined in the current package.
|
||||
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 +1107,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")
|
||||
|
||||
19
cl/import.go
19
cl/import.go
@@ -73,7 +73,7 @@ func (p *pkgSymInfo) initLinknames(ctx *context) {
|
||||
lines := bytes.Split(b, sep)
|
||||
for _, line := range lines {
|
||||
if bytes.HasPrefix(line, commentPrefix) {
|
||||
ctx.initLinkname(string(line), func(inPkgName string) (fullName string, isVar, ok bool) {
|
||||
ctx.initLinkname(string(line), func(inPkgName string, isExport bool) (fullName string, isVar, ok bool) {
|
||||
if sym, ok := p.syms[inPkgName]; ok && file == sym.file {
|
||||
return sym.fullName, sym.isVar, true
|
||||
}
|
||||
@@ -277,8 +277,8 @@ func (p *context) initLinknameByDoc(doc *ast.CommentGroup, fullName, inPkgName s
|
||||
if doc != nil {
|
||||
for n := len(doc.List) - 1; n >= 0; n-- {
|
||||
line := doc.List[n].Text
|
||||
ret := p.initLinkname(line, func(name string) (_ string, _, ok bool) {
|
||||
return fullName, isVar, name == inPkgName
|
||||
ret := p.initLinkname(line, func(name string, isExport bool) (_ string, _, ok bool) {
|
||||
return fullName, isVar, name == inPkgName || (isExport && enableExportRename)
|
||||
})
|
||||
if ret != unknownDirective {
|
||||
return ret == hasLinkname
|
||||
@@ -294,7 +294,7 @@ const (
|
||||
unknownDirective = -1
|
||||
)
|
||||
|
||||
func (p *context) initLinkname(line string, f func(inPkgName string) (fullName string, isVar, ok bool)) int {
|
||||
func (p *context) initLinkname(line string, f func(inPkgName string, isExport bool) (fullName string, isVar, ok bool)) int {
|
||||
const (
|
||||
linkname = "//go:linkname "
|
||||
llgolink = "//llgo:link "
|
||||
@@ -324,17 +324,24 @@ func (p *context) initLinkname(line string, f func(inPkgName string) (fullName s
|
||||
return noDirective
|
||||
}
|
||||
|
||||
func (p *context) initLink(line string, prefix int, export bool, f func(inPkgName string) (fullName string, isVar, ok bool)) {
|
||||
func (p *context) initLink(line string, prefix int, export bool, f func(inPkgName string, isExport bool) (fullName string, isVar, ok bool)) {
|
||||
text := strings.TrimSpace(line[prefix:])
|
||||
if idx := strings.IndexByte(text, ' '); idx > 0 {
|
||||
inPkgName := text[:idx]
|
||||
if fullName, _, ok := f(inPkgName); ok {
|
||||
if fullName, _, ok := f(inPkgName, export); ok {
|
||||
link := strings.TrimLeft(text[idx+1:], " ")
|
||||
p.prog.SetLinkname(fullName, link)
|
||||
if export {
|
||||
p.pkg.SetExport(fullName, link)
|
||||
}
|
||||
} else {
|
||||
// Export with different names already processed by initLinknameByDoc
|
||||
if export && enableExportRename {
|
||||
return
|
||||
}
|
||||
if export {
|
||||
panic(fmt.Sprintf("export comment has wrong name %q", inPkgName))
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "==>", line)
|
||||
fmt.Fprintf(os.Stderr, "llgo: linkname %s not found and ignored\n", inPkgName)
|
||||
}
|
||||
|
||||
169
cl/rewrite_internal_test.go
Normal file
169
cl/rewrite_internal_test.go
Normal file
@@ -0,0 +1,169 @@
|
||||
//go:build !llgo
|
||||
// +build !llgo
|
||||
|
||||
package cl
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
gpackages "github.com/goplus/gogen/packages"
|
||||
llssa "github.com/goplus/llgo/ssa"
|
||||
"github.com/goplus/llgo/ssa/ssatest"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
"golang.org/x/tools/go/ssa/ssautil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
llssa.Initialize(llssa.InitAll | llssa.InitNative)
|
||||
}
|
||||
|
||||
func compileWithRewrites(t *testing.T, src string, rewrites map[string]string) string {
|
||||
t.Helper()
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, "rewrite.go", src, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed: %v", err)
|
||||
}
|
||||
importer := gpackages.NewImporter(fset)
|
||||
mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics
|
||||
pkg, _, err := ssautil.BuildPackage(&types.Config{Importer: importer}, fset,
|
||||
types.NewPackage(file.Name.Name, file.Name.Name), []*ast.File{file}, mode)
|
||||
if err != nil {
|
||||
t.Fatalf("build package failed: %v", err)
|
||||
}
|
||||
prog := ssatest.NewProgramEx(t, nil, importer)
|
||||
prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH))
|
||||
ret, _, err := NewPackageEx(prog, nil, rewrites, pkg, []*ast.File{file})
|
||||
if err != nil {
|
||||
t.Fatalf("NewPackageEx failed: %v", err)
|
||||
}
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func TestRewriteGlobalStrings(t *testing.T) {
|
||||
const src = `package rewritepkg
|
||||
var VarInit = "original_value"
|
||||
var VarPlain string
|
||||
func Use() string { return VarInit + VarPlain }
|
||||
`
|
||||
ir := compileWithRewrites(t, src, map[string]string{
|
||||
"VarInit": "rewrite_init",
|
||||
"VarPlain": "rewrite_plain",
|
||||
})
|
||||
if strings.Contains(ir, "original_value") {
|
||||
t.Fatalf("original initializer still present:\n%s", ir)
|
||||
}
|
||||
for _, want := range []string{`c"rewrite_init"`, `c"rewrite_plain"`} {
|
||||
if !strings.Contains(ir, want) {
|
||||
t.Fatalf("missing %s in IR:\n%s", want, ir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteSkipsNonConstStores(t *testing.T) {
|
||||
const src = `package rewritepkg
|
||||
import "strings"
|
||||
var VarInit = strings.ToUpper("original_value")
|
||||
var VarPlain string
|
||||
func Use() string { return VarInit + VarPlain }
|
||||
`
|
||||
ir := compileWithRewrites(t, src, map[string]string{
|
||||
"VarInit": "rewrite_init",
|
||||
"VarPlain": "rewrite_plain",
|
||||
})
|
||||
if !strings.Contains(ir, `c"rewrite_init"`) {
|
||||
t.Fatalf("expected rewrite_init constant to remain:\n%s", ir)
|
||||
}
|
||||
if !strings.Contains(ir, "strings.ToUpper") {
|
||||
t.Fatalf("expected call to strings.ToUpper in IR:\n%s", ir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteValueNoDot(t *testing.T) {
|
||||
ctx := &context{rewrites: map[string]string{"VarInit": "rewrite_init"}}
|
||||
if _, ok := ctx.rewriteValue("VarInit"); ok {
|
||||
t.Fatalf("rewriteValue should skip names without package prefix")
|
||||
}
|
||||
if _, ok := ctx.rewriteValue("pkg."); ok {
|
||||
t.Fatalf("rewriteValue should skip trailing dot names")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsStringPtrTypeDefault(t *testing.T) {
|
||||
ctx := &context{}
|
||||
if ctx.isStringPtrType(types.NewPointer(types.Typ[types.Int])) {
|
||||
t.Fatalf("expected non-string pointer to return false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsStringPtrTypeBranches(t *testing.T) {
|
||||
ctx := &context{}
|
||||
if ctx.isStringPtrType(types.NewSlice(types.Typ[types.String])) {
|
||||
t.Fatalf("slice should trigger default branch and return false")
|
||||
}
|
||||
if ctx.isStringPtrType(nil) {
|
||||
t.Fatalf("nil type should return false")
|
||||
}
|
||||
if !ctx.isStringPtrType(types.NewPointer(types.Typ[types.String])) {
|
||||
t.Fatalf("*string should return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteIgnoredInNonInitStore(t *testing.T) {
|
||||
const src = `package rewritepkg
|
||||
var VarInit = "original_value"
|
||||
func Override() { VarInit = "override_value" }
|
||||
`
|
||||
ir := compileWithRewrites(t, src, map[string]string{"VarInit": "rewrite_init"})
|
||||
if !strings.Contains(ir, `c"override_value"`) {
|
||||
t.Fatalf("override store should retain original literal:\n%s", ir)
|
||||
}
|
||||
if !strings.Contains(ir, `c"rewrite_init"`) {
|
||||
t.Fatalf("global initializer should still be rewritten:\n%s", ir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteMissingEntry(t *testing.T) {
|
||||
const src = `package rewritepkg
|
||||
var VarInit = "original_value"
|
||||
var VarOther = "other_value"
|
||||
`
|
||||
ir := compileWithRewrites(t, src, map[string]string{"VarInit": "rewrite_init"})
|
||||
if !strings.Contains(ir, `c"other_value"`) {
|
||||
t.Fatalf("VarOther should keep original initializer:\n%s", ir)
|
||||
}
|
||||
if !strings.Contains(ir, `c"rewrite_init"`) {
|
||||
t.Fatalf("VarInit should still be rewritten:\n%s", ir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteIgnoresNonStringVar(t *testing.T) {
|
||||
const src = `package rewritepkg
|
||||
type wrapper struct{ v int }
|
||||
var VarStruct = wrapper{v: 1}
|
||||
`
|
||||
ir := compileWithRewrites(t, src, map[string]string{"VarStruct": "rewrite_struct"})
|
||||
if strings.Contains(ir, `c"rewrite_struct"`) {
|
||||
t.Fatalf("non-string variables must not be rewritten:\n%s", ir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteIgnoresStringAlias(t *testing.T) {
|
||||
const src = `package rewritepkg
|
||||
type T string
|
||||
var VarAlias T = "original_value"
|
||||
`
|
||||
ir := compileWithRewrites(t, src, map[string]string{"VarAlias": "rewrite_alias"})
|
||||
if strings.Contains(ir, `c"rewrite_alias"`) {
|
||||
t.Fatalf("string alias types must not be rewritten:\n%s", ir)
|
||||
}
|
||||
if !strings.Contains(ir, `c"original_value"`) {
|
||||
t.Fatalf("original value should remain for alias type:\n%s", ir)
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,9 @@ var CheckLinkArgs bool
|
||||
var CheckLLFiles bool
|
||||
var GenLLFiles bool
|
||||
var ForceEspClang bool
|
||||
var SizeReport bool
|
||||
var SizeFormat string
|
||||
var SizeLevel string
|
||||
|
||||
func AddCommonFlags(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&Verbose, "v", false, "Verbose output")
|
||||
@@ -51,6 +54,10 @@ func AddBuildFlags(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&GenLLFiles, "gen-llfiles", false, "generate .ll files for pkg export")
|
||||
fs.BoolVar(&ForceEspClang, "force-espclang", false, "force to use esp-clang")
|
||||
}
|
||||
|
||||
fs.BoolVar(&SizeReport, "size", false, "Print size report after build")
|
||||
fs.StringVar(&SizeFormat, "size:format", "", "Size report format (text,json)")
|
||||
fs.StringVar(&SizeLevel, "size:level", "", "Size report aggregation level (full,module,package)")
|
||||
}
|
||||
|
||||
func AddBuildModeFlags(fs *flag.FlagSet) {
|
||||
@@ -79,6 +86,15 @@ func UpdateConfig(conf *build.Config) error {
|
||||
conf.Target = Target
|
||||
conf.Port = Port
|
||||
conf.BaudRate = BaudRate
|
||||
if SizeReport || SizeFormat != "" || SizeLevel != "" {
|
||||
conf.SizeReport = true
|
||||
if SizeFormat != "" {
|
||||
conf.SizeFormat = SizeFormat
|
||||
}
|
||||
if SizeLevel != "" {
|
||||
conf.SizeLevel = SizeLevel
|
||||
}
|
||||
}
|
||||
|
||||
switch conf.Mode {
|
||||
case build.ModeBuild:
|
||||
|
||||
58
doc/size-report.md
Normal file
58
doc/size-report.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Size Report Options
|
||||
|
||||
The `llgo build -size` flag emits a TinyGo-style table showing how much code,
|
||||
rodata, data, and BSS each component contributes to the final binary. This
|
||||
document captures the parsing strategy and new aggregation controls.
|
||||
|
||||
## Parsing Strategy
|
||||
|
||||
- We invoke `llvm-readelf --elf-output-style=LLVM --all <binary>` and parse the textual output with an
|
||||
indentation-sensitive state machine (no JSON). Only the `Sections` and
|
||||
`Symbols` blocks are inspected.
|
||||
- Section metadata records the index, address, size, name, and segment. Each
|
||||
section is classified into text/rodata/data/bss buckets.
|
||||
- Symbols are attached to their containing sections with start addresses. By
|
||||
sorting symbols and walking their ranges, we compute byte spans that can be
|
||||
attributed to packages/modules.
|
||||
- Sections with no symbols fall back to `(unknown <section>)`, and gaps become
|
||||
`(padding <section>)` entries so totals still add up.
|
||||
|
||||
## Aggregation Levels
|
||||
|
||||
`-size:level` controls how symbol names are grouped prior to reporting:
|
||||
|
||||
| Level | Behavior |
|
||||
|-----------|---------------------------------------------------------------------------|
|
||||
| `full` | Keeps the raw owner from the symbol name (previous behavior). |
|
||||
| `package` | Uses the list of packages built in `build.Do` and groups by `pkg.PkgPath`. |
|
||||
| `module`* | Default. Groups by `pkg.Module.Path` (or `pkg.PkgPath` if the module is nil). |
|
||||
|
||||
Matching is performed by checking whether the demangled symbol name begins with
|
||||
`pkg.PkgPath + "."`. Symbols that do not match any package and contain `llgo` are
|
||||
bucketed into `llgo-stubs`; other unmatched entries keep their original owner
|
||||
names so we can inspect them later.
|
||||
|
||||
Examples:
|
||||
|
||||
```sh
|
||||
llgo build -size . # module-level aggregation (default)
|
||||
llgo build -size -size:level=package . # collapse by package ID
|
||||
llgo build -size -size:level=full . # show raw symbol owners
|
||||
llgo build -size -size:format=json . # JSON output (works with all levels)
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
1. Unit tests: `go test ./internal/build -run TestParseReadelfOutput -count=1`.
|
||||
2. Real binary test:
|
||||
```sh
|
||||
cd cl/_testgo/rewrite
|
||||
../../../llgo.sh build .
|
||||
LLGO_SIZE_REPORT_BIN=$(pwd)/rewrite \
|
||||
go test ./internal/build -run TestParseReadelfRealBinary -count=1
|
||||
```
|
||||
3. Manual smoke test: `../../../llgo.sh build -size -size:level=module .` (or
|
||||
`package`/`full` as desired).
|
||||
|
||||
The parser works across Mach-O and ELF targets as long as `llvm-readelf` is in
|
||||
`PATH`.
|
||||
@@ -132,10 +132,19 @@ type Config struct {
|
||||
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
|
||||
SizeReport bool // print size report after successful build
|
||||
SizeFormat string // size report format: text,json
|
||||
SizeLevel string // size aggregation level: full,module,package
|
||||
// GlobalRewrites specifies compile-time overrides for global string variables.
|
||||
// Keys are fully qualified package paths (e.g. "main" or "github.com/user/pkg").
|
||||
// Each Rewrites entry maps variable names to replacement string values. Only
|
||||
// string-typed globals are supported and "main" applies to all root main
|
||||
// packages in the current build.
|
||||
GlobalRewrites map[string]Rewrites
|
||||
}
|
||||
|
||||
type Rewrites map[string]string
|
||||
|
||||
func NewDefaultConf(mode Mode) *Config {
|
||||
bin := os.Getenv("GOBIN")
|
||||
if bin == "" {
|
||||
@@ -199,6 +208,15 @@ func Do(args []string, conf *Config) ([]Package, error) {
|
||||
if conf.BuildMode == "" {
|
||||
conf.BuildMode = BuildModeExe
|
||||
}
|
||||
if conf.SizeReport && conf.SizeFormat == "" {
|
||||
conf.SizeFormat = "text"
|
||||
}
|
||||
if conf.SizeReport && conf.SizeLevel == "" {
|
||||
conf.SizeLevel = "module"
|
||||
}
|
||||
if err := ensureSizeReporting(conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Handle crosscompile configuration first to set correct GOOS/GOARCH
|
||||
forceEspClang := conf.ForceEspClang || conf.Target != ""
|
||||
export, err := crosscompile.Use(conf.Goos, conf.Goarch, conf.Target, IsWasiThreadsEnabled(), forceEspClang)
|
||||
@@ -214,6 +232,11 @@ func Do(args []string, conf *Config) ([]Package, error) {
|
||||
conf.Goarch = export.GOARCH
|
||||
}
|
||||
|
||||
// Enable different export names for TinyGo compatibility when using -target
|
||||
if conf.Target != "" {
|
||||
cl.EnableExportRename(true)
|
||||
}
|
||||
|
||||
verbose := conf.Verbose
|
||||
patterns := args
|
||||
tags := "llgo,math_big_pure_go"
|
||||
@@ -335,6 +358,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
|
||||
addGlobalString(conf, "runtime.defaultGOROOT="+runtime.GOROOT(), nil)
|
||||
addGlobalString(conf, "runtime.buildVersion="+runtime.Version(), nil)
|
||||
pkgs, err := buildAllPkgs(ctx, initial, verbose)
|
||||
check(err)
|
||||
if mode == ModeGen {
|
||||
@@ -345,19 +372,11 @@ func Do(args []string, conf *Config) ([]Package, error) {
|
||||
}
|
||||
return nil, fmt.Errorf("initial package not found")
|
||||
}
|
||||
|
||||
dpkg, err := buildAllPkgs(ctx, altPkgs[noRt:], verbose)
|
||||
check(err)
|
||||
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,10 +388,15 @@ 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
|
||||
}
|
||||
if conf.Mode == ModeBuild && conf.SizeReport {
|
||||
if err := reportBinarySize(outFmts.Out, conf.SizeFormat, conf.SizeLevel, allPkgs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate C headers for c-archive and c-shared modes before linking
|
||||
if ctx.buildConf.BuildMode == BuildModeCArchive || ctx.buildConf.BuildMode == BuildModeCShared {
|
||||
@@ -629,58 +653,59 @@ var (
|
||||
errXflags = errors.New("-X flag requires argument of the form importpath.name=value")
|
||||
)
|
||||
|
||||
// maxRewriteValueLength limits the size of rewrite values to prevent
|
||||
// excessive memory usage and potential resource exhaustion during compilation.
|
||||
const maxRewriteValueLength = 1 << 20 // 1 MiB cap per rewrite value
|
||||
|
||||
func addGlobalString(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 {
|
||||
panic(errXflags)
|
||||
}
|
||||
pkg := arg[:dot]
|
||||
varName := arg[dot+1 : eq]
|
||||
value := arg[eq+1:]
|
||||
validateRewriteInput(pkg, varName, value)
|
||||
pkgs := []string{pkg}
|
||||
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]Rewrites)
|
||||
}
|
||||
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)
|
||||
for _, realPkg := range pkgs {
|
||||
vars := conf.GlobalRewrites[realPkg]
|
||||
if vars == nil {
|
||||
vars = make(Rewrites)
|
||||
conf.GlobalRewrites[realPkg] = vars
|
||||
}
|
||||
conf.GlobalDatas[name] = value
|
||||
if skipIfExists {
|
||||
if _, exists := vars[varName]; exists {
|
||||
continue
|
||||
}
|
||||
}
|
||||
vars[varName] = value
|
||||
}
|
||||
}
|
||||
|
||||
func createGlobals(ctx *context, prog llssa.Program, pkgs []*aPackage) (llssa.Package, error) {
|
||||
if len(ctx.buildConf.GlobalDatas) == 0 {
|
||||
return nil, nil
|
||||
func validateRewriteInput(pkg, varName, value string) {
|
||||
if pkg == "" || strings.ContainsAny(pkg, " \t\r\n") {
|
||||
panic(fmt.Errorf("invalid package path for rewrite: %q", pkg))
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if !token.IsIdentifier(varName) {
|
||||
panic(fmt.Errorf("invalid variable name for rewrite: %q", varName))
|
||||
}
|
||||
global := prog.NewPackage("", "global")
|
||||
for name, value := range ctx.buildConf.GlobalDatas {
|
||||
global.AddGlobalString(name, value)
|
||||
if len(value) > maxRewriteValueLength {
|
||||
panic(fmt.Errorf("rewrite value too large: %d bytes", len(value)))
|
||||
}
|
||||
return global, nil
|
||||
}
|
||||
|
||||
// compileExtraFiles compiles extra files (.s/.c) from target configuration and returns object files
|
||||
@@ -752,7 +777,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
|
||||
@@ -769,7 +794,6 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
|
||||
if p.ExportFile != "" && aPkg != nil { // skip packages that only contain declarations
|
||||
linkArgs = append(linkArgs, aPkg.LinkArgs...)
|
||||
objFiles = append(objFiles, aPkg.LLFiles...)
|
||||
objFiles = append(objFiles, aPkg.ExportFile)
|
||||
need1, need2 := isNeedRuntimeOrPyInit(ctx, p)
|
||||
if !needRuntime {
|
||||
needRuntime = need1
|
||||
@@ -794,14 +818,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
|
||||
@@ -938,7 +954,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)
|
||||
@@ -965,10 +981,11 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error {
|
||||
aPkg.LinkArgs = append(aPkg.LinkArgs, altLdflags...)
|
||||
}
|
||||
if pkg.ExportFile != "" {
|
||||
pkg.ExportFile, err = exportObject(ctx, pkg.PkgPath, pkg.ExportFile, []byte(ret.String()))
|
||||
exportFile, err := exportObject(ctx, pkg.PkgPath, pkg.ExportFile, []byte(ret.String()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("export object of %v failed: %v", pkgPath, err)
|
||||
}
|
||||
aPkg.LLFiles = append(aPkg.LLFiles, exportFile)
|
||||
if debugBuild || verbose {
|
||||
fmt.Fprintf(os.Stderr, "==> Export %s: %s\n", aPkg.PkgPath, pkg.ExportFile)
|
||||
}
|
||||
@@ -977,11 +994,15 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error {
|
||||
}
|
||||
|
||||
func exportObject(ctx *context, pkgPath string, exportFile string, data []byte) (string, error) {
|
||||
f, err := os.CreateTemp("", "llgo-*.ll")
|
||||
base := filepath.Base(exportFile)
|
||||
f, err := os.CreateTemp("", base+"-*.ll")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
f.Write(data)
|
||||
if _, err := f.Write(data); err != nil {
|
||||
f.Close()
|
||||
return "", err
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return exportFile, err
|
||||
@@ -999,13 +1020,17 @@ func exportObject(ctx *context, pkgPath string, exportFile string, data []byte)
|
||||
}
|
||||
return exportFile, os.Rename(f.Name(), exportFile)
|
||||
}
|
||||
exportFile += ".o"
|
||||
args := []string{"-o", exportFile, "-c", f.Name(), "-Wno-override-module"}
|
||||
objFile, err := os.CreateTemp("", base+"-*.o")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
objFile.Close()
|
||||
args := []string{"-o", objFile.Name(), "-c", f.Name(), "-Wno-override-module"}
|
||||
if ctx.buildConf.Verbose {
|
||||
fmt.Fprintln(os.Stderr, "clang", args)
|
||||
}
|
||||
cmd := ctx.compiler()
|
||||
return exportFile, cmd.Compile(args...)
|
||||
return objFile.Name(), cmd.Compile(args...)
|
||||
}
|
||||
|
||||
func llcCheck(env *llvm.Env, exportFile string) (msg string, err error) {
|
||||
@@ -1059,8 +1084,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
|
||||
@@ -1081,7 +1107,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)
|
||||
}
|
||||
@@ -1089,6 +1116,32 @@ func allPkgs(ctx *context, initial []*packages.Package, verbose bool) (all []*aP
|
||||
return
|
||||
}
|
||||
|
||||
func collectRewriteVars(ctx *context, pkgPath string) map[string]string {
|
||||
data := ctx.buildConf.GlobalRewrites
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
basePath := strings.TrimPrefix(pkgPath, altPkgPathPrefix)
|
||||
if vars := data[basePath]; vars != nil {
|
||||
return cloneRewrites(vars)
|
||||
}
|
||||
if vars := data[pkgPath]; vars != nil {
|
||||
return cloneRewrites(vars)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloneRewrites(src Rewrites) map[string]string {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
dup := make(map[string]string, len(src))
|
||||
for k, v := range src {
|
||||
dup[k] = v
|
||||
}
|
||||
return dup
|
||||
}
|
||||
|
||||
func createSSAPkg(prog *ssa.Program, p *packages.Package, verbose bool) *ssa.Package {
|
||||
pkgSSA := prog.ImportedPackage(p.ID)
|
||||
if pkgSSA == nil {
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/goplus/llgo/internal/mockable"
|
||||
@@ -94,4 +97,70 @@ 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)
|
||||
}
|
||||
|
||||
func TestRunPrintfWithStdioNobuf(t *testing.T) {
|
||||
t.Setenv(llgoStdioNobuf, "1")
|
||||
mockRun([]string{"../../cl/_testdata/printf"}, &Config{Mode: ModeRun})
|
||||
}
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Package build contains the llgo compiler build orchestration logic.
|
||||
//
|
||||
// The main_module.go file generates the entry point module for llgo programs,
|
||||
// which contains the main() function, initialization sequence, and global
|
||||
// variables like argc/argv. This module is generated differently depending on
|
||||
// BuildMode (exe, c-archive, c-shared).
|
||||
|
||||
package build
|
||||
|
||||
import (
|
||||
@@ -29,6 +36,11 @@ import (
|
||||
llssa "github.com/goplus/llgo/ssa"
|
||||
)
|
||||
|
||||
// genMainModule generates the main entry module for an llgo program.
|
||||
//
|
||||
// The module contains argc/argv globals and, for executable build modes,
|
||||
// the entry function that wires initialization and main. For C archive or
|
||||
// shared library modes, only the globals are emitted.
|
||||
func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, needRuntime, needPyInit bool) Package {
|
||||
prog := ctx.prog
|
||||
mainPkg := prog.NewPackage("", pkg.ID+".main")
|
||||
@@ -57,7 +69,7 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, needRu
|
||||
}
|
||||
|
||||
runtimeStub := defineWeakNoArgStub(mainPkg, "runtime.init")
|
||||
// Define syscall.init as a weak stub to allow linking even when syscall package is not imported
|
||||
// TODO(lijie): workaround for syscall patch
|
||||
defineWeakNoArgStub(mainPkg, "syscall.init")
|
||||
|
||||
var pyInit llssa.Function
|
||||
@@ -82,6 +94,13 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, needRu
|
||||
return mainAPkg
|
||||
}
|
||||
|
||||
// defineEntryFunction creates the program's entry function. The name is
|
||||
// "main" for standard targets, or "__main_argc_argv" with hidden visibility
|
||||
// for WASM targets that don't require _start.
|
||||
//
|
||||
// The entry stores argc/argv, optionally disables stdio buffering, runs
|
||||
// initialization hooks (Python, runtime, package init), and finally calls
|
||||
// main.main before returning 0.
|
||||
func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa.Global, argvType llssa.Type, runtimeStub, mainInit, mainMain llssa.Function, pyInit, rtInit llssa.Function) llssa.Function {
|
||||
prog := pkg.Prog
|
||||
entryName := "main"
|
||||
@@ -99,7 +118,7 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa
|
||||
b.Store(argcVar.Expr, fn.Param(0))
|
||||
b.Store(argvVar.Expr, fn.Param(1))
|
||||
if IsStdioNobuf() {
|
||||
emitStdioNobuf(b, pkg, ctx.buildConf.Goarch)
|
||||
emitStdioNobuf(b, pkg, ctx.buildConf.Goos)
|
||||
}
|
||||
if pyInit != nil {
|
||||
b.Call(pyInit.Expr)
|
||||
@@ -135,44 +154,43 @@ func defineWeakNoArgStub(pkg llssa.Package, name string) llssa.Function {
|
||||
return fn
|
||||
}
|
||||
|
||||
func emitStdioNobuf(b llssa.Builder, pkg llssa.Package, goarch string) {
|
||||
const (
|
||||
// ioNoBuf represents the _IONBF flag for setvbuf (no buffering)
|
||||
ioNoBuf = 2
|
||||
)
|
||||
|
||||
// emitStdioNobuf generates code to disable buffering on stdout and stderr
|
||||
// when the LLGO_STDIO_NOBUF environment variable is set. Only Darwin uses
|
||||
// the alternate `__stdoutp`/`__stderrp` symbols; other targets rely on the
|
||||
// standard `stdout`/`stderr` globals.
|
||||
func emitStdioNobuf(b llssa.Builder, pkg llssa.Package, goos string) {
|
||||
prog := pkg.Prog
|
||||
streamType := prog.VoidPtr()
|
||||
streamPtrType := prog.Pointer(streamType)
|
||||
stdout := declareExternalPtrGlobal(pkg, "stdout", streamType)
|
||||
stderr := declareExternalPtrGlobal(pkg, "stderr", streamType)
|
||||
stdoutAlt := declareExternalPtrGlobal(pkg, "__stdout", streamType)
|
||||
stderrAlt := declareExternalPtrGlobal(pkg, "__stderr", streamType)
|
||||
|
||||
stdoutName := "stdout"
|
||||
stderrName := "stderr"
|
||||
if goos == "darwin" {
|
||||
stdoutName = "__stdoutp"
|
||||
stderrName = "__stderrp"
|
||||
}
|
||||
stdout := declareExternalPtrGlobal(pkg, stdoutName, streamPtrType)
|
||||
stderr := declareExternalPtrGlobal(pkg, stderrName, streamPtrType)
|
||||
stdoutPtr := b.Load(stdout)
|
||||
stderrPtr := b.Load(stderr)
|
||||
sizeType := prog.Uintptr()
|
||||
setvbuf := declareSetvbuf(pkg, streamPtrType, prog.CStr(), prog.Int32(), sizeType)
|
||||
|
||||
stdoutSlot := b.AllocaT(streamPtrType)
|
||||
b.Store(stdoutSlot, stdout)
|
||||
condOut := b.BinOp(token.EQL, stdout, prog.Nil(streamPtrType))
|
||||
b.IfThen(condOut, func() {
|
||||
b.Store(stdoutSlot, stdoutAlt)
|
||||
})
|
||||
stdoutPtr := b.Load(stdoutSlot)
|
||||
|
||||
stderrSlot := b.AllocaT(streamPtrType)
|
||||
b.Store(stderrSlot, stderr)
|
||||
condErr := b.BinOp(token.EQL, stderr, prog.Nil(streamPtrType))
|
||||
b.IfThen(condErr, func() {
|
||||
b.Store(stderrSlot, stderrAlt)
|
||||
})
|
||||
stderrPtr := b.Load(stderrSlot)
|
||||
|
||||
mode := prog.IntVal(2, prog.Int32())
|
||||
noBufMode := prog.IntVal(ioNoBuf, prog.Int32())
|
||||
zeroSize := prog.Zero(sizeType)
|
||||
nullBuf := prog.Nil(prog.CStr())
|
||||
|
||||
b.Call(setvbuf.Expr, stdoutPtr, nullBuf, mode, zeroSize)
|
||||
b.Call(setvbuf.Expr, stderrPtr, nullBuf, mode, zeroSize)
|
||||
b.Call(setvbuf.Expr, stdoutPtr, nullBuf, noBufMode, zeroSize)
|
||||
b.Call(setvbuf.Expr, stderrPtr, nullBuf, noBufMode, zeroSize)
|
||||
}
|
||||
|
||||
func declareExternalPtrGlobal(pkg llssa.Package, name string, valueType llssa.Type) llssa.Expr {
|
||||
ptrType := pkg.Prog.Pointer(valueType)
|
||||
global := pkg.NewVarEx(name, ptrType)
|
||||
global := pkg.NewVarEx(name, valueType)
|
||||
pkg.Module().NamedGlobal(name).SetLinkage(llvm.ExternalLinkage)
|
||||
return global.Expr
|
||||
}
|
||||
|
||||
109
internal/build/resolver.go
Normal file
109
internal/build/resolver.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package build
|
||||
|
||||
import "strings"
|
||||
|
||||
// nameResolver maps symbol names to aggregation buckets based on the requested level.
|
||||
type nameResolver struct {
|
||||
level string
|
||||
pkgs []Package
|
||||
moduleMap map[string]string
|
||||
packageMap map[string]string
|
||||
}
|
||||
|
||||
func newNameResolver(level string, pkgs []Package) *nameResolver {
|
||||
lvl := strings.ToLower(strings.TrimSpace(level))
|
||||
if lvl == "" {
|
||||
lvl = "module"
|
||||
}
|
||||
return &nameResolver{
|
||||
level: lvl,
|
||||
pkgs: pkgs,
|
||||
moduleMap: make(map[string]string),
|
||||
packageMap: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *nameResolver) resolve(sym string) string {
|
||||
base := moduleNameFromSymbol(sym)
|
||||
symbol := trimSymbolForMatch(sym)
|
||||
switch r.level {
|
||||
case "full":
|
||||
return base
|
||||
case "package":
|
||||
if pkg := r.matchPackage(symbol); pkg != "" {
|
||||
return pkg
|
||||
}
|
||||
case "module":
|
||||
if mod := r.matchModule(symbol); mod != "" {
|
||||
return mod
|
||||
}
|
||||
}
|
||||
if strings.Contains(symbol, "llgo") {
|
||||
return "llgo-stubs"
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func (r *nameResolver) matchPackage(symbol string) string {
|
||||
if symbol == "" {
|
||||
return ""
|
||||
}
|
||||
if cached := r.packageMap[symbol]; cached != "" {
|
||||
return cached
|
||||
}
|
||||
for _, pkg := range r.pkgs {
|
||||
if pkg == nil || pkg.Package == nil {
|
||||
continue
|
||||
}
|
||||
id := pkg.PkgPath
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(symbol, id+".") {
|
||||
r.packageMap[symbol] = id
|
||||
return id
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *nameResolver) matchModule(symbol string) string {
|
||||
if symbol == "" {
|
||||
return ""
|
||||
}
|
||||
if cached := r.moduleMap[symbol]; cached != "" {
|
||||
return cached
|
||||
}
|
||||
for _, pkg := range r.pkgs {
|
||||
if pkg == nil || pkg.Package == nil {
|
||||
continue
|
||||
}
|
||||
path := pkg.PkgPath
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(symbol, path+".") {
|
||||
mod := path
|
||||
if pkg.Module != nil && pkg.Module.Path != "" {
|
||||
mod = pkg.Module.Path
|
||||
}
|
||||
r.moduleMap[symbol] = mod
|
||||
return mod
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func trimSymbolForMatch(sym string) string {
|
||||
name := strings.TrimSpace(sym)
|
||||
for len(name) > 0 && (name[0] == '_' || name[0] == '.') {
|
||||
name = name[1:]
|
||||
}
|
||||
if idx := strings.Index(name, " "); idx >= 0 {
|
||||
name = name[:idx]
|
||||
}
|
||||
if idx := strings.Index(name, "@"); idx >= 0 {
|
||||
name = name[:idx]
|
||||
}
|
||||
return name
|
||||
}
|
||||
627
internal/build/size_report.go
Normal file
627
internal/build/size_report.go
Normal file
@@ -0,0 +1,627 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goplus/llgo/xtool/env/llvm"
|
||||
)
|
||||
|
||||
type sectionKind int
|
||||
|
||||
const (
|
||||
sectionUnknown sectionKind = iota
|
||||
sectionText
|
||||
sectionROData
|
||||
sectionData
|
||||
sectionBSS
|
||||
)
|
||||
|
||||
const (
|
||||
// readelfInitialBuffer is the initial buffer size for reading readelf output.
|
||||
// Most lines in readelf output are less than 1KB.
|
||||
readelfInitialBuffer = 64 * 1024
|
||||
|
||||
// readelfMaxBuffer is the maximum buffer size to handle very long symbol names
|
||||
// or section dumps. Reduced from 64MB to prevent excessive memory consumption
|
||||
// while still accommodating reasonably large binaries.
|
||||
readelfMaxBuffer = 4 * 1024 * 1024
|
||||
)
|
||||
|
||||
// ELF special section indices (from ELF specification)
|
||||
const (
|
||||
SHN_UNDEF = 0x0000 // Undefined section
|
||||
SHN_LORESERVE = 0xFF00 // Start of reserved indices
|
||||
SHN_ABS = 0xFFF1 // Absolute values
|
||||
SHN_COMMON = 0xFFF2 // Common symbols
|
||||
SHN_XINDEX = 0xFFFF // Escape value for extended section indices
|
||||
)
|
||||
|
||||
type sectionInfo struct {
|
||||
Index int
|
||||
Name string
|
||||
Segment string
|
||||
Address uint64
|
||||
Size uint64
|
||||
Kind sectionKind
|
||||
}
|
||||
|
||||
type symbolInfo struct {
|
||||
Name string
|
||||
SectionIndex int
|
||||
Address uint64
|
||||
}
|
||||
|
||||
type readelfData struct {
|
||||
sections map[int]*sectionInfo
|
||||
symbols map[int][]symbolInfo
|
||||
}
|
||||
|
||||
type moduleSize struct {
|
||||
Name string
|
||||
Code uint64
|
||||
ROData uint64
|
||||
Data uint64
|
||||
BSS uint64
|
||||
}
|
||||
|
||||
func (m *moduleSize) Flash() uint64 {
|
||||
return m.Code + m.ROData + m.Data
|
||||
}
|
||||
|
||||
func (m *moduleSize) RAM() uint64 {
|
||||
return m.Data + m.BSS
|
||||
}
|
||||
|
||||
type sizeReport struct {
|
||||
Binary string
|
||||
Modules map[string]*moduleSize
|
||||
Total moduleSize
|
||||
}
|
||||
|
||||
func (r *sizeReport) module(name string) *moduleSize {
|
||||
if name == "" {
|
||||
name = "(anonymous)"
|
||||
}
|
||||
if r.Modules == nil {
|
||||
r.Modules = make(map[string]*moduleSize)
|
||||
}
|
||||
m, ok := r.Modules[name]
|
||||
if !ok {
|
||||
m = &moduleSize{Name: name}
|
||||
r.Modules[name] = m
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (r *sizeReport) add(name string, kind sectionKind, size uint64) {
|
||||
if size == 0 {
|
||||
return
|
||||
}
|
||||
m := r.module(name)
|
||||
switch kind {
|
||||
case sectionText:
|
||||
m.Code += size
|
||||
r.Total.Code += size
|
||||
case sectionROData:
|
||||
m.ROData += size
|
||||
r.Total.ROData += size
|
||||
case sectionData:
|
||||
m.Data += size
|
||||
r.Total.Data += size
|
||||
case sectionBSS:
|
||||
m.BSS += size
|
||||
r.Total.BSS += size
|
||||
}
|
||||
}
|
||||
|
||||
func reportBinarySize(path, format, level string, pkgs []Package) error {
|
||||
report, err := collectBinarySize(path, pkgs, level)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch format {
|
||||
case "", "text":
|
||||
printTextReport(os.Stdout, report)
|
||||
case "json":
|
||||
return emitJSONReport(os.Stdout, report)
|
||||
default:
|
||||
return fmt.Errorf("unknown size format %q (valid: text,json)", format)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectBinarySize(path string, pkgs []Package, level string) (*sizeReport, error) {
|
||||
cmd, err := llvm.New("").Readelf("--elf-output-style=LLVM", "--all", path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("llvm-readelf: %w", err)
|
||||
}
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("llvm-readelf stdout: %w", err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("failed to execute llvm-readelf: %w", err)
|
||||
}
|
||||
parsed, parseErr := parseReadelfOutput(stdout)
|
||||
closeErr := stdout.Close()
|
||||
waitErr := cmd.Wait()
|
||||
if waitErr != nil {
|
||||
return nil, fmt.Errorf("llvm-readelf failed: %w\n%s", waitErr, stderr.String())
|
||||
}
|
||||
if parseErr != nil {
|
||||
return nil, fmt.Errorf("parsing llvm-readelf output failed: %w", parseErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
return nil, fmt.Errorf("closing llvm-readelf stdout pipe failed: %w", closeErr)
|
||||
}
|
||||
report := buildSizeReport(path, parsed, pkgs, level)
|
||||
if report == nil || len(report.Modules) == 0 {
|
||||
return nil, fmt.Errorf("size report: no allocatable sections found in %s", path)
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func parseReadelfOutput(r io.Reader) (*readelfData, error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Buffer(make([]byte, 0, readelfInitialBuffer), readelfMaxBuffer)
|
||||
|
||||
type ctxKind int
|
||||
const (
|
||||
ctxRoot ctxKind = iota
|
||||
ctxSections
|
||||
ctxSection
|
||||
ctxSymbols
|
||||
ctxSymbol
|
||||
)
|
||||
|
||||
type ctx struct {
|
||||
kind ctxKind
|
||||
indent int
|
||||
}
|
||||
|
||||
stack := []ctx{{kind: ctxRoot, indent: -1}}
|
||||
push := func(kind ctxKind, indent int) {
|
||||
stack = append(stack, ctx{kind: kind, indent: indent})
|
||||
}
|
||||
pop := func(expected ctxKind, indent int) bool {
|
||||
top := stack[len(stack)-1]
|
||||
if top.kind != expected || top.indent != indent {
|
||||
return false
|
||||
}
|
||||
stack = stack[:len(stack)-1]
|
||||
return true
|
||||
}
|
||||
current := func() ctx {
|
||||
return stack[len(stack)-1]
|
||||
}
|
||||
|
||||
data := &readelfData{
|
||||
sections: make(map[int]*sectionInfo),
|
||||
symbols: make(map[int][]symbolInfo),
|
||||
}
|
||||
|
||||
// readelf outputs section references differently:
|
||||
// - Mach-O: section numbers are 1-based in symbol references
|
||||
// - ELF: section numbers in symbol references match the Index directly
|
||||
secIndexBase := 1 // default to Mach-O behavior; switch to 0 for ELF once detected
|
||||
|
||||
var currentSection *sectionInfo
|
||||
var currentSymbol *symbolInfo
|
||||
|
||||
for scanner.Scan() {
|
||||
raw := scanner.Text()
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect object format early to adjust section index base
|
||||
if strings.HasPrefix(trimmed, "Format:") {
|
||||
lower := strings.ToLower(trimmed)
|
||||
if strings.Contains(lower, "mach-o") {
|
||||
secIndexBase = 1
|
||||
} else if strings.Contains(lower, "elf") {
|
||||
secIndexBase = 0
|
||||
}
|
||||
}
|
||||
indent := countLeadingSpaces(raw)
|
||||
top := current()
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(trimmed, "Sections [") && top.kind == ctxRoot:
|
||||
push(ctxSections, indent)
|
||||
continue
|
||||
case strings.HasPrefix(trimmed, "Symbols [") && top.kind == ctxRoot:
|
||||
push(ctxSymbols, indent)
|
||||
continue
|
||||
case trimmed == "Section {" && top.kind == ctxSections && indent == top.indent+2:
|
||||
currentSection = §ionInfo{Index: -1}
|
||||
push(ctxSection, indent)
|
||||
continue
|
||||
case trimmed == "Symbol {" && top.kind == ctxSymbols && indent == top.indent+2:
|
||||
currentSymbol = &symbolInfo{SectionIndex: -1}
|
||||
push(ctxSymbol, indent)
|
||||
continue
|
||||
case trimmed == "}" && pop(ctxSection, indent):
|
||||
if currentSection != nil && currentSection.Index >= 0 {
|
||||
currentSection.Kind = classifySection(currentSection.Name, currentSection.Segment)
|
||||
data.sections[currentSection.Index] = currentSection
|
||||
}
|
||||
currentSection = nil
|
||||
continue
|
||||
case trimmed == "}" && pop(ctxSymbol, indent):
|
||||
if currentSymbol != nil && currentSymbol.SectionIndex >= 0 {
|
||||
data.symbols[currentSymbol.SectionIndex] = append(data.symbols[currentSymbol.SectionIndex], *currentSymbol)
|
||||
}
|
||||
currentSymbol = nil
|
||||
continue
|
||||
case trimmed == "]" && (top.kind == ctxSections || top.kind == ctxSymbols) && indent == top.indent:
|
||||
stack = stack[:len(stack)-1]
|
||||
continue
|
||||
}
|
||||
|
||||
switch top.kind {
|
||||
case ctxSection:
|
||||
if currentSection == nil {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(trimmed, "Index: "):
|
||||
if idx, err := strconv.Atoi(strings.TrimSpace(trimmed[len("Index: "):])); err == nil {
|
||||
currentSection.Index = idx
|
||||
}
|
||||
case strings.HasPrefix(trimmed, "Name: "):
|
||||
currentSection.Name = parseNameField(trimmed[len("Name: "):])
|
||||
case strings.HasPrefix(trimmed, "Segment: "):
|
||||
currentSection.Segment = parseNameField(trimmed[len("Segment: "):])
|
||||
case strings.HasPrefix(trimmed, "Address: "):
|
||||
if val, err := parseUintField(trimmed[len("Address: "):]); err == nil {
|
||||
currentSection.Address = val
|
||||
}
|
||||
case strings.HasPrefix(trimmed, "Size: "):
|
||||
if val, err := parseUintField(trimmed[len("Size: "):]); err == nil {
|
||||
currentSection.Size = val
|
||||
}
|
||||
}
|
||||
case ctxSymbol:
|
||||
if currentSymbol == nil {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(trimmed, "Name: "):
|
||||
currentSymbol.Name = parseNameField(trimmed[len("Name: "):])
|
||||
case strings.HasPrefix(trimmed, "Section: "):
|
||||
name, idx := parseSectionRef(trimmed[len("Section: "):], secIndexBase)
|
||||
currentSymbol.SectionIndex = idx
|
||||
if currentSymbol.Name == "" {
|
||||
currentSymbol.Name = name
|
||||
}
|
||||
case strings.HasPrefix(trimmed, "Value: "):
|
||||
if val, err := parseUintField(trimmed[len("Value: "):]); err == nil {
|
||||
currentSymbol.Address = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func countLeadingSpaces(line string) int {
|
||||
count := 0
|
||||
for _, ch := range line {
|
||||
if ch != ' ' {
|
||||
break
|
||||
}
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func classifySection(name, segment string) sectionKind {
|
||||
ln := strings.ToLower(name)
|
||||
ls := strings.ToLower(segment)
|
||||
switch {
|
||||
case strings.Contains(ln, "text"), strings.Contains(ln, "code"), strings.Contains(ln, "plt"):
|
||||
return sectionText
|
||||
case strings.Contains(ln, "rodata"), strings.Contains(ln, "const"), strings.Contains(ln, "literal"), strings.Contains(ln, "cstring"):
|
||||
return sectionROData
|
||||
case strings.Contains(ln, "bss"), strings.Contains(ln, "tbss"), strings.Contains(ln, "sbss"), strings.Contains(ln, "common"), strings.Contains(ln, "zerofill"):
|
||||
return sectionBSS
|
||||
case strings.Contains(ln, "data"), strings.Contains(ln, "got"), strings.Contains(ln, "init_array"), strings.Contains(ln, "cfstring"), strings.Contains(ln, "tdata"):
|
||||
return sectionData
|
||||
}
|
||||
switch {
|
||||
case strings.Contains(ls, "__text"):
|
||||
return sectionText
|
||||
case strings.Contains(ls, "data_const"):
|
||||
return sectionROData
|
||||
case strings.Contains(ls, "__data"):
|
||||
return sectionData
|
||||
}
|
||||
return sectionUnknown
|
||||
}
|
||||
|
||||
func buildSizeReport(path string, data *readelfData, pkgs []Package, level string) *sizeReport {
|
||||
report := &sizeReport{Binary: path, Modules: make(map[string]*moduleSize)}
|
||||
if data == nil {
|
||||
return report
|
||||
}
|
||||
res := newNameResolver(level, pkgs)
|
||||
var recognized bool
|
||||
for idx, sec := range data.sections {
|
||||
if sec == nil || sec.Size == 0 {
|
||||
continue
|
||||
}
|
||||
if sec.Kind == sectionUnknown {
|
||||
continue
|
||||
}
|
||||
recognized = true
|
||||
end := sec.Address + sec.Size
|
||||
syms := data.symbols[idx]
|
||||
if len(syms) == 0 {
|
||||
report.add("(unknown "+sec.Name+")", sec.Kind, sec.Size)
|
||||
continue
|
||||
}
|
||||
// Sort symbols by address to calculate sizes based on address ranges
|
||||
sort.Slice(syms, func(i, j int) bool {
|
||||
if syms[i].Address == syms[j].Address {
|
||||
return syms[i].Name < syms[j].Name
|
||||
}
|
||||
return syms[i].Address < syms[j].Address
|
||||
})
|
||||
cursor := sec.Address
|
||||
for i := 0; i < len(syms); i++ {
|
||||
sym := syms[i]
|
||||
// Skip symbols that are beyond the section bounds
|
||||
if sym.Address >= end {
|
||||
continue
|
||||
}
|
||||
addr := sym.Address
|
||||
// Clamp symbol address to section start if it's before the section
|
||||
if addr < sec.Address {
|
||||
addr = sec.Address
|
||||
}
|
||||
// Add padding bytes between cursor and current symbol
|
||||
if addr > cursor {
|
||||
report.add("(padding "+sec.Name+")", sec.Kind, addr-cursor)
|
||||
cursor = addr
|
||||
}
|
||||
// Find the next symbol address to calculate this symbol's size.
|
||||
// Symbols at the same address are handled by taking the next different address.
|
||||
next := end
|
||||
// Optimize: check next symbol first before scanning
|
||||
if i+1 < len(syms) && syms[i+1].Address > addr {
|
||||
next = syms[i+1].Address
|
||||
} else {
|
||||
// Only search if next symbol is at same address
|
||||
for j := i + 1; j < len(syms); j++ {
|
||||
if syms[j].Address > addr {
|
||||
next = syms[j].Address
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if next > end {
|
||||
next = end
|
||||
}
|
||||
// Skip symbols with zero size
|
||||
if next <= addr {
|
||||
continue
|
||||
}
|
||||
// Attribute the address range [addr, next) to the symbol's module
|
||||
mod := res.resolve(sym.Name)
|
||||
report.add(mod, sec.Kind, next-addr)
|
||||
cursor = next
|
||||
}
|
||||
// Add any remaining padding at the end of the section
|
||||
if cursor < end {
|
||||
report.add("(padding "+sec.Name+")", sec.Kind, end-cursor)
|
||||
}
|
||||
}
|
||||
if !recognized {
|
||||
return nil
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
func emitJSONReport(w io.Writer, report *sizeReport) error {
|
||||
type moduleJSON struct {
|
||||
Name string `json:"name"`
|
||||
Code uint64 `json:"code"`
|
||||
ROData uint64 `json:"rodata"`
|
||||
Data uint64 `json:"data"`
|
||||
BSS uint64 `json:"bss"`
|
||||
Flash uint64 `json:"flash"`
|
||||
RAM uint64 `json:"ram"`
|
||||
}
|
||||
mods := report.sortedModules()
|
||||
jsonMods := make([]moduleJSON, 0, len(mods))
|
||||
for _, m := range mods {
|
||||
jsonMods = append(jsonMods, moduleJSON{
|
||||
Name: m.Name,
|
||||
Code: m.Code,
|
||||
ROData: m.ROData,
|
||||
Data: m.Data,
|
||||
BSS: m.BSS,
|
||||
Flash: m.Flash(),
|
||||
RAM: m.RAM(),
|
||||
})
|
||||
}
|
||||
payload := struct {
|
||||
Binary string `json:"binary"`
|
||||
Modules []moduleJSON `json:"modules"`
|
||||
Total moduleJSON `json:"total"`
|
||||
}{
|
||||
Binary: filepath.Clean(report.Binary),
|
||||
Modules: jsonMods,
|
||||
Total: moduleJSON{
|
||||
Name: "total",
|
||||
Code: report.Total.Code,
|
||||
ROData: report.Total.ROData,
|
||||
Data: report.Total.Data,
|
||||
BSS: report.Total.BSS,
|
||||
Flash: report.Total.Flash(),
|
||||
RAM: report.Total.RAM(),
|
||||
},
|
||||
}
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(payload)
|
||||
}
|
||||
|
||||
func printTextReport(w io.Writer, report *sizeReport) {
|
||||
fmt.Fprintf(w, "\nSize report for %s\n", filepath.Clean(report.Binary))
|
||||
fmt.Fprintln(w, " code rodata data bss | flash ram | module")
|
||||
fmt.Fprintln(w, "------------------------------- | --------------- | ----------------")
|
||||
for _, m := range report.sortedModules() {
|
||||
fmt.Fprintf(w, "%7d %7d %7d %7d | %7d %7d | %s\n", m.Code, m.ROData, m.Data, m.BSS, m.Flash(), m.RAM(), m.Name)
|
||||
}
|
||||
fmt.Fprintln(w, "------------------------------- | --------------- | ----------------")
|
||||
fmt.Fprintf(w, "%7d %7d %7d %7d | %7d %7d | total\n", report.Total.Code, report.Total.ROData, report.Total.Data, report.Total.BSS, report.Total.Flash(), report.Total.RAM())
|
||||
}
|
||||
|
||||
func (r *sizeReport) sortedModules() []*moduleSize {
|
||||
mods := make([]*moduleSize, 0, len(r.Modules))
|
||||
for _, m := range r.Modules {
|
||||
mods = append(mods, m)
|
||||
}
|
||||
sort.Slice(mods, func(i, j int) bool {
|
||||
if mods[i].Flash() == mods[j].Flash() {
|
||||
return mods[i].Name < mods[j].Name
|
||||
}
|
||||
return mods[i].Flash() > mods[j].Flash()
|
||||
})
|
||||
return mods
|
||||
}
|
||||
|
||||
// moduleNameFromSymbol extracts the Go package name from a symbol name.
|
||||
// It handles various symbol naming conventions:
|
||||
// - C symbols: Strip leading underscore (e.g., "_main" -> "main")
|
||||
// - Assembler symbols: Strip leading dot (e.g., ".text" -> "text")
|
||||
// - Versioned symbols: Remove version suffix (e.g., "symbol@@GLIBC_2.2.5" -> "symbol")
|
||||
// - Go symbols: Extract package from "package.symbol" format
|
||||
// - Generic types: Strip type parameters (e.g., "pkg(T)" -> "pkg")
|
||||
func moduleNameFromSymbol(raw string) string {
|
||||
name := strings.TrimSpace(raw)
|
||||
// Strip C symbol prefix
|
||||
name = strings.TrimPrefix(name, "_")
|
||||
// Strip assembler symbol prefix
|
||||
name = strings.TrimPrefix(name, ".")
|
||||
if name == "" {
|
||||
return "(anonymous)"
|
||||
}
|
||||
// Remove trailing attributes (e.g., "symbol (weak)")
|
||||
if idx := strings.Index(name, " "); idx > 0 {
|
||||
name = name[:idx]
|
||||
}
|
||||
// Remove version suffix for versioned symbols (e.g., "symbol@@GLIBC_2.2.5")
|
||||
if idx := strings.Index(name, "@"); idx > 0 {
|
||||
name = name[:idx]
|
||||
}
|
||||
// Extract Go package name from "package.symbol" format
|
||||
lastDot := strings.LastIndex(name, ".")
|
||||
if lastDot > 0 {
|
||||
pkg := name[:lastDot]
|
||||
// Strip generic type parameters (e.g., "slices.Sort[int]" -> "slices")
|
||||
if paren := strings.Index(pkg, "("); paren > 0 {
|
||||
pkg = pkg[:paren]
|
||||
}
|
||||
pkg = strings.Trim(pkg, " ")
|
||||
if pkg != "" {
|
||||
return pkg
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func parseNameField(field string) string {
|
||||
val := strings.TrimSpace(field)
|
||||
if idx := strings.Index(val, "("); idx >= 0 {
|
||||
val = strings.TrimSpace(val[:idx])
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func parseSectionRef(field string, indexBase int) (string, int) {
|
||||
name := parseNameField(field)
|
||||
idx := strings.Index(field, "(")
|
||||
if idx < 0 {
|
||||
return name, -1
|
||||
}
|
||||
end := strings.Index(field[idx:], ")")
|
||||
if end < 0 {
|
||||
return name, -1
|
||||
}
|
||||
val := strings.TrimSpace(field[idx+1 : idx+end])
|
||||
val = strings.TrimPrefix(val, "0x")
|
||||
if val == "" {
|
||||
return name, -1
|
||||
}
|
||||
num, err := strconv.ParseUint(val, 16, 64)
|
||||
if err != nil {
|
||||
return name, -1
|
||||
}
|
||||
if num == 0 {
|
||||
return name, -1
|
||||
}
|
||||
if indexBase == 0 && num >= SHN_LORESERVE {
|
||||
// Special ELF section indices (SHN_ABS, SHN_COMMON, etc.)
|
||||
return name, -1
|
||||
}
|
||||
if num > math.MaxInt {
|
||||
return name, -1
|
||||
}
|
||||
res := int(num) - indexBase
|
||||
if res < 0 {
|
||||
return name, -1
|
||||
}
|
||||
return name, res
|
||||
}
|
||||
|
||||
func parseUintField(field string) (uint64, error) {
|
||||
val := strings.TrimSpace(field)
|
||||
if strings.HasPrefix(val, "0x") || strings.HasPrefix(val, "0X") {
|
||||
return strconv.ParseUint(val[2:], 16, 64)
|
||||
}
|
||||
return strconv.ParseUint(val, 10, 64)
|
||||
}
|
||||
|
||||
func ensureSizeReporting(conf *Config) error {
|
||||
if !conf.SizeReport {
|
||||
return nil
|
||||
}
|
||||
switch strings.ToLower(conf.SizeLevel) {
|
||||
case "", "module":
|
||||
conf.SizeLevel = "module"
|
||||
case "package", "full":
|
||||
conf.SizeLevel = strings.ToLower(conf.SizeLevel)
|
||||
default:
|
||||
return fmt.Errorf("invalid size level %q (valid: full,module,package)", conf.SizeLevel)
|
||||
}
|
||||
cmd, err := llvm.New("").Readelf("--version")
|
||||
if err != nil {
|
||||
return fmt.Errorf("llvm-readelf not available: %w", err)
|
||||
}
|
||||
cmd.Stdout = io.Discard
|
||||
cmd.Stderr = io.Discard
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("llvm-readelf not available: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
156
internal/build/size_report_test.go
Normal file
156
internal/build/size_report_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
//go:build !llgo
|
||||
|
||||
package build
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
const sampleReadelf = `Sections [
|
||||
Section {
|
||||
Index: 0
|
||||
Name: __text (5F)
|
||||
Segment: __TEXT (5F)
|
||||
Address: 0x1000
|
||||
Size: 0x20
|
||||
}
|
||||
Section {
|
||||
Index: 1
|
||||
Name: __data
|
||||
Segment: __DATA
|
||||
Address: 0x2000
|
||||
Size: 0x10
|
||||
}
|
||||
Section {
|
||||
Index: 2
|
||||
Name: __common
|
||||
Segment: __DATA
|
||||
Address: 0x3000
|
||||
Size: 0x8
|
||||
}
|
||||
]
|
||||
Symbols [
|
||||
Symbol {
|
||||
Name: _main.main
|
||||
Section: __text (0x1)
|
||||
Value: 0x1000
|
||||
}
|
||||
Symbol {
|
||||
Name: _runtime.init
|
||||
Section: __text (0x1)
|
||||
Value: 0x1010
|
||||
}
|
||||
Symbol {
|
||||
Name: _main.dataVar
|
||||
Section: __data (0x2)
|
||||
Value: 0x2000
|
||||
}
|
||||
Symbol {
|
||||
Name: _runtime.dataVar
|
||||
Section: __data (0x2)
|
||||
Value: 0x2008
|
||||
}
|
||||
Symbol {
|
||||
Name: _runtime.bssVar
|
||||
Section: __common (0x3)
|
||||
Value: 0x3000
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
func TestParseReadelfOutput(t *testing.T) {
|
||||
parsed, err := parseReadelfOutput(strings.NewReader(sampleReadelf))
|
||||
if err != nil {
|
||||
t.Fatalf("parseReadelfOutput: %v", err)
|
||||
}
|
||||
report := buildSizeReport("fake.bin", parsed, nil, "")
|
||||
if report == nil {
|
||||
t.Fatal("expected report")
|
||||
}
|
||||
modules := report.Modules
|
||||
if len(modules) == 0 {
|
||||
t.Fatal("expected modules in report")
|
||||
}
|
||||
mainMod, ok := modules["main"]
|
||||
if !ok {
|
||||
t.Fatalf("expected main module, got %v", modules)
|
||||
}
|
||||
if mainMod.Code != 0x10 {
|
||||
t.Fatalf("unexpected main code size: %d", mainMod.Code)
|
||||
}
|
||||
if mainMod.Data != 0x8 {
|
||||
t.Fatalf("unexpected main data size: %d", mainMod.Data)
|
||||
}
|
||||
runtimeMod := modules["runtime"]
|
||||
if runtimeMod.Code != 0x10 {
|
||||
t.Fatalf("unexpected runtime code size: %d", runtimeMod.Code)
|
||||
}
|
||||
if runtimeMod.Data != 0x8 {
|
||||
t.Fatalf("unexpected runtime data size: %d", runtimeMod.Data)
|
||||
}
|
||||
if runtimeMod.BSS != 0x8 {
|
||||
t.Fatalf("unexpected runtime bss size: %d", runtimeMod.BSS)
|
||||
}
|
||||
if report.Total.Flash() != 0x10+0x10+0x8+0x8 {
|
||||
t.Fatalf("unexpected flash total: %d", report.Total.Flash())
|
||||
}
|
||||
if report.Total.RAM() != 0x8+0x8+0x8 {
|
||||
t.Fatalf("unexpected ram total: %d", report.Total.RAM())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseReadelfRealBinary(t *testing.T) {
|
||||
path := os.Getenv("LLGO_SIZE_REPORT_BIN")
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
t.Fatalf("abs path: %v", err)
|
||||
}
|
||||
cmd := exec.Command("llvm-readelf", "--all", absPath)
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("llvm-readelf failed: %v", err)
|
||||
}
|
||||
parsed, err := parseReadelfOutput(bytes.NewReader(buf.Bytes()))
|
||||
if err != nil {
|
||||
t.Fatalf("parseReadelfOutput(real): %v", err)
|
||||
}
|
||||
if len(parsed.sections) == 0 {
|
||||
t.Fatal("expected sections in real binary")
|
||||
}
|
||||
report := buildSizeReport(absPath, parsed, nil, "")
|
||||
if len(report.Modules) == 0 {
|
||||
t.Fatalf("expected modules for %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNameResolver(t *testing.T) {
|
||||
pkgs := []Package{
|
||||
&aPackage{Package: &packages.Package{PkgPath: "github.com/foo/bar", Module: &packages.Module{Path: "github.com/foo"}}},
|
||||
}
|
||||
symbol := "_github.com/foo/bar.Type.method"
|
||||
if got := newNameResolver("package", pkgs).resolve(symbol); got != "github.com/foo/bar" {
|
||||
t.Fatalf("package level want github.com/foo/bar, got %q", got)
|
||||
}
|
||||
if got := newNameResolver("module", pkgs).resolve(symbol); got != "github.com/foo" {
|
||||
t.Fatalf("module level want github.com/foo, got %q", got)
|
||||
}
|
||||
full := newNameResolver("full", pkgs).resolve(symbol)
|
||||
if full != "github.com/foo/bar.Type" {
|
||||
t.Fatalf("full level unexpected: %q", full)
|
||||
}
|
||||
if got := newNameResolver("package", nil).resolve("_llgo_stub.foo"); got != "llgo-stubs" {
|
||||
t.Fatalf("llgo default grouping failed: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !nogc
|
||||
// +build !nogc
|
||||
//go:build !nogc && !baremetal
|
||||
|
||||
/*
|
||||
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build nogc
|
||||
// +build nogc
|
||||
//go:build nogc || baremetal
|
||||
|
||||
/*
|
||||
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llgo
|
||||
//go:build llgo && !baremetal
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llgo && !nogc
|
||||
//go:build llgo && !baremetal && !nogc
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llgo && nogc
|
||||
//go:build llgo && (nogc || baremetal)
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !llgo
|
||||
//go:build !llgo || baremetal
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
package runtime
|
||||
|
||||
import "runtime"
|
||||
|
||||
// Layout of in-memory per-function information prepared by linker
|
||||
// See https://golang.org/s/go12symtab.
|
||||
// Keep in sync with linker (../cmd/link/internal/ld/pcln.go:/pclntab)
|
||||
@@ -30,10 +28,6 @@ func StopTrace() {
|
||||
panic("todo: runtime.StopTrace")
|
||||
}
|
||||
|
||||
func ReadMemStats(m *runtime.MemStats) {
|
||||
panic("todo: runtime.ReadMemStats")
|
||||
}
|
||||
|
||||
func SetMutexProfileFraction(rate int) int {
|
||||
panic("todo: runtime.SetMutexProfileFraction")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
//go:build !nogc
|
||||
//go:build !nogc && !baremetal
|
||||
|
||||
package runtime
|
||||
|
||||
import "github.com/goplus/llgo/runtime/internal/clite/bdwgc"
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/goplus/llgo/runtime/internal/clite/bdwgc"
|
||||
)
|
||||
|
||||
func ReadMemStats(m *runtime.MemStats) {
|
||||
panic("todo: runtime.ReadMemStats")
|
||||
}
|
||||
|
||||
func GC() {
|
||||
bdwgc.Gcollect()
|
||||
|
||||
29
runtime/internal/lib/runtime/runtime_gc_baremetal.go
Normal file
29
runtime/internal/lib/runtime/runtime_gc_baremetal.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build !nogc && baremetal
|
||||
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/goplus/llgo/runtime/internal/runtime/tinygogc"
|
||||
)
|
||||
|
||||
func ReadMemStats(m *runtime.MemStats) {
|
||||
stats := tinygogc.ReadGCStats()
|
||||
m.Alloc = stats.Alloc
|
||||
m.TotalAlloc = stats.TotalAlloc
|
||||
m.Sys = stats.Sys
|
||||
m.Mallocs = stats.Mallocs
|
||||
m.Frees = stats.Frees
|
||||
m.HeapAlloc = stats.HeapAlloc
|
||||
m.HeapSys = stats.HeapSys
|
||||
m.HeapIdle = stats.HeapIdle
|
||||
m.HeapInuse = stats.HeapInuse
|
||||
m.StackInuse = stats.StackInuse
|
||||
m.StackSys = stats.StackSys
|
||||
m.GCSys = stats.GCSys
|
||||
}
|
||||
|
||||
func GC() {
|
||||
tinygogc.GC()
|
||||
}
|
||||
184
runtime/internal/runtime/tinygogc/gc.go
Normal file
184
runtime/internal/runtime/tinygogc/gc.go
Normal file
@@ -0,0 +1,184 @@
|
||||
//go:build baremetal || testGC
|
||||
|
||||
package tinygogc
|
||||
|
||||
import "unsafe"
|
||||
|
||||
type GCStats struct {
|
||||
// General statistics.
|
||||
|
||||
// Alloc is bytes of allocated heap objects.
|
||||
//
|
||||
// This is the same as HeapAlloc (see below).
|
||||
Alloc uint64
|
||||
|
||||
// TotalAlloc is cumulative bytes allocated for heap objects.
|
||||
//
|
||||
// TotalAlloc increases as heap objects are allocated, but
|
||||
// unlike Alloc and HeapAlloc, it does not decrease when
|
||||
// objects are freed.
|
||||
TotalAlloc uint64
|
||||
|
||||
// Sys is the total bytes of memory obtained from the OS.
|
||||
//
|
||||
// Sys is the sum of the XSys fields below. Sys measures the
|
||||
// virtual address space reserved by the Go runtime for the
|
||||
// heap, stacks, and other internal data structures. It's
|
||||
// likely that not all of the virtual address space is backed
|
||||
// by physical memory at any given moment, though in general
|
||||
// it all was at some point.
|
||||
Sys uint64
|
||||
|
||||
// Mallocs is the cumulative count of heap objects allocated.
|
||||
// The number of live objects is Mallocs - Frees.
|
||||
Mallocs uint64
|
||||
|
||||
// Frees is the cumulative count of heap objects freed.
|
||||
Frees uint64
|
||||
|
||||
// Heap memory statistics.
|
||||
//
|
||||
// Interpreting the heap statistics requires some knowledge of
|
||||
// how Go organizes memory. Go divides the virtual address
|
||||
// space of the heap into "spans", which are contiguous
|
||||
// regions of memory 8K or larger. A span may be in one of
|
||||
// three states:
|
||||
//
|
||||
// An "idle" span contains no objects or other data. The
|
||||
// physical memory backing an idle span can be released back
|
||||
// to the OS (but the virtual address space never is), or it
|
||||
// can be converted into an "in use" or "stack" span.
|
||||
//
|
||||
// An "in use" span contains at least one heap object and may
|
||||
// have free space available to allocate more heap objects.
|
||||
//
|
||||
// A "stack" span is used for goroutine stacks. Stack spans
|
||||
// are not considered part of the heap. A span can change
|
||||
// between heap and stack memory; it is never used for both
|
||||
// simultaneously.
|
||||
|
||||
// HeapAlloc is bytes of allocated heap objects.
|
||||
//
|
||||
// "Allocated" heap objects include all reachable objects, as
|
||||
// well as unreachable objects that the garbage collector has
|
||||
// not yet freed. Specifically, HeapAlloc increases as heap
|
||||
// objects are allocated and decreases as the heap is swept
|
||||
// and unreachable objects are freed. Sweeping occurs
|
||||
// incrementally between GC cycles, so these two processes
|
||||
// occur simultaneously, and as a result HeapAlloc tends to
|
||||
// change smoothly (in contrast with the sawtooth that is
|
||||
// typical of stop-the-world garbage collectors).
|
||||
HeapAlloc uint64
|
||||
|
||||
// HeapSys is bytes of heap memory obtained from the OS.
|
||||
//
|
||||
// HeapSys measures the amount of virtual address space
|
||||
// reserved for the heap. This includes virtual address space
|
||||
// that has been reserved but not yet used, which consumes no
|
||||
// physical memory, but tends to be small, as well as virtual
|
||||
// address space for which the physical memory has been
|
||||
// returned to the OS after it became unused (see HeapReleased
|
||||
// for a measure of the latter).
|
||||
//
|
||||
// HeapSys estimates the largest size the heap has had.
|
||||
HeapSys uint64
|
||||
|
||||
// HeapIdle is bytes in idle (unused) spans.
|
||||
//
|
||||
// Idle spans have no objects in them. These spans could be
|
||||
// (and may already have been) returned to the OS, or they can
|
||||
// be reused for heap allocations, or they can be reused as
|
||||
// stack memory.
|
||||
//
|
||||
// HeapIdle minus HeapReleased estimates the amount of memory
|
||||
// that could be returned to the OS, but is being retained by
|
||||
// the runtime so it can grow the heap without requesting more
|
||||
// memory from the OS. If this difference is significantly
|
||||
// larger than the heap size, it indicates there was a recent
|
||||
// transient spike in live heap size.
|
||||
HeapIdle uint64
|
||||
|
||||
// HeapInuse is bytes in in-use spans.
|
||||
//
|
||||
// In-use spans have at least one object in them. These spans
|
||||
// can only be used for other objects of roughly the same
|
||||
// size.
|
||||
//
|
||||
// HeapInuse minus HeapAlloc estimates the amount of memory
|
||||
// that has been dedicated to particular size classes, but is
|
||||
// not currently being used. This is an upper bound on
|
||||
// fragmentation, but in general this memory can be reused
|
||||
// efficiently.
|
||||
HeapInuse uint64
|
||||
|
||||
// Stack memory statistics.
|
||||
//
|
||||
// Stacks are not considered part of the heap, but the runtime
|
||||
// can reuse a span of heap memory for stack memory, and
|
||||
// vice-versa.
|
||||
|
||||
// StackInuse is bytes in stack spans.
|
||||
//
|
||||
// In-use stack spans have at least one stack in them. These
|
||||
// spans can only be used for other stacks of the same size.
|
||||
//
|
||||
// There is no StackIdle because unused stack spans are
|
||||
// returned to the heap (and hence counted toward HeapIdle).
|
||||
StackInuse uint64
|
||||
|
||||
// StackSys is bytes of stack memory obtained from the OS.
|
||||
//
|
||||
// StackSys is StackInuse, plus any memory obtained directly
|
||||
// from the OS for OS thread stacks.
|
||||
//
|
||||
// In non-cgo programs this metric is currently equal to StackInuse
|
||||
// (but this should not be relied upon, and the value may change in
|
||||
// the future).
|
||||
//
|
||||
// In cgo programs this metric includes OS thread stacks allocated
|
||||
// directly from the OS. Currently, this only accounts for one stack in
|
||||
// c-shared and c-archive build modes and other sources of stacks from
|
||||
// the OS (notably, any allocated by C code) are not currently measured.
|
||||
// Note this too may change in the future.
|
||||
StackSys uint64
|
||||
|
||||
// GCSys is bytes of memory in garbage collection metadata.
|
||||
GCSys uint64
|
||||
}
|
||||
|
||||
func ReadGCStats() GCStats {
|
||||
var heapInuse, heapIdle uint64
|
||||
|
||||
lock(&gcMutex)
|
||||
|
||||
for block := uintptr(0); block < endBlock; block++ {
|
||||
bstate := gcStateOf(block)
|
||||
if bstate == blockStateFree {
|
||||
heapIdle += uint64(bytesPerBlock)
|
||||
} else {
|
||||
heapInuse += uint64(bytesPerBlock)
|
||||
}
|
||||
}
|
||||
|
||||
stackEnd := uintptr(unsafe.Pointer(&_stackEnd))
|
||||
stackSys := stackTop - stackEnd
|
||||
|
||||
stats := GCStats{
|
||||
Alloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock),
|
||||
TotalAlloc: gcTotalAlloc,
|
||||
Sys: uint64(heapEnd - heapStart),
|
||||
Mallocs: gcMallocs,
|
||||
Frees: gcFrees,
|
||||
HeapAlloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock),
|
||||
HeapSys: heapInuse + heapIdle,
|
||||
HeapIdle: heapIdle,
|
||||
HeapInuse: heapInuse,
|
||||
StackInuse: uint64(stackTop - uintptr(getsp())),
|
||||
StackSys: uint64(stackSys),
|
||||
GCSys: uint64(heapEnd - uintptr(metadataStart)),
|
||||
}
|
||||
|
||||
unlock(&gcMutex)
|
||||
|
||||
return stats
|
||||
}
|
||||
54
runtime/internal/runtime/tinygogc/gc_link.go
Normal file
54
runtime/internal/runtime/tinygogc/gc_link.go
Normal file
@@ -0,0 +1,54 @@
|
||||
//go:build !testGC
|
||||
|
||||
package tinygogc
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
// LLGoPackage instructs the LLGo linker to wrap C standard library memory allocation
|
||||
// functions (malloc, realloc, calloc) so they use the tinygogc allocator instead.
|
||||
// This ensures all memory allocations go through the GC, including C library calls.
|
||||
const LLGoPackage = "link: --wrap=malloc --wrap=realloc --wrap=calloc"
|
||||
|
||||
//export __wrap_malloc
|
||||
func __wrap_malloc(size uintptr) unsafe.Pointer {
|
||||
return Alloc(size)
|
||||
}
|
||||
|
||||
//export __wrap_calloc
|
||||
func __wrap_calloc(nmemb, size uintptr) unsafe.Pointer {
|
||||
totalSize := nmemb * size
|
||||
// Check for multiplication overflow
|
||||
if nmemb != 0 && totalSize/nmemb != size {
|
||||
return nil // Overflow
|
||||
}
|
||||
return Alloc(totalSize)
|
||||
}
|
||||
|
||||
//export __wrap_realloc
|
||||
func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
|
||||
return Realloc(ptr, size)
|
||||
}
|
||||
|
||||
//go:linkname getsp llgo.stackSave
|
||||
func getsp() unsafe.Pointer
|
||||
|
||||
//go:linkname _heapStart _heapStart
|
||||
var _heapStart [0]byte
|
||||
|
||||
//go:linkname _heapEnd _heapEnd
|
||||
var _heapEnd [0]byte
|
||||
|
||||
//go:linkname _stackStart _stack_top
|
||||
var _stackStart [0]byte
|
||||
|
||||
//go:linkname _stackEnd _stack_end
|
||||
var _stackEnd [0]byte
|
||||
|
||||
//go:linkname _globals_start _globals_start
|
||||
var _globals_start [0]byte
|
||||
|
||||
//go:linkname _globals_end _globals_end
|
||||
var _globals_end [0]byte
|
||||
25
runtime/internal/runtime/tinygogc/gc_test.go
Normal file
25
runtime/internal/runtime/tinygogc/gc_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build testGC
|
||||
|
||||
package tinygogc
|
||||
|
||||
import (
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
var currentStack uintptr
|
||||
|
||||
func getsp() uintptr {
|
||||
return currentStack
|
||||
}
|
||||
|
||||
var _heapStart [0]byte
|
||||
|
||||
var _heapEnd [0]byte
|
||||
|
||||
var _stackStart [0]byte
|
||||
|
||||
var _stackEnd [0]byte
|
||||
|
||||
var _globals_start [0]byte
|
||||
|
||||
var _globals_end [0]byte
|
||||
570
runtime/internal/runtime/tinygogc/gc_tinygo.go
Normal file
570
runtime/internal/runtime/tinygogc/gc_tinygo.go
Normal file
@@ -0,0 +1,570 @@
|
||||
//go:build baremetal || testGC
|
||||
|
||||
/*
|
||||
* Copyright (c) 2018-2025 The TinyGo Authors. All rights reserved.
|
||||
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 tinygogc implements a conservative mark-and-sweep garbage collector
|
||||
// for baremetal environments where the standard Go runtime and bdwgc are unavailable.
|
||||
//
|
||||
// This implementation is based on TinyGo's GC and is designed for resource-constrained
|
||||
// embedded systems. It uses a block-based allocator with conservative pointer scanning.
|
||||
//
|
||||
// Build tags:
|
||||
// - baremetal: Enables this GC for baremetal targets
|
||||
// - testGC: Enables testing mode with mock implementations
|
||||
//
|
||||
// Memory Layout:
|
||||
// The heap is divided into fixed-size blocks (32 bytes on 64-bit). Metadata is stored
|
||||
// at the end of the heap, using 2 bits per block to track state (free/head/tail/mark).
|
||||
package tinygogc
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
c "github.com/goplus/llgo/runtime/internal/clite"
|
||||
)
|
||||
|
||||
const gcDebug = false
|
||||
|
||||
// blockState stores the four states in which a block can be. It is two bits in
|
||||
// size.
|
||||
const (
|
||||
blockStateFree uint8 = 0 // 00
|
||||
blockStateHead uint8 = 1 // 01
|
||||
blockStateTail uint8 = 2 // 10
|
||||
blockStateMark uint8 = 3 // 11
|
||||
blockStateMask uint8 = 3 // 11
|
||||
)
|
||||
|
||||
// The byte value of a block where every block is a 'tail' block.
|
||||
const blockStateByteAllTails = 0 |
|
||||
uint8(blockStateTail<<(stateBits*3)) |
|
||||
uint8(blockStateTail<<(stateBits*2)) |
|
||||
uint8(blockStateTail<<(stateBits*1)) |
|
||||
uint8(blockStateTail<<(stateBits*0))
|
||||
|
||||
var (
|
||||
heapStart uintptr // start address of heap area
|
||||
heapEnd uintptr // end address of heap area
|
||||
globalsStart uintptr // start address of global variable area
|
||||
globalsEnd uintptr // end address of global variable area
|
||||
stackTop uintptr // the top of stack
|
||||
endBlock uintptr // GC end block index
|
||||
metadataStart unsafe.Pointer // start address of GC metadata
|
||||
|
||||
nextAlloc uintptr // the next block that should be tried by the allocator
|
||||
gcTotalAlloc uint64 // total number of bytes allocated
|
||||
gcTotalBlocks uint64 // total number of allocated blocks
|
||||
gcMallocs uint64 // total number of allocations
|
||||
gcFrees uint64 // total number of objects freed
|
||||
gcFreedBlocks uint64 // total number of freed blocks
|
||||
|
||||
// stackOverflow is a flag which is set when the GC scans too deep while marking.
|
||||
// After it is set, all marked allocations must be re-scanned.
|
||||
markStackOverflow bool
|
||||
|
||||
// zeroSizedAlloc is just a sentinel that gets returned when allocating 0 bytes.
|
||||
zeroSizedAlloc uint8
|
||||
|
||||
gcMutex mutex // gcMutex protects GC related variables
|
||||
isGCInit bool // isGCInit indicates GC initialization state
|
||||
)
|
||||
|
||||
// Some globals + constants for the entire GC.
|
||||
|
||||
const (
|
||||
wordsPerBlock = 4 // number of pointers in an allocated block
|
||||
bytesPerBlock = wordsPerBlock * unsafe.Sizeof(heapStart)
|
||||
stateBits = 2 // how many bits a block state takes (see blockState type)
|
||||
blocksPerStateByte = 8 / stateBits
|
||||
markStackSize = 8 * unsafe.Sizeof((*int)(nil)) // number of to-be-marked blocks to queue before forcing a rescan
|
||||
)
|
||||
|
||||
// this function MUST be initalized first, which means it's required to be initalized before runtime
|
||||
func initGC() {
|
||||
// reserve 2K blocks for libc internal malloc, we cannot wrap those internal functions
|
||||
heapStart = uintptr(unsafe.Pointer(&_heapStart)) + 2048
|
||||
heapEnd = uintptr(unsafe.Pointer(&_heapEnd))
|
||||
globalsStart = uintptr(unsafe.Pointer(&_globals_start))
|
||||
globalsEnd = uintptr(unsafe.Pointer(&_globals_end))
|
||||
totalSize := heapEnd - heapStart
|
||||
metadataSize := (totalSize + blocksPerStateByte*bytesPerBlock) / (1 + blocksPerStateByte*bytesPerBlock)
|
||||
metadataStart = unsafe.Pointer(heapEnd - metadataSize)
|
||||
endBlock = (uintptr(metadataStart) - heapStart) / bytesPerBlock
|
||||
stackTop = uintptr(unsafe.Pointer(&_stackStart))
|
||||
|
||||
c.Memset(metadataStart, 0, metadataSize)
|
||||
}
|
||||
|
||||
func lazyInit() {
|
||||
if !isGCInit {
|
||||
initGC()
|
||||
isGCInit = true
|
||||
}
|
||||
}
|
||||
|
||||
func gcPanic(s *c.Char) {
|
||||
c.Printf(c.Str("%s"), s)
|
||||
c.Exit(2)
|
||||
}
|
||||
|
||||
// blockFromAddr returns a block given an address somewhere in the heap (which
|
||||
// might not be heap-aligned).
|
||||
func blockFromAddr(addr uintptr) uintptr {
|
||||
if addr < heapStart || addr >= uintptr(metadataStart) {
|
||||
gcPanic(c.Str("gc: trying to get block from invalid address"))
|
||||
}
|
||||
return (addr - heapStart) / bytesPerBlock
|
||||
}
|
||||
|
||||
// Return a pointer to the start of the allocated object.
|
||||
func gcPointerOf(blockAddr uintptr) unsafe.Pointer {
|
||||
return unsafe.Pointer(gcAddressOf(blockAddr))
|
||||
}
|
||||
|
||||
// Return the address of the start of the allocated object.
|
||||
func gcAddressOf(blockAddr uintptr) uintptr {
|
||||
addr := heapStart + blockAddr*bytesPerBlock
|
||||
if addr > uintptr(metadataStart) {
|
||||
gcPanic(c.Str("gc: block pointing inside metadata"))
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
// findHead returns the head (first block) of an object, assuming the block
|
||||
// points to an allocated object. It returns the same block if this block
|
||||
// already points to the head.
|
||||
func gcFindHead(blockAddr uintptr) uintptr {
|
||||
for {
|
||||
// Optimization: check whether the current block state byte (which
|
||||
// contains the state of multiple blocks) is composed entirely of tail
|
||||
// blocks. If so, we can skip back to the last block in the previous
|
||||
// state byte.
|
||||
// This optimization speeds up findHead for pointers that point into a
|
||||
// large allocation.
|
||||
stateByte := gcStateByteOf(blockAddr)
|
||||
if stateByte == blockStateByteAllTails {
|
||||
blockAddr -= (blockAddr % blocksPerStateByte) + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Check whether we've found a non-tail block, which means we found the
|
||||
// head.
|
||||
state := gcStateFromByte(blockAddr, stateByte)
|
||||
if state != blockStateTail {
|
||||
break
|
||||
}
|
||||
blockAddr--
|
||||
}
|
||||
if gcStateOf(blockAddr) != blockStateHead && gcStateOf(blockAddr) != blockStateMark {
|
||||
gcPanic(c.Str("gc: found tail without head"))
|
||||
}
|
||||
return blockAddr
|
||||
}
|
||||
|
||||
// findNext returns the first block just past the end of the tail. This may or
|
||||
// may not be the head of an object.
|
||||
func gcFindNext(blockAddr uintptr) uintptr {
|
||||
if gcStateOf(blockAddr) == blockStateHead || gcStateOf(blockAddr) == blockStateMark {
|
||||
blockAddr++
|
||||
}
|
||||
for gcAddressOf(blockAddr) < uintptr(metadataStart) && gcStateOf(blockAddr) == blockStateTail {
|
||||
blockAddr++
|
||||
}
|
||||
return blockAddr
|
||||
}
|
||||
|
||||
func gcStateByteOf(blockAddr uintptr) byte {
|
||||
return *(*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte))
|
||||
}
|
||||
|
||||
// Return the block state given a state byte. The state byte must have been
|
||||
// obtained using b.stateByte(), otherwise the result is incorrect.
|
||||
func gcStateFromByte(blockAddr uintptr, stateByte byte) uint8 {
|
||||
return uint8(stateByte>>((blockAddr%blocksPerStateByte)*stateBits)) & blockStateMask
|
||||
}
|
||||
|
||||
// State returns the current block state.
|
||||
func gcStateOf(blockAddr uintptr) uint8 {
|
||||
return gcStateFromByte(blockAddr, gcStateByteOf(blockAddr))
|
||||
}
|
||||
|
||||
// setState sets the current block to the given state, which must contain more
|
||||
// bits than the current state. Allowed transitions: from free to any state and
|
||||
// from head to mark.
|
||||
func gcSetState(blockAddr uintptr, newState uint8) {
|
||||
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte))
|
||||
*stateBytePtr |= uint8(newState << ((blockAddr % blocksPerStateByte) * stateBits))
|
||||
if gcStateOf(blockAddr) != newState {
|
||||
gcPanic(c.Str("gc: setState() was not successful"))
|
||||
}
|
||||
}
|
||||
|
||||
// markFree sets the block state to free, no matter what state it was in before.
|
||||
func gcMarkFree(blockAddr uintptr) {
|
||||
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte))
|
||||
*stateBytePtr &^= uint8(blockStateMask << ((blockAddr % blocksPerStateByte) * stateBits))
|
||||
if gcStateOf(blockAddr) != blockStateFree {
|
||||
gcPanic(c.Str("gc: markFree() was not successful"))
|
||||
}
|
||||
*(*[wordsPerBlock]uintptr)(unsafe.Pointer(gcAddressOf(blockAddr))) = [wordsPerBlock]uintptr{}
|
||||
}
|
||||
|
||||
// unmark changes the state of the block from mark to head. It must be marked
|
||||
// before calling this function.
|
||||
func gcUnmark(blockAddr uintptr) {
|
||||
if gcStateOf(blockAddr) != blockStateMark {
|
||||
gcPanic(c.Str("gc: unmark() on a block that is not marked"))
|
||||
}
|
||||
clearMask := blockStateMask ^ blockStateHead // the bits to clear from the state
|
||||
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte))
|
||||
*stateBytePtr &^= uint8(clearMask << ((blockAddr % blocksPerStateByte) * stateBits))
|
||||
if gcStateOf(blockAddr) != blockStateHead {
|
||||
gcPanic(c.Str("gc: unmark() was not successful"))
|
||||
}
|
||||
}
|
||||
|
||||
func isOnHeap(ptr uintptr) bool {
|
||||
return ptr >= heapStart && ptr < uintptr(metadataStart)
|
||||
}
|
||||
|
||||
func isPointer(ptr uintptr) bool {
|
||||
// TODO: implement precise GC
|
||||
return isOnHeap(ptr)
|
||||
}
|
||||
|
||||
// alloc tries to find some free space on the heap, possibly doing a garbage
|
||||
// collection cycle if needed. If no space is free, it panics.
|
||||
//
|
||||
//go:noinline
|
||||
func Alloc(size uintptr) unsafe.Pointer {
|
||||
if size == 0 {
|
||||
return unsafe.Pointer(&zeroSizedAlloc)
|
||||
}
|
||||
lock(&gcMutex)
|
||||
lazyInit()
|
||||
|
||||
gcTotalAlloc += uint64(size)
|
||||
gcMallocs++
|
||||
|
||||
neededBlocks := (size + (bytesPerBlock - 1)) / bytesPerBlock
|
||||
gcTotalBlocks += uint64(neededBlocks)
|
||||
|
||||
// Continue looping until a run of free blocks has been found that fits the
|
||||
// requested size.
|
||||
index := nextAlloc
|
||||
numFreeBlocks := uintptr(0)
|
||||
heapScanCount := uint8(0)
|
||||
for {
|
||||
if index == nextAlloc {
|
||||
if heapScanCount == 0 {
|
||||
heapScanCount = 1
|
||||
} else if heapScanCount == 1 {
|
||||
// The entire heap has been searched for free memory, but none
|
||||
// could be found. Run a garbage collection cycle to reclaim
|
||||
// free memory and try again.
|
||||
heapScanCount = 2
|
||||
freeBytes := gc()
|
||||
heapSize := uintptr(metadataStart) - heapStart
|
||||
if freeBytes < heapSize/3 {
|
||||
// Ensure there is at least 33% headroom.
|
||||
// This percentage was arbitrarily chosen, and may need to
|
||||
// be tuned in the future.
|
||||
growHeap()
|
||||
}
|
||||
} else {
|
||||
// Even after garbage collection, no free memory could be found.
|
||||
// Try to increase heap size.
|
||||
if growHeap() {
|
||||
// Success, the heap was increased in size. Try again with a
|
||||
// larger heap.
|
||||
} else {
|
||||
// Unfortunately the heap could not be increased. This
|
||||
// happens on baremetal systems for example (where all
|
||||
// available RAM has already been dedicated to the heap).
|
||||
gcPanic(c.Str("out of memory"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap around the end of the heap.
|
||||
if index == endBlock {
|
||||
index = 0
|
||||
// Reset numFreeBlocks as allocations cannot wrap.
|
||||
numFreeBlocks = 0
|
||||
// In rare cases, the initial heap might be so small that there are
|
||||
// no blocks at all. In this case, it's better to jump back to the
|
||||
// start of the loop and try again, until the GC realizes there is
|
||||
// no memory and grows the heap.
|
||||
// This can sometimes happen on WebAssembly, where the initial heap
|
||||
// is created by whatever is left on the last memory page.
|
||||
continue
|
||||
}
|
||||
|
||||
// Is the block we're looking at free?
|
||||
if gcStateOf(index) != blockStateFree {
|
||||
// This block is in use. Try again from this point.
|
||||
numFreeBlocks = 0
|
||||
index++
|
||||
continue
|
||||
}
|
||||
numFreeBlocks++
|
||||
index++
|
||||
|
||||
// Are we finished?
|
||||
if numFreeBlocks == neededBlocks {
|
||||
// Found a big enough range of free blocks!
|
||||
nextAlloc = index
|
||||
thisAlloc := index - neededBlocks
|
||||
|
||||
// Set the following blocks as being allocated.
|
||||
gcSetState(thisAlloc, blockStateHead)
|
||||
for i := thisAlloc + 1; i != nextAlloc; i++ {
|
||||
gcSetState(i, blockStateTail)
|
||||
}
|
||||
unlock(&gcMutex)
|
||||
// Return a pointer to this allocation.
|
||||
return c.Memset(gcPointerOf(thisAlloc), 0, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
|
||||
if ptr == nil {
|
||||
return Alloc(size)
|
||||
}
|
||||
lock(&gcMutex)
|
||||
lazyInit()
|
||||
unlock(&gcMutex)
|
||||
|
||||
ptrAddress := uintptr(ptr)
|
||||
endOfTailAddress := gcAddressOf(gcFindNext(blockFromAddr(ptrAddress)))
|
||||
|
||||
// this might be a few bytes longer than the original size of
|
||||
// ptr, because we align to full blocks of size bytesPerBlock
|
||||
oldSize := endOfTailAddress - ptrAddress
|
||||
if size <= oldSize {
|
||||
return ptr
|
||||
}
|
||||
|
||||
newAlloc := Alloc(size)
|
||||
c.Memcpy(newAlloc, ptr, oldSize)
|
||||
free(ptr)
|
||||
|
||||
return newAlloc
|
||||
}
|
||||
|
||||
func free(ptr unsafe.Pointer) {
|
||||
// TODO: free blocks on request, when the compiler knows they're unused.
|
||||
}
|
||||
|
||||
func GC() uintptr {
|
||||
lock(&gcMutex)
|
||||
freeBytes := gc()
|
||||
unlock(&gcMutex)
|
||||
return freeBytes
|
||||
}
|
||||
|
||||
// runGC performs a garbage collection cycle. It is the internal implementation
|
||||
// of the runtime.GC() function. The difference is that it returns the number of
|
||||
// free bytes in the heap after the GC is finished.
|
||||
func gc() (freeBytes uintptr) {
|
||||
lazyInit()
|
||||
|
||||
if gcDebug {
|
||||
println("running collection cycle...")
|
||||
}
|
||||
|
||||
// Mark phase: mark all reachable objects, recursively.
|
||||
gcMarkReachable()
|
||||
|
||||
finishMark()
|
||||
|
||||
// If we're using threads, resume all other threads before starting the
|
||||
// sweep.
|
||||
gcResumeWorld()
|
||||
|
||||
// Sweep phase: free all non-marked objects and unmark marked objects for
|
||||
// the next collection cycle.
|
||||
freeBytes = sweep()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// markRoots reads all pointers from start to end (exclusive) and if they look
|
||||
// like a heap pointer and are unmarked, marks them and scans that object as
|
||||
// well (recursively). The start and end parameters must be valid pointers and
|
||||
// must be aligned.
|
||||
func markRoots(start, end uintptr) {
|
||||
if start >= end {
|
||||
gcPanic(c.Str("gc: unexpected range to mark"))
|
||||
}
|
||||
// Reduce the end bound to avoid reading too far on platforms where pointer alignment is smaller than pointer size.
|
||||
// If the size of the range is 0, then end will be slightly below start after this.
|
||||
end -= unsafe.Sizeof(end) - unsafe.Alignof(end)
|
||||
|
||||
for addr := start; addr < end; addr += unsafe.Alignof(addr) {
|
||||
root := *(*uintptr)(unsafe.Pointer(addr))
|
||||
markRoot(addr, root)
|
||||
}
|
||||
}
|
||||
|
||||
// startMark starts the marking process on a root and all of its children.
|
||||
func startMark(root uintptr) {
|
||||
var stack [markStackSize]uintptr
|
||||
stack[0] = root
|
||||
gcSetState(root, blockStateMark)
|
||||
stackLen := 1
|
||||
for stackLen > 0 {
|
||||
// Pop a block off of the stack.
|
||||
stackLen--
|
||||
block := stack[stackLen]
|
||||
|
||||
start, end := gcAddressOf(block), gcAddressOf(gcFindNext(block))
|
||||
|
||||
for addr := start; addr != end; addr += unsafe.Alignof(addr) {
|
||||
// Load the word.
|
||||
word := *(*uintptr)(unsafe.Pointer(addr))
|
||||
|
||||
if !isPointer(word) {
|
||||
// Not a heap pointer.
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the corresponding memory block.
|
||||
referencedBlock := blockFromAddr(word)
|
||||
|
||||
if gcStateOf(referencedBlock) == blockStateFree {
|
||||
// The to-be-marked object doesn't actually exist.
|
||||
// This is probably a false positive.
|
||||
continue
|
||||
}
|
||||
|
||||
// Move to the block's head.
|
||||
referencedBlock = gcFindHead(referencedBlock)
|
||||
|
||||
if gcStateOf(referencedBlock) == blockStateMark {
|
||||
// The block has already been marked by something else.
|
||||
continue
|
||||
}
|
||||
|
||||
// Mark block.
|
||||
gcSetState(referencedBlock, blockStateMark)
|
||||
|
||||
if stackLen == len(stack) {
|
||||
// The stack is full.
|
||||
// It is necessary to rescan all marked blocks once we are done.
|
||||
markStackOverflow = true
|
||||
if gcDebug {
|
||||
println("gc stack overflowed")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Push the pointer onto the stack to be scanned later.
|
||||
stack[stackLen] = referencedBlock
|
||||
stackLen++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finishMark finishes the marking process by processing all stack overflows.
|
||||
func finishMark() {
|
||||
for markStackOverflow {
|
||||
// Re-mark all blocks.
|
||||
markStackOverflow = false
|
||||
for block := uintptr(0); block < endBlock; block++ {
|
||||
if gcStateOf(block) != blockStateMark {
|
||||
// Block is not marked, so we do not need to rescan it.
|
||||
continue
|
||||
}
|
||||
|
||||
// Re-mark the block.
|
||||
startMark(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mark a GC root at the address addr.
|
||||
func markRoot(addr, root uintptr) {
|
||||
if isOnHeap(root) {
|
||||
block := blockFromAddr(root)
|
||||
if gcStateOf(block) == blockStateFree {
|
||||
// The to-be-marked object doesn't actually exist.
|
||||
// This could either be a dangling pointer (oops!) but most likely
|
||||
// just a false positive.
|
||||
return
|
||||
}
|
||||
head := gcFindHead(block)
|
||||
|
||||
if gcStateOf(head) != blockStateMark {
|
||||
startMark(head)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sweep goes through all memory and frees unmarked
|
||||
// It returns how many bytes are free in the heap after the sweep.
|
||||
func sweep() (freeBytes uintptr) {
|
||||
freeCurrentObject := false
|
||||
var freed uint64
|
||||
|
||||
for block := uintptr(0); block < endBlock; block++ {
|
||||
switch gcStateOf(block) {
|
||||
case blockStateHead:
|
||||
// Unmarked head. Free it, including all tail blocks following it.
|
||||
gcMarkFree(block)
|
||||
freeCurrentObject = true
|
||||
gcFrees++
|
||||
freed++
|
||||
case blockStateTail:
|
||||
if freeCurrentObject {
|
||||
// This is a tail object following an unmarked head.
|
||||
// Free it now.
|
||||
gcMarkFree(block)
|
||||
freed++
|
||||
}
|
||||
case blockStateMark:
|
||||
// This is a marked object. The next tail blocks must not be freed,
|
||||
// but the mark bit must be removed so the next GC cycle will
|
||||
// collect this object if it is unreferenced then.
|
||||
gcUnmark(block)
|
||||
freeCurrentObject = false
|
||||
case blockStateFree:
|
||||
freeBytes += bytesPerBlock
|
||||
}
|
||||
}
|
||||
gcFreedBlocks += freed
|
||||
freeBytes += uintptr(freed) * bytesPerBlock
|
||||
return
|
||||
}
|
||||
|
||||
// growHeap tries to grow the heap size. It returns true if it succeeds, false
|
||||
// otherwise.
|
||||
func growHeap() bool {
|
||||
// On baremetal, there is no way the heap can be grown.
|
||||
return false
|
||||
}
|
||||
|
||||
func gcMarkReachable() {
|
||||
markRoots(uintptr(getsp()), stackTop)
|
||||
markRoots(globalsStart, globalsEnd)
|
||||
}
|
||||
|
||||
func gcResumeWorld() {
|
||||
// Nothing to do here (single threaded).
|
||||
}
|
||||
8
runtime/internal/runtime/tinygogc/mutex.go
Normal file
8
runtime/internal/runtime/tinygogc/mutex.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package tinygogc
|
||||
|
||||
// TODO(MeteorsLiu): mutex lock for baremetal GC
|
||||
type mutex struct{}
|
||||
|
||||
func lock(m *mutex) {}
|
||||
|
||||
func unlock(m *mutex) {}
|
||||
604
runtime/internal/runtime/tinygogc/pc_mock_test.go
Normal file
604
runtime/internal/runtime/tinygogc/pc_mock_test.go
Normal file
@@ -0,0 +1,604 @@
|
||||
//go:build testGC
|
||||
|
||||
package tinygogc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
c "github.com/goplus/llgo/runtime/internal/clite"
|
||||
)
|
||||
|
||||
const (
|
||||
// Mock a typical embedded system with 128KB RAM
|
||||
mockHeapSize = 128 * 1024 // 128KB
|
||||
mockGlobalsSize = 4 * 1024 // 4KB for globals
|
||||
mockStackSize = 8 * 1024 // 8KB for stack
|
||||
mockReservedSize = 2048 // 2KB reserved as in real implementation
|
||||
)
|
||||
|
||||
type testObject struct {
|
||||
data [4]uintptr
|
||||
}
|
||||
|
||||
// mockGCEnv provides a controlled root environment for GC testing
|
||||
type mockGCEnv struct {
|
||||
memory []byte
|
||||
heapStart uintptr
|
||||
heapEnd uintptr
|
||||
globalsStart uintptr
|
||||
globalsEnd uintptr
|
||||
stackStart uintptr
|
||||
stackEnd uintptr
|
||||
// Controlled root sets for testing
|
||||
rootObjects []unsafe.Pointer
|
||||
// Original GC state to restore
|
||||
originalHeapStart uintptr
|
||||
originalHeapEnd uintptr
|
||||
originalGlobalsStart uintptr
|
||||
originalGlobalsEnd uintptr
|
||||
originalStackTop uintptr
|
||||
originalEndBlock uintptr
|
||||
originalMetadataStart unsafe.Pointer
|
||||
originalNextAlloc uintptr
|
||||
originalIsGCInit bool
|
||||
// Mock mode flag
|
||||
mockMode bool
|
||||
}
|
||||
|
||||
// createMockGCEnv creates a completely isolated GC environment
|
||||
func createMockGCEnv() *mockGCEnv {
|
||||
totalMemory := mockHeapSize + mockGlobalsSize + mockStackSize
|
||||
memory := make([]byte, totalMemory)
|
||||
baseAddr := uintptr(unsafe.Pointer(&memory[0]))
|
||||
|
||||
env := &mockGCEnv{
|
||||
memory: memory,
|
||||
globalsStart: baseAddr,
|
||||
globalsEnd: baseAddr + mockGlobalsSize,
|
||||
heapStart: baseAddr + mockGlobalsSize + mockReservedSize,
|
||||
heapEnd: baseAddr + mockGlobalsSize + mockHeapSize,
|
||||
stackStart: baseAddr + mockGlobalsSize + mockHeapSize,
|
||||
stackEnd: baseAddr + uintptr(totalMemory),
|
||||
rootObjects: make([]unsafe.Pointer, 0),
|
||||
mockMode: false,
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// setupMockGC initializes the GC with mock memory layout using initGC's logic
|
||||
func (env *mockGCEnv) setupMockGC() {
|
||||
// Save original GC state
|
||||
env.originalHeapStart = heapStart
|
||||
env.originalHeapEnd = heapEnd
|
||||
env.originalGlobalsStart = globalsStart
|
||||
env.originalGlobalsEnd = globalsEnd
|
||||
env.originalStackTop = stackTop
|
||||
env.originalEndBlock = endBlock
|
||||
env.originalMetadataStart = metadataStart
|
||||
env.originalNextAlloc = nextAlloc
|
||||
env.originalIsGCInit = isGCInit
|
||||
|
||||
// Set currentStack for getsp()
|
||||
currentStack = env.stackStart
|
||||
|
||||
// Apply initGC's logic with our mock memory layout
|
||||
// This is the same logic as initGC() but with our mock addresses
|
||||
heapStart = env.heapStart + 2048 // reserve 2K blocks like initGC does
|
||||
heapEnd = env.heapEnd
|
||||
globalsStart = env.globalsStart
|
||||
globalsEnd = env.globalsEnd
|
||||
stackTop = env.stackEnd
|
||||
|
||||
totalSize := heapEnd - heapStart
|
||||
metadataSize := (totalSize + blocksPerStateByte*bytesPerBlock) / (1 + blocksPerStateByte*bytesPerBlock)
|
||||
metadataStart = unsafe.Pointer(heapEnd - metadataSize)
|
||||
endBlock = (uintptr(metadataStart) - heapStart) / bytesPerBlock
|
||||
|
||||
// Clear metadata using memset like initGC does
|
||||
c.Memset(metadataStart, 0, metadataSize)
|
||||
|
||||
// Reset allocator state and all GC statistics for clean test environment
|
||||
nextAlloc = 0
|
||||
isGCInit = true
|
||||
|
||||
// Reset all GC statistics to start from clean state
|
||||
gcTotalAlloc = 0
|
||||
gcTotalBlocks = 0
|
||||
gcMallocs = 0
|
||||
gcFrees = 0
|
||||
gcFreedBlocks = 0
|
||||
markStackOverflow = false
|
||||
}
|
||||
|
||||
// restoreOriginalGC restores the original GC state
|
||||
func (env *mockGCEnv) restoreOriginalGC() {
|
||||
heapStart = env.originalHeapStart
|
||||
heapEnd = env.originalHeapEnd
|
||||
globalsStart = env.originalGlobalsStart
|
||||
globalsEnd = env.originalGlobalsEnd
|
||||
stackTop = env.originalStackTop
|
||||
endBlock = env.originalEndBlock
|
||||
metadataStart = env.originalMetadataStart
|
||||
nextAlloc = env.originalNextAlloc
|
||||
isGCInit = false
|
||||
}
|
||||
|
||||
// enableMockMode enables mock root scanning mode
|
||||
func (env *mockGCEnv) enableMockMode() {
|
||||
env.mockMode = true
|
||||
}
|
||||
|
||||
// disableMockMode disables mock root scanning mode
|
||||
func (env *mockGCEnv) disableMockMode() {
|
||||
env.mockMode = false
|
||||
}
|
||||
|
||||
// addRoot adds an object to the controlled root set
|
||||
func (env *mockGCEnv) addRoot(ptr unsafe.Pointer) {
|
||||
env.rootObjects = append(env.rootObjects, ptr)
|
||||
}
|
||||
|
||||
// clearRoots removes all objects from the controlled root set
|
||||
func (env *mockGCEnv) clearRoots() {
|
||||
env.rootObjects = env.rootObjects[:0]
|
||||
}
|
||||
|
||||
// mockMarkReachable replaces gcMarkReachable when in mock mode
|
||||
func (env *mockGCEnv) mockMarkReachable() {
|
||||
if !env.mockMode {
|
||||
// Use original logic
|
||||
markRoots(uintptr(getsp()), stackTop)
|
||||
markRoots(globalsStart, globalsEnd)
|
||||
return
|
||||
}
|
||||
|
||||
// Mock mode: only scan our controlled roots
|
||||
for _, root := range env.rootObjects {
|
||||
addr := uintptr(root)
|
||||
markRoot(addr, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// runMockGC runs standard GC but with controlled root scanning
|
||||
func (env *mockGCEnv) runMockGC() uintptr {
|
||||
lock(&gcMutex)
|
||||
defer unlock(&gcMutex)
|
||||
|
||||
lazyInit()
|
||||
|
||||
if gcDebug {
|
||||
println("running mock collection cycle...")
|
||||
}
|
||||
|
||||
// Mark phase: use our mock root scanning
|
||||
env.mockMarkReachable()
|
||||
finishMark()
|
||||
|
||||
// Resume world (no-op in single threaded)
|
||||
gcResumeWorld()
|
||||
|
||||
// Sweep phase: use standard sweep logic
|
||||
return sweep()
|
||||
}
|
||||
|
||||
// createTestObjects creates a network of objects for testing reachability
|
||||
func createTestObjects(env *mockGCEnv) []*testObject {
|
||||
// Allocate several test objects
|
||||
objects := make([]*testObject, 0, 10)
|
||||
|
||||
// Dependencies Graph
|
||||
// root1 -> child1 -> grandchild1 -> child2
|
||||
// root1 -> child2 -> grandchild1
|
||||
|
||||
// Create root objects (reachable from stack/globals)
|
||||
root1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
|
||||
root2 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
|
||||
objects = append(objects, root1, root2)
|
||||
|
||||
// Create objects reachable from root1
|
||||
child1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
|
||||
child2 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
|
||||
root1.data[0] = uintptr(unsafe.Pointer(child1))
|
||||
root1.data[1] = uintptr(unsafe.Pointer(child2))
|
||||
objects = append(objects, child1, child2)
|
||||
|
||||
// Create objects reachable from child1
|
||||
grandchild1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
|
||||
child1.data[0] = uintptr(unsafe.Pointer(grandchild1))
|
||||
objects = append(objects, grandchild1)
|
||||
|
||||
// Create circular reference between child2 and grandchild1
|
||||
child2.data[0] = uintptr(unsafe.Pointer(grandchild1))
|
||||
grandchild1.data[0] = uintptr(unsafe.Pointer(child2))
|
||||
|
||||
// Create unreachable objects (garbage)
|
||||
garbage1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
|
||||
garbage2 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
|
||||
// Create circular reference in garbage
|
||||
garbage1.data[0] = uintptr(unsafe.Pointer(garbage2))
|
||||
garbage2.data[0] = uintptr(unsafe.Pointer(garbage1))
|
||||
objects = append(objects, garbage1, garbage2)
|
||||
|
||||
return objects
|
||||
}
|
||||
|
||||
func TestMockGCBasicAllocation(t *testing.T) {
|
||||
env := createMockGCEnv()
|
||||
env.setupMockGC()
|
||||
defer env.restoreOriginalGC()
|
||||
|
||||
// Test basic allocation
|
||||
ptr1 := Alloc(32)
|
||||
if ptr1 == nil {
|
||||
t.Fatal("Failed to allocate 32 bytes")
|
||||
}
|
||||
|
||||
ptr2 := Alloc(64)
|
||||
if ptr2 == nil {
|
||||
t.Fatal("Failed to allocate 64 bytes")
|
||||
}
|
||||
|
||||
// Verify pointers are within heap bounds
|
||||
addr1 := uintptr(ptr1)
|
||||
addr2 := uintptr(ptr2)
|
||||
|
||||
if addr1 < heapStart || addr1 >= uintptr(metadataStart) {
|
||||
t.Errorf("ptr1 %x not within heap bounds [%x, %x)", addr1, heapStart, uintptr(metadataStart))
|
||||
}
|
||||
|
||||
if addr2 < heapStart || addr2 >= uintptr(metadataStart) {
|
||||
t.Errorf("ptr2 %x not within heap bounds [%x, %x)", addr2, heapStart, uintptr(metadataStart))
|
||||
}
|
||||
|
||||
t.Logf("Allocated ptr1 at %x, ptr2 at %x", addr1, addr2)
|
||||
t.Logf("Heap bounds: [%x, %x)", heapStart, uintptr(metadataStart))
|
||||
}
|
||||
|
||||
func TestMockGCReachabilityAndSweep(t *testing.T) {
|
||||
env := createMockGCEnv()
|
||||
env.setupMockGC()
|
||||
defer env.restoreOriginalGC()
|
||||
|
||||
// Track initial stats
|
||||
initialMallocs := gcMallocs
|
||||
initialFrees := gcFrees
|
||||
|
||||
// Create test object network
|
||||
objects := createTestObjects(env)
|
||||
|
||||
// Add first 2 objects as roots using mock control
|
||||
env.enableMockMode()
|
||||
env.addRoot(unsafe.Pointer(objects[0])) // root1
|
||||
env.addRoot(unsafe.Pointer(objects[1])) // root2
|
||||
|
||||
t.Logf("Created %d objects, 2 are roots", len(objects))
|
||||
t.Logf("Mallocs: %d", gcMallocs-initialMallocs)
|
||||
|
||||
// Verify all objects are initially allocated
|
||||
for i, obj := range objects {
|
||||
addr := uintptr(unsafe.Pointer(obj))
|
||||
block := blockFromAddr(addr)
|
||||
state := gcStateOf(block)
|
||||
if state != blockStateHead {
|
||||
t.Errorf("Object %d at %x has state %d, expected %d (HEAD)", i, addr, state, blockStateHead)
|
||||
}
|
||||
}
|
||||
|
||||
// Perform GC with controlled root scanning
|
||||
freedBytes := env.runMockGC()
|
||||
t.Logf("Freed %d bytes during GC", freedBytes)
|
||||
t.Logf("Frees: %d (delta: %d)", gcFrees, gcFrees-initialFrees)
|
||||
|
||||
// Verify reachable objects are still allocated
|
||||
reachableObjects := []unsafe.Pointer{
|
||||
unsafe.Pointer(objects[0]), // root1
|
||||
unsafe.Pointer(objects[1]), // root2
|
||||
unsafe.Pointer(objects[2]), // child1 (reachable from root1)
|
||||
unsafe.Pointer(objects[3]), // child2 (reachable from root1)
|
||||
unsafe.Pointer(objects[4]), // grandchild1 (reachable from child1, child2)
|
||||
}
|
||||
|
||||
for i, obj := range reachableObjects {
|
||||
addr := uintptr(obj)
|
||||
block := blockFromAddr(addr)
|
||||
state := gcStateOf(block)
|
||||
if state != blockStateHead {
|
||||
t.Errorf("Reachable object %d at %x has state %d, expected %d (HEAD)", i, addr, state, blockStateHead)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify unreachable objects are freed
|
||||
unreachableObjects := []unsafe.Pointer{
|
||||
unsafe.Pointer(objects[5]), // garbage1
|
||||
unsafe.Pointer(objects[6]), // garbage2
|
||||
}
|
||||
|
||||
for i, obj := range unreachableObjects {
|
||||
addr := uintptr(obj)
|
||||
block := blockFromAddr(addr)
|
||||
state := gcStateOf(block)
|
||||
if state != blockStateFree {
|
||||
t.Errorf("Unreachable object %d at %x has state %d, expected %d (FREE)", i, addr, state, blockStateFree)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify some memory was actually freed
|
||||
if freedBytes == 0 {
|
||||
t.Error("Expected some memory to be freed, but freed 0 bytes")
|
||||
}
|
||||
|
||||
if gcFrees == initialFrees {
|
||||
t.Error("Expected some objects to be freed, but free count didn't change")
|
||||
}
|
||||
|
||||
// Clear refs to make grandchild1 unreachable
|
||||
objects[2].data[0] = 0 // child1 -> grandchild1
|
||||
objects[3].data[0] = 0 // child2 -> grandchild1
|
||||
|
||||
// Run GC again with same roots
|
||||
freedBytes = env.runMockGC()
|
||||
|
||||
// child2 should still be reachable (through root1)
|
||||
blockAddr := blockFromAddr(uintptr(unsafe.Pointer(objects[3])))
|
||||
state := gcStateOf(blockAddr)
|
||||
if state != blockStateHead {
|
||||
t.Errorf("Object child2 at %x has state %d, expected %d (HEAD)", blockAddr, state, blockStateHead)
|
||||
}
|
||||
|
||||
// grandchild1 should now be unreachable and freed
|
||||
blockAddr = blockFromAddr(uintptr(unsafe.Pointer(objects[4])))
|
||||
state = gcStateOf(blockAddr)
|
||||
if state != blockStateFree {
|
||||
t.Errorf("Object grandchild1 at %x has state %d, expected %d (FREE)", blockAddr, state, blockStateFree)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockGCMemoryPressure(t *testing.T) {
|
||||
env := createMockGCEnv()
|
||||
env.setupMockGC()
|
||||
defer env.restoreOriginalGC()
|
||||
|
||||
// Calculate available heap space
|
||||
heapSize := uintptr(metadataStart) - heapStart
|
||||
blockSize := bytesPerBlock
|
||||
maxBlocks := heapSize / blockSize
|
||||
|
||||
t.Logf("Heap size: %d bytes, Block size: %d bytes, Max blocks: %d",
|
||||
heapSize, blockSize, maxBlocks)
|
||||
|
||||
// Allocate until we trigger GC
|
||||
var allocations []unsafe.Pointer
|
||||
allocSize := uintptr(32) // Small allocations
|
||||
|
||||
// Allocate about 80% of heap to trigger GC pressure
|
||||
targetAllocations := int(maxBlocks * 4 / 5) // 80% capacity
|
||||
|
||||
for i := 0; i < targetAllocations; i++ {
|
||||
ptr := Alloc(allocSize)
|
||||
if ptr == nil {
|
||||
t.Fatalf("Failed to allocate at iteration %d", i)
|
||||
}
|
||||
allocations = append(allocations, ptr)
|
||||
}
|
||||
|
||||
initialMallocs := gcMallocs
|
||||
t.Logf("Allocated %d objects (%d mallocs total)", len(allocations), initialMallocs)
|
||||
|
||||
// Enable mock mode and keep only half the allocations as roots
|
||||
env.enableMockMode()
|
||||
keepCount := len(allocations) / 2
|
||||
for i := 0; i < keepCount; i++ {
|
||||
env.addRoot(allocations[i])
|
||||
}
|
||||
|
||||
t.Logf("Keeping %d objects as roots, %d should be freed", keepCount, len(allocations)-keepCount)
|
||||
|
||||
// Force GC with controlled roots
|
||||
freeBytes := env.runMockGC()
|
||||
|
||||
t.Logf("GC freed %d bytes", freeBytes)
|
||||
t.Logf("Objects freed: %d", gcFrees)
|
||||
|
||||
// Try to allocate more after GC
|
||||
for i := 0; i < 10; i++ {
|
||||
ptr := Alloc(allocSize)
|
||||
if ptr == nil {
|
||||
t.Fatalf("Failed to allocate after GC at iteration %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
t.Log("Successfully allocated more objects after GC")
|
||||
}
|
||||
|
||||
func TestMockGCStats(t *testing.T) {
|
||||
env := createMockGCEnv()
|
||||
env.setupMockGC()
|
||||
defer env.restoreOriginalGC()
|
||||
|
||||
// Get initial stats
|
||||
initialStats := ReadGCStats()
|
||||
t.Logf("Initial stats - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d",
|
||||
initialStats.Mallocs, initialStats.Frees, initialStats.TotalAlloc, initialStats.Alloc)
|
||||
|
||||
// Verify basic system stats
|
||||
expectedSys := uint64(env.heapEnd - env.heapStart - 2048)
|
||||
if initialStats.Sys != expectedSys {
|
||||
t.Errorf("Expected Sys %d, got %d", expectedSys, initialStats.Sys)
|
||||
}
|
||||
|
||||
expectedGCSys := uint64(env.heapEnd - uintptr(metadataStart))
|
||||
if initialStats.GCSys != expectedGCSys {
|
||||
t.Errorf("Expected GCSys %d, got %d", expectedGCSys, initialStats.GCSys)
|
||||
}
|
||||
|
||||
// Allocate some objects
|
||||
var allocations []unsafe.Pointer
|
||||
allocSize := uintptr(64)
|
||||
numAllocs := 10
|
||||
|
||||
for i := 0; i < numAllocs; i++ {
|
||||
ptr := Alloc(allocSize)
|
||||
if ptr == nil {
|
||||
t.Fatalf("Failed to allocate at iteration %d", i)
|
||||
}
|
||||
allocations = append(allocations, ptr)
|
||||
}
|
||||
|
||||
// Check stats after allocation
|
||||
afterAllocStats := ReadGCStats()
|
||||
t.Logf("After allocation - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d",
|
||||
afterAllocStats.Mallocs, afterAllocStats.Frees, afterAllocStats.TotalAlloc, afterAllocStats.Alloc)
|
||||
|
||||
// Verify allocation stats increased
|
||||
if afterAllocStats.Mallocs <= initialStats.Mallocs {
|
||||
t.Errorf("Expected Mallocs to increase from %d, got %d", initialStats.Mallocs, afterAllocStats.Mallocs)
|
||||
}
|
||||
|
||||
if afterAllocStats.TotalAlloc <= initialStats.TotalAlloc {
|
||||
t.Errorf("Expected TotalAlloc to increase from %d, got %d", initialStats.TotalAlloc, afterAllocStats.TotalAlloc)
|
||||
}
|
||||
|
||||
if afterAllocStats.Alloc <= initialStats.Alloc {
|
||||
t.Errorf("Expected Alloc to increase from %d, got %d", initialStats.Alloc, afterAllocStats.Alloc)
|
||||
}
|
||||
|
||||
// Verify Alloc and HeapAlloc are the same
|
||||
if afterAllocStats.Alloc != afterAllocStats.HeapAlloc {
|
||||
t.Errorf("Expected Alloc (%d) to equal HeapAlloc (%d)", afterAllocStats.Alloc, afterAllocStats.HeapAlloc)
|
||||
}
|
||||
|
||||
// Perform GC with controlled roots - keep only half the allocations
|
||||
env.enableMockMode()
|
||||
keepCount := len(allocations) / 2
|
||||
for i := 0; i < keepCount; i++ {
|
||||
env.addRoot(allocations[i])
|
||||
}
|
||||
|
||||
freedBytes := env.runMockGC()
|
||||
t.Logf("GC freed %d bytes", freedBytes)
|
||||
|
||||
// Check stats after GC
|
||||
afterGCStats := ReadGCStats()
|
||||
t.Logf("After GC - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d",
|
||||
afterGCStats.Mallocs, afterGCStats.Frees, afterGCStats.TotalAlloc, afterGCStats.Alloc)
|
||||
|
||||
// Verify GC stats
|
||||
if afterGCStats.Frees <= afterAllocStats.Frees {
|
||||
t.Errorf("Expected Frees to increase from %d, got %d", afterAllocStats.Frees, afterGCStats.Frees)
|
||||
}
|
||||
|
||||
// TotalAlloc should not decrease (cumulative)
|
||||
if afterGCStats.TotalAlloc != afterAllocStats.TotalAlloc {
|
||||
t.Errorf("Expected TotalAlloc to remain %d after GC, got %d", afterAllocStats.TotalAlloc, afterGCStats.TotalAlloc)
|
||||
}
|
||||
|
||||
// Alloc should decrease (freed objects)
|
||||
if afterGCStats.Alloc >= afterAllocStats.Alloc {
|
||||
t.Errorf("Expected Alloc to decrease from %d after GC, got %d", afterAllocStats.Alloc, afterGCStats.Alloc)
|
||||
}
|
||||
|
||||
// Verify heap statistics consistency
|
||||
if afterGCStats.HeapSys != afterGCStats.HeapInuse+afterGCStats.HeapIdle {
|
||||
t.Errorf("Expected HeapSys (%d) to equal HeapInuse (%d) + HeapIdle (%d)",
|
||||
afterGCStats.HeapSys, afterGCStats.HeapInuse, afterGCStats.HeapIdle)
|
||||
}
|
||||
|
||||
// Verify live objects calculation
|
||||
expectedLiveObjects := afterGCStats.Mallocs - afterGCStats.Frees
|
||||
t.Logf("Live objects: %d (Mallocs: %d - Frees: %d)", expectedLiveObjects, afterGCStats.Mallocs, afterGCStats.Frees)
|
||||
|
||||
// The number of live objects should be reasonable (we kept half the allocations plus some overhead)
|
||||
if expectedLiveObjects < uint64(keepCount) {
|
||||
t.Errorf("Expected at least %d live objects, got %d", keepCount, expectedLiveObjects)
|
||||
}
|
||||
|
||||
// Test stack statistics
|
||||
if afterGCStats.StackInuse > afterGCStats.StackSys {
|
||||
t.Errorf("StackInuse (%d) should not exceed StackSys (%d)", afterGCStats.StackInuse, afterGCStats.StackSys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockGCCircularReferences(t *testing.T) {
|
||||
env := createMockGCEnv()
|
||||
env.setupMockGC()
|
||||
defer env.restoreOriginalGC()
|
||||
|
||||
type Node struct {
|
||||
data [3]uintptr
|
||||
next uintptr
|
||||
}
|
||||
|
||||
// Create a circular linked list
|
||||
nodes := make([]*Node, 5)
|
||||
for i := range nodes {
|
||||
nodes[i] = (*Node)(Alloc(unsafe.Sizeof(Node{})))
|
||||
nodes[i].data[0] = uintptr(i) // Store index as data
|
||||
}
|
||||
|
||||
// Link them in a circle
|
||||
for i := range nodes {
|
||||
nextIdx := (i + 1) % len(nodes)
|
||||
nodes[i].next = uintptr(unsafe.Pointer(nodes[nextIdx]))
|
||||
}
|
||||
|
||||
t.Logf("Created circular list of %d nodes", len(nodes))
|
||||
|
||||
// Initially all should be allocated
|
||||
for i, node := range nodes {
|
||||
addr := uintptr(unsafe.Pointer(node))
|
||||
block := blockFromAddr(addr)
|
||||
state := gcStateOf(block)
|
||||
if state != blockStateHead {
|
||||
t.Errorf("Node %d at %x has state %d, expected %d", i, addr, state, blockStateHead)
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: With root references - objects should NOT be freed
|
||||
env.enableMockMode()
|
||||
// Add the first node as root (keeps entire circle reachable)
|
||||
env.addRoot(unsafe.Pointer(nodes[0]))
|
||||
|
||||
freeBytes := env.runMockGC()
|
||||
t.Logf("GC with root reference freed %d bytes", freeBytes)
|
||||
|
||||
// All nodes should still be allocated since they're reachable through the root
|
||||
for i, node := range nodes {
|
||||
addr := uintptr(unsafe.Pointer(node))
|
||||
block := blockFromAddr(addr)
|
||||
state := gcStateOf(block)
|
||||
if state != blockStateHead {
|
||||
t.Errorf("Node %d at %x should still be allocated, but has state %d", i, addr, state)
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: Without root references - all circular objects should be freed
|
||||
env.clearRoots() // Remove all root references
|
||||
|
||||
freeBytes = env.runMockGC()
|
||||
t.Logf("GC without roots freed %d bytes", freeBytes)
|
||||
|
||||
// All nodes should now be freed since they're not reachable from any roots
|
||||
expectedFreed := uintptr(len(nodes)) * ((unsafe.Sizeof(Node{}) + bytesPerBlock - 1) / bytesPerBlock) * bytesPerBlock
|
||||
|
||||
if freeBytes < expectedFreed {
|
||||
t.Errorf("Expected at least %d bytes freed, got %d", expectedFreed, freeBytes)
|
||||
}
|
||||
|
||||
// Verify all nodes are actually freed
|
||||
for i, node := range nodes {
|
||||
addr := uintptr(unsafe.Pointer(node))
|
||||
block := blockFromAddr(addr)
|
||||
state := gcStateOf(block)
|
||||
if state != blockStateFree {
|
||||
t.Errorf("Node %d at %x should be freed, but has state %d", i, addr, state)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify we can allocate new objects in the freed space
|
||||
newPtr := Alloc(unsafe.Sizeof(Node{}))
|
||||
if newPtr == nil {
|
||||
t.Error("Failed to allocate after freeing circular references")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !nogc
|
||||
//go:build !nogc && !baremetal
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build nogc
|
||||
//go:build nogc || baremetal
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !nogc
|
||||
// +build !nogc
|
||||
//go:build !nogc && !baremetal
|
||||
|
||||
/*
|
||||
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
|
||||
35
runtime/internal/runtime/z_gc_baremetal.go
Normal file
35
runtime/internal/runtime/z_gc_baremetal.go
Normal file
@@ -0,0 +1,35 @@
|
||||
//go:build !nogc && baremetal
|
||||
|
||||
/*
|
||||
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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"
|
||||
|
||||
"github.com/goplus/llgo/runtime/internal/runtime/tinygogc"
|
||||
)
|
||||
|
||||
// AllocU allocates uninitialized memory.
|
||||
func AllocU(size uintptr) unsafe.Pointer {
|
||||
return tinygogc.Alloc(size)
|
||||
}
|
||||
|
||||
// AllocZ allocates zero-initialized memory.
|
||||
func AllocZ(size uintptr) unsafe.Pointer {
|
||||
return tinygogc.Alloc(size)
|
||||
}
|
||||
@@ -31,6 +31,19 @@ func (pkg Package) AddGlobalString(name string, value string) {
|
||||
pkg.NewVarEx(name, prog.Pointer(styp)).Init(Expr{cv, styp})
|
||||
}
|
||||
|
||||
// ConstString creates an SSA expression representing a Go string literal. The
|
||||
// returned value is backed by an anonymous global constant and can be used to
|
||||
// initialize package-level variables or other constant contexts that expect a
|
||||
// Go string value.
|
||||
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
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
__stack = ORIGIN(dram_seg) + LENGTH(dram_seg);
|
||||
__MIN_STACK_SIZE = 0x1000;
|
||||
_stack_top = __stack;
|
||||
_heapEnd = ORIGIN(dram_seg) + LENGTH(dram_seg);
|
||||
|
||||
/* Default entry point */
|
||||
ENTRY(_start)
|
||||
@@ -94,6 +92,12 @@ SECTIONS
|
||||
_iram_end = .;
|
||||
} > iram_seg
|
||||
|
||||
.stack (NOLOAD) :
|
||||
{
|
||||
. += 16K;
|
||||
__stack = .;
|
||||
} > dram_seg
|
||||
|
||||
/**
|
||||
* This section is required to skip .iram0.text area because iram0_0_seg and
|
||||
* dram0_0_seg reflect the same address space on different buses.
|
||||
@@ -148,7 +152,7 @@ SECTIONS
|
||||
} > dram_seg
|
||||
|
||||
/* Check if data + heap + stack exceeds RAM limit */
|
||||
ASSERT(_end <= __stack - __MIN_STACK_SIZE, "region DRAM overflowed by .data and .bss sections")
|
||||
ASSERT(_end <= _heapEnd, "region DRAM overflowed by .data and .bss sections")
|
||||
|
||||
/* Stabs debugging sections. */
|
||||
.stab 0 : { *(.stab) }
|
||||
@@ -193,3 +197,8 @@ SECTIONS
|
||||
.gnu.attributes 0 : { KEEP (*(.gnu.attributes)) }
|
||||
/DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
|
||||
}
|
||||
|
||||
_globals_start = _data_start;
|
||||
_globals_end = _end;
|
||||
_heapStart = _end;
|
||||
_stack_top = __stack;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
__stack = ORIGIN(dram_seg) + LENGTH(dram_seg);
|
||||
__MIN_STACK_SIZE = 0x2000;
|
||||
_heapEnd = ORIGIN(dram_seg) + LENGTH(dram_seg);
|
||||
|
||||
ENTRY(_start)
|
||||
SECTIONS
|
||||
@@ -26,6 +25,14 @@ SECTIONS
|
||||
the same address within the page on the next page up. */
|
||||
. = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) & (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
|
||||
|
||||
.stack (NOLOAD) :
|
||||
{
|
||||
_stack_end = .;
|
||||
. = ALIGN(16);
|
||||
. += 16K;
|
||||
__stack = .;
|
||||
}
|
||||
|
||||
|
||||
.rodata :
|
||||
{
|
||||
@@ -116,7 +123,7 @@ SECTIONS
|
||||
. = DATA_SEGMENT_END (.);
|
||||
|
||||
/* Check if data + heap + stack exceeds RAM limit */
|
||||
ASSERT(. <= __stack - __MIN_STACK_SIZE, "region DRAM overflowed by .data and .bss sections")
|
||||
ASSERT(. <= _heapEnd, "region DRAM overflowed by .data and .bss sections")
|
||||
|
||||
/* Stabs debugging sections. */
|
||||
.stab 0 : { *(.stab) }
|
||||
@@ -165,4 +172,7 @@ SECTIONS
|
||||
|
||||
_sbss = __bss_start;
|
||||
_ebss = _end;
|
||||
|
||||
_globals_start = _data_start;
|
||||
_globals_end = _end;
|
||||
_heapStart = _end;
|
||||
_stack_top = __stack;
|
||||
|
||||
@@ -19,8 +19,8 @@ MEMORY
|
||||
/* 64k at the end of DRAM, after ROM bootloader stack
|
||||
* or entire DRAM (for QEMU only)
|
||||
*/
|
||||
dram_seg (RW) : org = 0x3FFF0000 ,
|
||||
len = 0x10000
|
||||
dram_seg (RW) : org = 0x3ffae000 ,
|
||||
len = 0x52000
|
||||
}
|
||||
|
||||
INCLUDE "targets/esp32.app.elf.ld";
|
||||
|
||||
60
xtool/env/llvm/llvm.go
vendored
60
xtool/env/llvm/llvm.go
vendored
@@ -17,9 +17,11 @@
|
||||
package llvm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/goplus/llgo/internal/env"
|
||||
@@ -108,4 +110,62 @@ func (e *Env) InstallNameTool() *install_name_tool.Cmd {
|
||||
return install_name_tool.New(bin)
|
||||
}
|
||||
|
||||
// Readelf returns a command to execute llvm-readelf with given arguments.
|
||||
func (e *Env) Readelf(args ...string) (*exec.Cmd, error) {
|
||||
path, err := e.toolPath("llvm-readelf")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return exec.Command(path, args...), nil
|
||||
}
|
||||
|
||||
func (e *Env) toolPath(base string) (string, error) {
|
||||
if tool := searchTool(e.binDir, base); tool != "" {
|
||||
return tool, nil
|
||||
}
|
||||
if tool, err := exec.LookPath(base); err == nil {
|
||||
return tool, nil
|
||||
}
|
||||
if tool := searchToolInPath(base); tool != "" {
|
||||
return tool, nil
|
||||
}
|
||||
return "", fmt.Errorf("%s not found", base)
|
||||
}
|
||||
|
||||
func searchTool(dir, base string) string {
|
||||
if dir == "" {
|
||||
return ""
|
||||
}
|
||||
candidate := filepath.Join(dir, base)
|
||||
if isExecutable(candidate) {
|
||||
return candidate
|
||||
}
|
||||
pattern := filepath.Join(dir, base+"-*")
|
||||
matches, _ := filepath.Glob(pattern)
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
|
||||
for _, match := range matches {
|
||||
if isExecutable(match) {
|
||||
return match
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func searchToolInPath(base string) string {
|
||||
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
|
||||
if tool := searchTool(dir, base); tool != "" {
|
||||
return tool
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isExecutable(path string) bool {
|
||||
if path == "" {
|
||||
return false
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user