Compare commits

...

44 Commits

Author SHA1 Message Date
xgopilot
ae690476ba fix: address code review feedback for size report feature
- Fix error handling priority: check waitErr first, then parseErr, then closeErr
- Optimize O(n²) symbol lookup by checking next symbol first
- Add ELF section constants (SHN_LORESERVE, SHN_ABS, etc.) and use them
- Fix documentation: add missing --elf-output-style=LLVM flag
- Fix documentation: correct field names from pkg.ID to pkg.PkgPath

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-17 08:15:12 +00:00
Li Jie
dc39b84187 fix size report parser for elf 2025-11-17 15:31:34 +08:00
Li Jie
faa5330b69 build: fix llvm-readelf call 2025-11-17 15:11:06 +08:00
xgopilot
8e5b34057e fix: address code review feedback for size report feature
- Add bounds checking for uint64→int conversion to prevent overflow
- Reduce max buffer size from 64MB to 4MB with documented constants
- Add comprehensive comments to symbol-to-size calculation algorithm
- Document moduleNameFromSymbol function and symbol naming conventions

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-17 06:34:23 +00:00
Li Jie
cee22db053 feat: add size level aggregation 2025-11-17 14:09:27 +08:00
xushiwei
b25ae1b4e7 Merge pull request #1384 from luoliwoshang/feature/export-different-names-1378
cl: support //export with different symbol names on embedded targets
2025-11-17 08:10:09 +08:00
xushiwei
a6516de181 Merge pull request #1309 from MeteorsLiu/impl-baremetal-gc
feat: implement baremetal gc
2025-11-17 07:41:31 +08:00
xushiwei
4a268650fc Merge pull request #1400 from cpunion/build/refactor-main-module-generation
build: refactor main module generation and fix stdout/stderr null checks
2025-11-17 07:34:03 +08:00
Li Jie
7e4e53eb1c build: refactor emitStdioNobuf for performance and readability 2025-11-16 20:17:30 +08:00
Li Jie
1473ee98f7 build: add package and function docs 2025-11-16 20:16:16 +08:00
xgopilot
8c7e8b6290 build: apply review feedback on main module generation 2025-11-16 20:16:16 +08:00
xgopilot
2a4b2ef023 build: remove error return from genMainModule
The genMainModule function never returns an error, so simplified
its signature to return only Package.

Updated:
- genMainModule signature: (Package, error) -> Package
- Call site in build.go to not handle error
- Both test cases to remove error checking

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-16 20:16:16 +08:00
xgopilot
7abb468592 fix: separate stdout and stderr null checks in emitStdioNobuf
The original code incorrectly used the stdout null check condition
for both stdout and stderr pointer selection. This caused incorrect
behavior when stderr is null but stdout is not, or vice-versa.

This fix separates the null checks for stdout and stderr into
independent conditions, ensuring each stream is properly selected
based on its own null status.

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-16 20:16:16 +08:00
Li Jie
bf9c6abb23 build: refactor main module generation 2025-11-16 20:16:16 +08:00
xushiwei
2d80951e7d Merge pull request #1396 from goplus/pr-1395
build: fix ldflags rewrites and prep caching
2025-11-15 07:36:31 +08:00
xgopilot
034b05c53c cl: remove Underlying() call to reject string type aliases in rewrites
Type aliases like `type T string` are no longer supported for
-ldflags -X rewrites. Only direct *string types are now allowed.

- Removed Underlying() call from isStringPtrType
- Added TestRewriteIgnoresStringAlias test case

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-14 14:49:30 +00:00
xgopilot
1ba7d1e561 fix: change to isStringPtrType for global string var
- Replace isStringType with isStringPtrType to properly validate that only *string type variables can be rewritten with -ldflags -X
- Remove maxStringTypeDepth constant as it's no longer needed
- Update tests to reflect the new function name and add test case for valid *string type
- Fix compileGlobal to use gbl.Type() for accurate type checking

This addresses the review feedback that Go only allows -X rewrites for the basic string type, not derived types like "type T string".

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-14 14:38:46 +00:00
xgopilot
d17ff2592a build: improve error handling and code quality
- Fix missing error handling in exportObject function
- Add explicit warning for non-string variable rewrites
- Improve documentation for maxRewriteValueLength constant

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-14 11:37:09 +00:00
Li Jie
4b26cccc90 cl: cover rewrite guards 2025-11-14 18:29:00 +08:00
Li Jie
3a1d8693e9 rewrite: address review feedback 2025-11-14 17:57:05 +08:00
Li Jie
2a52d422c5 cl: broaden rewrite coverage 2025-11-14 17:48:16 +08:00
Li Jie
b0f5d34b39 cl: add rewrite coverage test 2025-11-14 17:32:52 +08:00
Li Jie
8ba8ec71b5 build: write exports into temp files 2025-11-14 16:58:33 +08:00
Li Jie
1e4616a758 build: don't replace ExportFile 2025-11-14 16:58:33 +08:00
Li Jie
22a43622a0 cl: fix global var rewrite in alt pkg 2025-11-14 16:58:30 +08:00
Haolan
065126e270 feat: make defer tls stub for baremetal 2025-11-14 16:13:43 +08:00
Haolan
552156ff40 fix: adjust gc stats struct 2025-11-14 16:13:43 +08:00
Haolan
36e84196c6 test: add test for gc stats 2025-11-14 16:13:43 +08:00
Haolan
7f1e07755a ci: use llgo test instead
ci: use llgo test instead
2025-11-14 16:13:41 +08:00
Haolan
af27d0475d revert some unnecessary change 2025-11-14 16:13:41 +08:00
Haolan
bb29e8c768 docs: add commets for gc mutex
docs: add commets for tinygo gc
2025-11-14 16:13:38 +08:00
Haolan
eec2c271bd feat: replace println with gcPanic 2025-11-14 16:13:38 +08:00
Haolan
1ed924ed50 fix: add pthread GC support for baremetal
test: fix test logic

chore: format codes
2025-11-14 16:13:34 +08:00
Haolan
c46ca84122 revert disabling stdio buffer 2025-11-14 16:13:34 +08:00
Haolan
b36be05c1e fix: GC() signature 2025-11-14 16:13:34 +08:00
Haolan
66a537ad29 fix: add gc dummy mutex 2025-11-14 16:13:34 +08:00
Haolan
33a00dff1b fix: invalid import and improve tests
refactor: remove initGC

test: add test for GC

fix: invalid import in z_gc

ci: test baremetal GC for coverage

ci: test baremetal GC for coverage
2025-11-14 16:13:03 +08:00
Haolan
e4a69ce413 fix: disable buffers 2025-11-14 16:13:03 +08:00
Haolan
531f69ae6a fix: bdwgc.init() causing archive mode building fail
P

P

P
2025-11-14 16:13:03 +08:00
Haolan
812dfd45c9 feat: implement baremetal GC
fix: pthread gc

fix: xiao-esp32c3 symbol

refactor: use clite memset instead of linking

fix: stack top symbol
2025-11-14 16:12:56 +08:00
Li Jie
e2bb68489d build: override vars in alt pkg 2025-11-14 14:37:39 +08:00
Li Jie
9b76be9e9e support ldflags rewrites for initialized globals 2025-11-14 11:59:13 +08:00
luoliwoshang
e11ae0e21b test: add comprehensive tests and CI for export feature
Add extensive test coverage, demo program, and CI integration for
//export with different names feature:

Unit Tests (cl/builtin_test.go):
- TestHandleExportDiffName: core functionality with 4 scenarios
  * Different names with enableExportRename
  * Same names with enableExportRename
  * Different names with spaces in export directive
  * Matching names without enableExportRename
- TestInitLinknameByDocExportDiffNames: flag behavior verification
  * Export with different names when enabled
  * Export with same name when enabled
  * Normal linkname directives
- TestInitLinkExportDiffNames: edge case handling
  * Symbol not found in decl packages (silent handling)

Demo (_demo/embed/export/):
- Example program demonstrating various export patterns
- Verification script testing both embedded and non-embedded targets
- Documents expected behavior and error cases

CI Integration (.github/workflows/llgo.yml):
- Add export demo to embedded target tests
- Ensure feature works correctly across platforms
- Catch regressions in future changes

The tests verify:
✓ Different names work with -target flag
✓ Same names work in all cases
✓ Different names fail without -target flag
✓ Proper error messages for invalid exports
✓ Silent handling for decl packages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 18:52:58 +08:00
luoliwoshang
dac3365c73 cl,build: support //export with different names on embedded targets
Add support for using different C symbol names in //export directives
when compiling for embedded targets (enabled via -target flag).

This is for TinyGo compatibility on embedded platforms where hardware
interrupt handlers often require specific naming conventions.

Implementation:
- Add EnableExportRename() to control export rename behavior
- Add isExport parameter to initLink callback for context awareness
- Update initLinknameByDoc() to handle export rename logic:
  * When isExport && enableExportRename: allow different names
  * Otherwise: require name match
- Clean error handling in initLink():
  * export + enableExportRename: silent return (already processed)
  * export + !enableExportRename: panic with clear error message
  * other cases: warning for missing linkname

Example:
  //export LPSPI2_IRQHandler
  func interruptLPSPI2() { ... }

The Go function is named interruptLPSPI2 but exported as
LPSPI2_IRQHandler for the hardware vector table.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 18:52:36 +08:00
43 changed files with 3767 additions and 222 deletions

View File

@@ -61,7 +61,7 @@ jobs:
if ${{ startsWith(matrix.os, 'macos') }}; then if ${{ startsWith(matrix.os, 'macos') }}; then
DEMO_PKG="cargs_darwin_arm64.zip" DEMO_PKG="cargs_darwin_arm64.zip"
else else
DEMO_PKG="cargs_linux_amd64.zip" DEMO_PKG="cargs_linux_amd64.zip"
fi fi
mkdir -p ./_demo/c/cargs/libs mkdir -p ./_demo/c/cargs/libs
@@ -133,6 +133,13 @@ jobs:
chmod +x test.sh chmod +x test.sh
./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 - name: _xtool build tests
run: | run: |
cd _xtool cd _xtool
@@ -186,11 +193,15 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version: ${{matrix.go}} 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 - name: run llgo test
run: | run: |
llgo test ./... llgo test ./...
hello: hello:
continue-on-error: true continue-on-error: true
timeout-minutes: 30 timeout-minutes: 30

View 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"))
}

View 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."

View 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)
}

View 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())
}

View File

@@ -0,0 +1 @@
;

View File

@@ -393,10 +393,10 @@ func TestErrImport(t *testing.T) {
func TestErrInitLinkname(t *testing.T) { func TestErrInitLinkname(t *testing.T) {
var ctx context 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 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 return "", false, false
}) })
defer func() { defer func() {
@@ -404,7 +404,7 @@ func TestErrInitLinkname(t *testing.T) {
t.Fatal("initLinkname: no error?") 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" return "foo.Printf", false, name == "Printf"
}) })
} }
@@ -506,3 +506,238 @@ func TestInstantiate(t *testing.T) {
t.Fatal("error") 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
})
})
}
}

View File

@@ -53,6 +53,11 @@ var (
enableDbg bool enableDbg bool
enableDbgSyms bool enableDbgSyms bool
disableInline 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. // SetDebug sets debug flags.
@@ -73,6 +78,12 @@ func EnableTrace(b bool) {
enableCallTracing = b 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 { type instrOrValue interface {
@@ -127,6 +138,58 @@ type context struct {
cgoArgs []llssa.Expr cgoArgs []llssa.Expr
cgoRet llssa.Expr cgoRet llssa.Expr
cgoSymbols []string 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 type pkgState byte
@@ -176,7 +239,16 @@ func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) {
log.Println("==> NewVar", name, typ) log.Println("==> NewVar", name, typ)
} }
g := pkg.NewVar(name, typ, llssa.Background(vtype)) 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() g.InitNil()
} }
} }
@@ -816,6 +888,13 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
return 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) ptr := p.compileValue(b, va)
val := p.compileValue(b, v.Val) val := p.compileValue(b, v.Val)
b.Store(ptr, val) b.Store(ptr, val)
@@ -980,12 +1059,22 @@ type Patches = map[string]Patch
// NewPackage compiles a Go package to LLVM IR package. // 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) { 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 return
} }
// NewPackageEx compiles a Go package to LLVM IR package. // 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 pkgProg := pkg.Prog
pkgTypes := pkg.Pkg pkgTypes := pkg.Pkg
oldTypes := pkgTypes 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? types.Unsafe: {kind: PkgDeclOnly}, // TODO(xsw): PkgNoInit or PkgDeclOnly?
}, },
cgoSymbols: make([]string, 0, 128), cgoSymbols: make([]string, 0, 128),
rewrites: rewrites,
} }
ctx.initPyModule() ctx.initPyModule()
ctx.initFiles(pkgPath, files, pkgName == "C") ctx.initFiles(pkgPath, files, pkgName == "C")

View File

@@ -73,7 +73,7 @@ func (p *pkgSymInfo) initLinknames(ctx *context) {
lines := bytes.Split(b, sep) lines := bytes.Split(b, sep)
for _, line := range lines { for _, line := range lines {
if bytes.HasPrefix(line, commentPrefix) { 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 { if sym, ok := p.syms[inPkgName]; ok && file == sym.file {
return sym.fullName, sym.isVar, true return sym.fullName, sym.isVar, true
} }
@@ -277,8 +277,8 @@ func (p *context) initLinknameByDoc(doc *ast.CommentGroup, fullName, inPkgName s
if doc != nil { if doc != nil {
for n := len(doc.List) - 1; n >= 0; n-- { for n := len(doc.List) - 1; n >= 0; n-- {
line := doc.List[n].Text line := doc.List[n].Text
ret := p.initLinkname(line, func(name string) (_ string, _, ok bool) { ret := p.initLinkname(line, func(name string, isExport bool) (_ string, _, ok bool) {
return fullName, isVar, name == inPkgName return fullName, isVar, name == inPkgName || (isExport && enableExportRename)
}) })
if ret != unknownDirective { if ret != unknownDirective {
return ret == hasLinkname return ret == hasLinkname
@@ -294,7 +294,7 @@ const (
unknownDirective = -1 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 ( const (
linkname = "//go:linkname " linkname = "//go:linkname "
llgolink = "//llgo:link " llgolink = "//llgo:link "
@@ -324,17 +324,24 @@ func (p *context) initLinkname(line string, f func(inPkgName string) (fullName s
return noDirective 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:]) text := strings.TrimSpace(line[prefix:])
if idx := strings.IndexByte(text, ' '); idx > 0 { if idx := strings.IndexByte(text, ' '); idx > 0 {
inPkgName := text[:idx] inPkgName := text[:idx]
if fullName, _, ok := f(inPkgName); ok { if fullName, _, ok := f(inPkgName, export); ok {
link := strings.TrimLeft(text[idx+1:], " ") link := strings.TrimLeft(text[idx+1:], " ")
p.prog.SetLinkname(fullName, link) p.prog.SetLinkname(fullName, link)
if export { if export {
p.pkg.SetExport(fullName, link) p.pkg.SetExport(fullName, link)
} }
} else { } 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.Fprintln(os.Stderr, "==>", line)
fmt.Fprintf(os.Stderr, "llgo: linkname %s not found and ignored\n", inPkgName) fmt.Fprintf(os.Stderr, "llgo: linkname %s not found and ignored\n", inPkgName)
} }

169
cl/rewrite_internal_test.go Normal file
View 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)
}
}

View File

@@ -36,6 +36,9 @@ var CheckLinkArgs bool
var CheckLLFiles bool var CheckLLFiles bool
var GenLLFiles bool var GenLLFiles bool
var ForceEspClang bool var ForceEspClang bool
var SizeReport bool
var SizeFormat string
var SizeLevel string
func AddCommonFlags(fs *flag.FlagSet) { func AddCommonFlags(fs *flag.FlagSet) {
fs.BoolVar(&Verbose, "v", false, "Verbose output") 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(&GenLLFiles, "gen-llfiles", false, "generate .ll files for pkg export")
fs.BoolVar(&ForceEspClang, "force-espclang", false, "force to use esp-clang") 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) { func AddBuildModeFlags(fs *flag.FlagSet) {
@@ -79,6 +86,15 @@ func UpdateConfig(conf *build.Config) error {
conf.Target = Target conf.Target = Target
conf.Port = Port conf.Port = Port
conf.BaudRate = BaudRate conf.BaudRate = BaudRate
if SizeReport || SizeFormat != "" || SizeLevel != "" {
conf.SizeReport = true
if SizeFormat != "" {
conf.SizeFormat = SizeFormat
}
if SizeLevel != "" {
conf.SizeLevel = SizeLevel
}
}
switch conf.Mode { switch conf.Mode {
case build.ModeBuild: case build.ModeBuild:

58
doc/size-report.md Normal file
View 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`.

View File

@@ -132,10 +132,19 @@ type Config struct {
CheckLinkArgs bool // check linkargs valid CheckLinkArgs bool // check linkargs valid
ForceEspClang bool // force to use esp-clang ForceEspClang bool // force to use esp-clang
Tags string Tags string
GlobalNames map[string][]string // pkg => names SizeReport bool // print size report after successful build
GlobalDatas map[string]string // pkg.name => data 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 { func NewDefaultConf(mode Mode) *Config {
bin := os.Getenv("GOBIN") bin := os.Getenv("GOBIN")
if bin == "" { if bin == "" {
@@ -199,6 +208,15 @@ func Do(args []string, conf *Config) ([]Package, error) {
if conf.BuildMode == "" { if conf.BuildMode == "" {
conf.BuildMode = BuildModeExe 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 // Handle crosscompile configuration first to set correct GOOS/GOARCH
forceEspClang := conf.ForceEspClang || conf.Target != "" forceEspClang := conf.ForceEspClang || conf.Target != ""
export, err := crosscompile.Use(conf.Goos, conf.Goarch, conf.Target, IsWasiThreadsEnabled(), forceEspClang) 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 conf.Goarch = export.GOARCH
} }
// Enable different export names for TinyGo compatibility when using -target
if conf.Target != "" {
cl.EnableExportRename(true)
}
verbose := conf.Verbose verbose := conf.Verbose
patterns := args patterns := args
tags := "llgo,math_big_pure_go" tags := "llgo,math_big_pure_go"
@@ -335,6 +358,10 @@ func Do(args []string, conf *Config) ([]Package, error) {
crossCompile: export, crossCompile: export,
cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), 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) pkgs, err := buildAllPkgs(ctx, initial, verbose)
check(err) check(err)
if mode == ModeGen { if mode == ModeGen {
@@ -345,19 +372,11 @@ func Do(args []string, conf *Config) ([]Package, error) {
} }
return nil, fmt.Errorf("initial package not found") return nil, fmt.Errorf("initial package not found")
} }
dpkg, err := buildAllPkgs(ctx, altPkgs[noRt:], verbose) dpkg, err := buildAllPkgs(ctx, altPkgs[noRt:], verbose)
check(err) check(err)
allPkgs := append([]*aPackage{}, pkgs...) allPkgs := append([]*aPackage{}, pkgs...)
allPkgs = append(allPkgs, dpkg...) 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 { for _, pkg := range initial {
if needLink(pkg, mode) { if needLink(pkg, mode) {
name := path.Base(pkg.PkgPath) 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 // 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 { if err != nil {
return nil, err 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 // Generate C headers for c-archive and c-shared modes before linking
if ctx.buildConf.BuildMode == BuildModeCArchive || ctx.buildConf.BuildMode == BuildModeCShared { 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") 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) { 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, "=") eq := strings.Index(arg, "=")
dot := strings.LastIndex(arg[:eq+1], ".") dot := strings.LastIndex(arg[:eq+1], ".")
if eq < 0 || dot < 0 { if eq < 0 || dot < 0 {
panic(errXflags) panic(errXflags)
} }
pkg := arg[:dot] pkg := arg[:dot]
varName := arg[dot+1 : eq]
value := arg[eq+1:]
validateRewriteInput(pkg, varName, value)
pkgs := []string{pkg} pkgs := []string{pkg}
if pkg == "main" { if pkg == "main" {
pkgs = mainPkgs pkgs = mainPkgs
} }
if conf.GlobalNames == nil { if len(pkgs) == 0 {
conf.GlobalNames = make(map[string][]string) return
} }
if conf.GlobalDatas == nil { if conf.GlobalRewrites == nil {
conf.GlobalDatas = make(map[string]string) conf.GlobalRewrites = make(map[string]Rewrites)
} }
for _, pkg := range pkgs { for _, realPkg := range pkgs {
name := pkg + arg[dot:eq] vars := conf.GlobalRewrites[realPkg]
value := arg[eq+1:] if vars == nil {
if _, ok := conf.GlobalDatas[name]; !ok { vars = make(Rewrites)
conf.GlobalNames[pkg] = append(conf.GlobalNames[pkg], name) 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) { func validateRewriteInput(pkg, varName, value string) {
if len(ctx.buildConf.GlobalDatas) == 0 { if pkg == "" || strings.ContainsAny(pkg, " \t\r\n") {
return nil, nil panic(fmt.Errorf("invalid package path for rewrite: %q", pkg))
} }
for _, pkg := range pkgs { if !token.IsIdentifier(varName) {
if pkg.ExportFile == "" { panic(fmt.Errorf("invalid variable name for rewrite: %q", varName))
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
}
}
} }
global := prog.NewPackage("", "global") if len(value) > maxRewriteValueLength {
for name, value := range ctx.buildConf.GlobalDatas { panic(fmt.Errorf("rewrite value too large: %d bytes", len(value)))
global.AddGlobalString(name, value)
} }
return global, nil
} }
// compileExtraFiles compiles extra files (.s/.c) from target configuration and returns object files // 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 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 needRuntime := false
needPyInit := 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 if p.ExportFile != "" && aPkg != nil { // skip packages that only contain declarations
linkArgs = append(linkArgs, aPkg.LinkArgs...) linkArgs = append(linkArgs, aPkg.LinkArgs...)
objFiles = append(objFiles, aPkg.LLFiles...) objFiles = append(objFiles, aPkg.LLFiles...)
objFiles = append(objFiles, aPkg.ExportFile)
need1, need2 := isNeedRuntimeOrPyInit(ctx, p) need1, need2 := isNeedRuntimeOrPyInit(ctx, p)
if !needRuntime { if !needRuntime {
needRuntime = need1 needRuntime = need1
@@ -780,11 +804,11 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
} }
}) })
// Generate main module file (needed for global variables even in library modes) // Generate main module file (needed for global variables even in library modes)
entryObjFile, err := genMainModuleFile(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit) entryPkg := genMainModule(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit)
entryObjFile, err := exportObject(ctx, entryPkg.PkgPath, entryPkg.ExportFile, []byte(entryPkg.LPkg.String()))
if err != nil { if err != nil {
return err return err
} }
// defer os.Remove(entryLLFile)
objFiles = append(objFiles, entryObjFile) objFiles = append(objFiles, entryObjFile)
// Compile extra files from target configuration // Compile extra files from target configuration
@@ -794,14 +818,6 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
} }
objFiles = append(objFiles, extraObjFiles...) 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() { if IsFullRpathEnabled() {
// Treat every link-time library search path, specified by the -L parameter, as a runtime search path as well. // 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 // This is to ensure the final executable can locate libraries with a relocatable install_name
@@ -914,118 +930,6 @@ func needStart(ctx *context) bool {
} }
} }
func genMainModuleFile(ctx *context, rtPkgPath string, pkg *packages.Package, needRuntime, needPyInit bool) (path string, err error) {
var (
pyInitDecl string
pyInit string
rtInitDecl string
rtInit string
)
mainPkgPath := pkg.PkgPath
if needRuntime {
rtInit = "call void @\"" + rtPkgPath + ".init\"()"
rtInitDecl = "declare void @\"" + rtPkgPath + ".init\"()"
}
if needPyInit {
pyInit = "call void @Py_Initialize()"
pyInitDecl = "declare void @Py_Initialize()"
}
declSizeT := "%size_t = type i64"
if is32Bits(ctx.buildConf.Goarch) {
declSizeT = "%size_t = type i32"
}
stdioDecl := ""
stdioNobuf := ""
if IsStdioNobuf() {
stdioDecl = `
@stdout = external global ptr
@stderr = external global ptr
@__stdout = external global ptr
@__stderr = external global ptr
declare i32 @setvbuf(ptr, ptr, i32, %size_t)
`
stdioNobuf = `
; Set stdout with no buffer
%stdout_is_null = icmp eq ptr @stdout, null
%stdout_ptr = select i1 %stdout_is_null, ptr @__stdout, ptr @stdout
call i32 @setvbuf(ptr %stdout_ptr, ptr null, i32 2, %size_t 0)
; Set stderr with no buffer
%stderr_ptr = select i1 %stdout_is_null, ptr @__stderr, ptr @stderr
call i32 @setvbuf(ptr %stderr_ptr, ptr null, i32 2, %size_t 0)
`
}
// TODO(lijie): workaround for libc-free
// Remove main/_start when -buildmode and libc are ready
startDefine := `
define weak void @_start() {
; argc = 0
%argc = add i32 0, 0
; argv = null
%argv = inttoptr i64 0 to i8**
call i32 @main(i32 %argc, i8** %argv)
ret void
}
`
mainDefine := "define i32 @main(i32 noundef %0, ptr nocapture noundef readnone %1) local_unnamed_addr"
if !needStart(ctx) && isWasmTarget(ctx.buildConf.Goos) {
mainDefine = "define hidden noundef i32 @__main_argc_argv(i32 noundef %0, ptr nocapture noundef readnone %1) local_unnamed_addr"
}
if !needStart(ctx) {
startDefine = ""
}
var mainCode string
// For library modes (c-archive, c-shared), only generate global variables
if ctx.buildConf.BuildMode != BuildModeExe {
mainCode = `; ModuleID = 'main'
source_filename = "main"
@__llgo_argc = global i32 0, align 4
@__llgo_argv = global ptr null, align 8
`
} else {
// For executable mode, generate full main function
mainCode = fmt.Sprintf(`; ModuleID = 'main'
source_filename = "main"
%s
@__llgo_argc = global i32 0, align 4
@__llgo_argv = global ptr null, align 8
%s
%s
%s
declare void @"%s.init"()
declare void @"%s.main"()
define weak void @runtime.init() {
ret void
}
; TODO(lijie): workaround for syscall patch
define weak void @"syscall.init"() {
ret void
}
%s
%s {
_llgo_0:
store i32 %%0, ptr @__llgo_argc, align 4
store ptr %%1, ptr @__llgo_argv, align 8
%s
%s
%s
call void @runtime.init()
call void @"%s.init"()
call void @"%s.main"()
ret i32 0
}
`, declSizeT, stdioDecl,
pyInitDecl, rtInitDecl, mainPkgPath, mainPkgPath,
startDefine, mainDefine, stdioNobuf,
pyInit, rtInit, mainPkgPath, mainPkgPath)
}
return exportObject(ctx, pkg.PkgPath+".main", pkg.ExportFile+"-main", []byte(mainCode))
}
func is32Bits(goarch string) bool { func is32Bits(goarch string) bool {
return goarch == "386" || goarch == "arm" || goarch == "mips" || goarch == "wasm" return goarch == "386" || goarch == "arm" || goarch == "mips" || goarch == "wasm"
} }
@@ -1050,7 +954,7 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error {
cl.SetDebug(cl.DbgFlagAll) 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 { if showDetail {
llssa.SetDebug(0) llssa.SetDebug(0)
cl.SetDebug(0) cl.SetDebug(0)
@@ -1077,10 +981,11 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error {
aPkg.LinkArgs = append(aPkg.LinkArgs, altLdflags...) aPkg.LinkArgs = append(aPkg.LinkArgs, altLdflags...)
} }
if pkg.ExportFile != "" { 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 { if err != nil {
return fmt.Errorf("export object of %v failed: %v", pkgPath, err) return fmt.Errorf("export object of %v failed: %v", pkgPath, err)
} }
aPkg.LLFiles = append(aPkg.LLFiles, exportFile)
if debugBuild || verbose { if debugBuild || verbose {
fmt.Fprintf(os.Stderr, "==> Export %s: %s\n", aPkg.PkgPath, pkg.ExportFile) fmt.Fprintf(os.Stderr, "==> Export %s: %s\n", aPkg.PkgPath, pkg.ExportFile)
} }
@@ -1089,11 +994,15 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error {
} }
func exportObject(ctx *context, pkgPath string, exportFile string, data []byte) (string, 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 { if err != nil {
return "", err return "", err
} }
f.Write(data) if _, err := f.Write(data); err != nil {
f.Close()
return "", err
}
err = f.Close() err = f.Close()
if err != nil { if err != nil {
return exportFile, err return exportFile, err
@@ -1111,13 +1020,17 @@ func exportObject(ctx *context, pkgPath string, exportFile string, data []byte)
} }
return exportFile, os.Rename(f.Name(), exportFile) return exportFile, os.Rename(f.Name(), exportFile)
} }
exportFile += ".o" objFile, err := os.CreateTemp("", base+"-*.o")
args := []string{"-o", exportFile, "-c", f.Name(), "-Wno-override-module"} if err != nil {
return "", err
}
objFile.Close()
args := []string{"-o", objFile.Name(), "-c", f.Name(), "-Wno-override-module"}
if ctx.buildConf.Verbose { if ctx.buildConf.Verbose {
fmt.Fprintln(os.Stderr, "clang", args) fmt.Fprintln(os.Stderr, "clang", args)
} }
cmd := ctx.compiler() 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) { func llcCheck(env *llvm.Env, exportFile string) (msg string, err error) {
@@ -1171,8 +1084,9 @@ type aPackage struct {
AltPkg *packages.Cached AltPkg *packages.Cached
LPkg llssa.Package LPkg llssa.Package
LinkArgs []string LinkArgs []string
LLFiles []string LLFiles []string
rewriteVars map[string]string
} }
type Package = *aPackage type Package = *aPackage
@@ -1193,7 +1107,8 @@ func allPkgs(ctx *context, initial []*packages.Package, verbose bool) (all []*aP
return 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 { } else {
errs = append(errs, p) errs = append(errs, p)
} }
@@ -1201,6 +1116,32 @@ func allPkgs(ctx *context, initial []*packages.Package, verbose bool) (all []*aP
return 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 { func createSSAPkg(prog *ssa.Program, p *packages.Package, verbose bool) *ssa.Package {
pkgSSA := prog.ImportedPackage(p.ID) pkgSSA := prog.ImportedPackage(p.ID)
if pkgSSA == nil { if pkgSSA == nil {

View File

@@ -8,6 +8,9 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"path/filepath"
"runtime"
"testing" "testing"
"github.com/goplus/llgo/internal/mockable" "github.com/goplus/llgo/internal/mockable"
@@ -94,4 +97,70 @@ func TestCmpTest(t *testing.T) {
mockRun([]string{"../../cl/_testgo/runtest"}, &Config{Mode: ModeCmpTest}) 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})
}

View File

@@ -0,0 +1,231 @@
//go:build !llgo
// +build !llgo
/*
* 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 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 (
"go/token"
"go/types"
"github.com/goplus/llgo/internal/packages"
llvm "github.com/goplus/llvm"
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")
argcVar := mainPkg.NewVarEx("__llgo_argc", prog.Pointer(prog.Int32()))
argcVar.Init(prog.Zero(prog.Int32()))
argvValueType := prog.Pointer(prog.CStr())
argvVar := mainPkg.NewVarEx("__llgo_argv", prog.Pointer(argvValueType))
argvVar.InitNil()
exportFile := pkg.ExportFile
if exportFile == "" {
exportFile = pkg.PkgPath
}
mainAPkg := &aPackage{
Package: &packages.Package{
PkgPath: pkg.PkgPath + ".main",
ExportFile: exportFile + "-main",
},
LPkg: mainPkg,
}
if ctx.buildConf.BuildMode != BuildModeExe {
return mainAPkg
}
runtimeStub := defineWeakNoArgStub(mainPkg, "runtime.init")
// TODO(lijie): workaround for syscall patch
defineWeakNoArgStub(mainPkg, "syscall.init")
var pyInit llssa.Function
if needPyInit {
pyInit = declareNoArgFunc(mainPkg, "Py_Initialize")
}
var rtInit llssa.Function
if needRuntime {
rtInit = declareNoArgFunc(mainPkg, rtPkgPath+".init")
}
mainInit := declareNoArgFunc(mainPkg, pkg.PkgPath+".init")
mainMain := declareNoArgFunc(mainPkg, pkg.PkgPath+".main")
entryFn := defineEntryFunction(ctx, mainPkg, argcVar, argvVar, argvValueType, runtimeStub, mainInit, mainMain, pyInit, rtInit)
if needStart(ctx) {
defineStart(mainPkg, entryFn, argvValueType)
}
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"
if !needStart(ctx) && isWasmTarget(ctx.buildConf.Goos) {
entryName = "__main_argc_argv"
}
sig := newEntrySignature(argvType.RawType())
fn := pkg.NewFunc(entryName, sig, llssa.InC)
fnVal := pkg.Module().NamedFunction(entryName)
if entryName != "main" {
fnVal.SetVisibility(llvm.HiddenVisibility)
fnVal.SetUnnamedAddr(true)
}
b := fn.MakeBody(1)
b.Store(argcVar.Expr, fn.Param(0))
b.Store(argvVar.Expr, fn.Param(1))
if IsStdioNobuf() {
emitStdioNobuf(b, pkg, ctx.buildConf.Goos)
}
if pyInit != nil {
b.Call(pyInit.Expr)
}
if rtInit != nil {
b.Call(rtInit.Expr)
}
b.Call(runtimeStub.Expr)
b.Call(mainInit.Expr)
b.Call(mainMain.Expr)
b.Return(prog.IntVal(0, prog.Int32()))
return fn
}
func defineStart(pkg llssa.Package, entry llssa.Function, argvType llssa.Type) {
fn := pkg.NewFunc("_start", llssa.NoArgsNoRet, llssa.InC)
pkg.Module().NamedFunction("_start").SetLinkage(llvm.WeakAnyLinkage)
b := fn.MakeBody(1)
prog := pkg.Prog
b.Call(entry.Expr, prog.IntVal(0, prog.Int32()), prog.Nil(argvType))
b.Return()
}
func declareNoArgFunc(pkg llssa.Package, name string) llssa.Function {
return pkg.NewFunc(name, llssa.NoArgsNoRet, llssa.InC)
}
func defineWeakNoArgStub(pkg llssa.Package, name string) llssa.Function {
fn := pkg.NewFunc(name, llssa.NoArgsNoRet, llssa.InC)
pkg.Module().NamedFunction(name).SetLinkage(llvm.WeakAnyLinkage)
b := fn.MakeBody(1)
b.Return()
return fn
}
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)
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)
noBufMode := prog.IntVal(ioNoBuf, prog.Int32())
zeroSize := prog.Zero(sizeType)
nullBuf := prog.Nil(prog.CStr())
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 {
global := pkg.NewVarEx(name, valueType)
pkg.Module().NamedGlobal(name).SetLinkage(llvm.ExternalLinkage)
return global.Expr
}
func declareSetvbuf(pkg llssa.Package, streamPtrType, bufPtrType, intType, sizeType llssa.Type) llssa.Function {
sig := newSignature(
[]types.Type{
streamPtrType.RawType(),
bufPtrType.RawType(),
intType.RawType(),
sizeType.RawType(),
},
[]types.Type{intType.RawType()},
)
return pkg.NewFunc("setvbuf", sig, llssa.InC)
}
func tupleOf(tys ...types.Type) *types.Tuple {
if len(tys) == 0 {
return types.NewTuple()
}
vars := make([]*types.Var, len(tys))
for i, t := range tys {
vars[i] = types.NewParam(token.NoPos, nil, "", t)
}
return types.NewTuple(vars...)
}
func newSignature(params []types.Type, results []types.Type) *types.Signature {
return types.NewSignatureType(nil, nil, nil, tupleOf(params...), tupleOf(results...), false)
}
func newEntrySignature(argvType types.Type) *types.Signature {
return newSignature(
[]types.Type{types.Typ[types.Int32], argvType},
[]types.Type{types.Typ[types.Int32]},
)
}

View File

@@ -0,0 +1,70 @@
//go:build !llgo
// +build !llgo
package build
import (
"strings"
"testing"
"github.com/goplus/llvm"
"github.com/goplus/llgo/internal/packages"
llssa "github.com/goplus/llgo/ssa"
)
func init() {
llssa.Initialize(llssa.InitAll)
}
func TestGenMainModuleExecutable(t *testing.T) {
llvm.InitializeAllTargets()
t.Setenv(llgoStdioNobuf, "")
ctx := &context{
prog: llssa.NewProgram(nil),
buildConf: &Config{
BuildMode: BuildModeExe,
Goos: "linux",
Goarch: "amd64",
},
}
pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"}
mod := genMainModule(ctx, llssa.PkgRuntime, pkg, true, true)
if mod.ExportFile != "foo.a-main" {
t.Fatalf("unexpected export file: %s", mod.ExportFile)
}
ir := mod.LPkg.String()
checks := []string{
"define i32 @main(",
"call void @Py_Initialize()",
"call void @\"example.com/foo.init\"()",
"define weak void @_start()",
}
for _, want := range checks {
if !strings.Contains(ir, want) {
t.Fatalf("main module IR missing %q:\n%s", want, ir)
}
}
}
func TestGenMainModuleLibrary(t *testing.T) {
llvm.InitializeAllTargets()
t.Setenv(llgoStdioNobuf, "")
ctx := &context{
prog: llssa.NewProgram(nil),
buildConf: &Config{
BuildMode: BuildModeCArchive,
Goos: "linux",
Goarch: "amd64",
},
}
pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"}
mod := genMainModule(ctx, llssa.PkgRuntime, pkg, false, false)
ir := mod.LPkg.String()
if strings.Contains(ir, "define i32 @main") {
t.Fatalf("library mode should not emit main function:\n%s", ir)
}
if !strings.Contains(ir, "@__llgo_argc = global i32 0") {
t.Fatalf("library mode missing argc global:\n%s", ir)
}
}

109
internal/build/resolver.go Normal file
View 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
}

View 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 = &sectionInfo{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
}

View 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)
}
}

View File

@@ -1,5 +1,4 @@
//go:build !nogc //go:build !nogc && !baremetal
// +build !nogc
/* /*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -1,5 +1,4 @@
//go:build nogc //go:build nogc || baremetal
// +build nogc
/* /*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -1,4 +1,4 @@
//go:build llgo //go:build llgo && !baremetal
/* /*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -1,4 +1,4 @@
//go:build llgo && !nogc //go:build llgo && !baremetal && !nogc
/* /*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -1,4 +1,4 @@
//go:build llgo && nogc //go:build llgo && (nogc || baremetal)
/* /*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -1,4 +1,4 @@
//go:build !llgo //go:build !llgo || baremetal
/* /*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -4,8 +4,6 @@
package runtime package runtime
import "runtime"
// Layout of in-memory per-function information prepared by linker // Layout of in-memory per-function information prepared by linker
// See https://golang.org/s/go12symtab. // See https://golang.org/s/go12symtab.
// Keep in sync with linker (../cmd/link/internal/ld/pcln.go:/pclntab) // Keep in sync with linker (../cmd/link/internal/ld/pcln.go:/pclntab)
@@ -30,10 +28,6 @@ func StopTrace() {
panic("todo: runtime.StopTrace") panic("todo: runtime.StopTrace")
} }
func ReadMemStats(m *runtime.MemStats) {
panic("todo: runtime.ReadMemStats")
}
func SetMutexProfileFraction(rate int) int { func SetMutexProfileFraction(rate int) int {
panic("todo: runtime.SetMutexProfileFraction") panic("todo: runtime.SetMutexProfileFraction")
} }

View File

@@ -1,8 +1,16 @@
//go:build !nogc //go:build !nogc && !baremetal
package runtime 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() { func GC() {
bdwgc.Gcollect() bdwgc.Gcollect()

View 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()
}

View 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
}

View 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

View 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

View 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).
}

View 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) {}

View 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")
}
}

View File

@@ -1,4 +1,4 @@
//go:build !nogc //go:build !nogc && !baremetal
/* /*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -1,4 +1,4 @@
//go:build nogc //go:build nogc || baremetal
/* /*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -1,5 +1,4 @@
//go:build !nogc //go:build !nogc && !baremetal
// +build !nogc
/* /*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.

View 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)
}

View File

@@ -31,6 +31,19 @@ func (pkg Package) AddGlobalString(name string, value string) {
pkg.NewVarEx(name, prog.Pointer(styp)).Init(Expr{cv, styp}) 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 // Undefined global string var by names
func (pkg Package) Undefined(names ...string) error { func (pkg Package) Undefined(names ...string) error {
prog := pkg.Prog prog := pkg.Prog

View File

@@ -1,6 +1,4 @@
__stack = ORIGIN(dram_seg) + LENGTH(dram_seg); _heapEnd = ORIGIN(dram_seg) + LENGTH(dram_seg);
__MIN_STACK_SIZE = 0x1000;
_stack_top = __stack;
/* Default entry point */ /* Default entry point */
ENTRY(_start) ENTRY(_start)
@@ -94,6 +92,12 @@ SECTIONS
_iram_end = .; _iram_end = .;
} > iram_seg } > iram_seg
.stack (NOLOAD) :
{
. += 16K;
__stack = .;
} > dram_seg
/** /**
* This section is required to skip .iram0.text area because iram0_0_seg and * 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. * dram0_0_seg reflect the same address space on different buses.
@@ -148,7 +152,7 @@ SECTIONS
} > dram_seg } > dram_seg
/* Check if data + heap + stack exceeds RAM limit */ /* 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. */ /* Stabs debugging sections. */
.stab 0 : { *(.stab) } .stab 0 : { *(.stab) }
@@ -193,3 +197,8 @@ SECTIONS
.gnu.attributes 0 : { KEEP (*(.gnu.attributes)) } .gnu.attributes 0 : { KEEP (*(.gnu.attributes)) }
/DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) } /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
} }
_globals_start = _data_start;
_globals_end = _end;
_heapStart = _end;
_stack_top = __stack;

View File

@@ -1,5 +1,4 @@
__stack = ORIGIN(dram_seg) + LENGTH(dram_seg); _heapEnd = ORIGIN(dram_seg) + LENGTH(dram_seg);
__MIN_STACK_SIZE = 0x2000;
ENTRY(_start) ENTRY(_start)
SECTIONS SECTIONS
@@ -26,6 +25,14 @@ SECTIONS
the same address within the page on the next page up. */ 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)); . = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) & (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
.stack (NOLOAD) :
{
_stack_end = .;
. = ALIGN(16);
. += 16K;
__stack = .;
}
.rodata : .rodata :
{ {
@@ -116,7 +123,7 @@ SECTIONS
. = DATA_SEGMENT_END (.); . = DATA_SEGMENT_END (.);
/* Check if data + heap + stack exceeds RAM limit */ /* 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. */ /* Stabs debugging sections. */
.stab 0 : { *(.stab) } .stab 0 : { *(.stab) }
@@ -165,4 +172,7 @@ SECTIONS
_sbss = __bss_start; _sbss = __bss_start;
_ebss = _end; _ebss = _end;
_globals_start = _data_start;
_globals_end = _end;
_heapStart = _end;
_stack_top = __stack;

View File

@@ -19,8 +19,8 @@ MEMORY
/* 64k at the end of DRAM, after ROM bootloader stack /* 64k at the end of DRAM, after ROM bootloader stack
* or entire DRAM (for QEMU only) * or entire DRAM (for QEMU only)
*/ */
dram_seg (RW) : org = 0x3FFF0000 , dram_seg (RW) : org = 0x3ffae000 ,
len = 0x10000 len = 0x52000
} }
INCLUDE "targets/esp32.app.elf.ld"; INCLUDE "targets/esp32.app.elf.ld";

View File

@@ -17,9 +17,11 @@
package llvm package llvm
import ( import (
"fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"github.com/goplus/llgo/internal/env" "github.com/goplus/llgo/internal/env"
@@ -108,4 +110,62 @@ func (e *Env) InstallNameTool() *install_name_tool.Cmd {
return install_name_tool.New(bin) 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()
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------