diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 124b2cd2..e3a47099 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -133,6 +133,13 @@ jobs: chmod +x test.sh ./test.sh + - name: Test export with different symbol names on embedded targets + run: | + echo "Testing //export with different symbol names on embedded targets..." + cd _demo/embed/export + chmod +x verify_export.sh + ./verify_export.sh + - name: _xtool build tests run: | cd _xtool diff --git a/_demo/embed/export/main.go b/_demo/embed/export/main.go new file mode 100644 index 00000000..0fcdcce1 --- /dev/null +++ b/_demo/embed/export/main.go @@ -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")) +} diff --git a/_demo/embed/export/verify_export.sh b/_demo/embed/export/verify_export.sh new file mode 100755 index 00000000..41dd62b1 --- /dev/null +++ b/_demo/embed/export/verify_export.sh @@ -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." diff --git a/cl/builtin_test.go b/cl/builtin_test.go index a07e5f39..544b146f 100644 --- a/cl/builtin_test.go +++ b/cl/builtin_test.go @@ -393,10 +393,10 @@ func TestErrImport(t *testing.T) { func TestErrInitLinkname(t *testing.T) { var ctx context - ctx.initLinkname("//llgo:link abc", func(name string) (string, bool, bool) { + ctx.initLinkname("//llgo:link abc", func(name string, isExport bool) (string, bool, bool) { return "", false, false }) - ctx.initLinkname("//go:linkname Printf printf", func(name string) (string, bool, bool) { + ctx.initLinkname("//go:linkname Printf printf", func(name string, isExport bool) (string, bool, bool) { return "", false, false }) defer func() { @@ -404,7 +404,7 @@ func TestErrInitLinkname(t *testing.T) { t.Fatal("initLinkname: no error?") } }() - ctx.initLinkname("//go:linkname Printf printf", func(name string) (string, bool, bool) { + ctx.initLinkname("//go:linkname Printf printf", func(name string, isExport bool) (string, bool, bool) { return "foo.Printf", false, name == "Printf" }) } @@ -506,3 +506,238 @@ func TestInstantiate(t *testing.T) { t.Fatal("error") } } + +func TestHandleExportDiffName(t *testing.T) { + tests := []struct { + name string + enableExportRename bool + line string + fullName string + inPkgName string + wantHasLinkname bool + wantLinkname string + wantExport string + }{ + { + name: "ExportDiffNames_DifferentName", + enableExportRename: true, + line: "//export IRQ_Handler", + fullName: "pkg.HandleInterrupt", + inPkgName: "HandleInterrupt", + wantHasLinkname: true, + wantLinkname: "IRQ_Handler", + wantExport: "IRQ_Handler", + }, + { + name: "ExportDiffNames_SameName", + enableExportRename: true, + line: "//export SameName", + fullName: "pkg.SameName", + inPkgName: "SameName", + wantHasLinkname: true, + wantLinkname: "SameName", + wantExport: "SameName", + }, + { + name: "ExportDiffNames_WithSpaces", + enableExportRename: true, + line: "//export Timer_Callback ", + fullName: "pkg.OnTimerTick", + inPkgName: "OnTimerTick", + wantHasLinkname: true, + wantLinkname: "Timer_Callback", + wantExport: "Timer_Callback", + }, + { + name: "ExportDiffNames_Disabled_MatchingName", + enableExportRename: false, + line: "//export Func", + fullName: "pkg.Func", + inPkgName: "Func", + wantHasLinkname: true, + wantLinkname: "Func", + wantExport: "Func", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save and restore global state + oldEnableExportRename := enableExportRename + defer func() { + EnableExportRename(oldEnableExportRename) + }() + EnableExportRename(tt.enableExportRename) + + // Setup context + prog := llssa.NewProgram(nil) + pkg := prog.NewPackage("test", "test") + ctx := &context{ + prog: prog, + pkg: pkg, + } + + // Call initLinkname with closure that mimics initLinknameByDoc behavior + ret := ctx.initLinkname(tt.line, func(name string, isExport bool) (string, bool, bool) { + return tt.fullName, false, name == tt.inPkgName || (isExport && enableExportRename) + }) + + // Verify result + hasLinkname := (ret == hasLinkname) + if hasLinkname != tt.wantHasLinkname { + t.Errorf("hasLinkname = %v, want %v", hasLinkname, tt.wantHasLinkname) + } + + if tt.wantHasLinkname { + // Check linkname was set + if link, ok := prog.Linkname(tt.fullName); !ok || link != tt.wantLinkname { + t.Errorf("linkname = %q (ok=%v), want %q", link, ok, tt.wantLinkname) + } + + // Check export was set + exports := pkg.ExportFuncs() + if export, ok := exports[tt.fullName]; !ok || export != tt.wantExport { + t.Errorf("export = %q (ok=%v), want %q", export, ok, tt.wantExport) + } + } + }) + } +} + +func TestInitLinknameByDocExportDiffNames(t *testing.T) { + tests := []struct { + name string + enableExportRename bool + doc *ast.CommentGroup + fullName string + inPkgName string + wantExported bool // Whether the symbol should be exported with different name + wantLinkname string + wantExport string + }{ + { + name: "WithExportDiffNames_DifferentNameExported", + enableExportRename: true, + doc: &ast.CommentGroup{ + List: []*ast.Comment{ + {Text: "//export IRQ_Handler"}, + }, + }, + fullName: "pkg.HandleInterrupt", + inPkgName: "HandleInterrupt", + wantExported: true, + wantLinkname: "IRQ_Handler", + wantExport: "IRQ_Handler", + }, + { + name: "WithoutExportDiffNames_NotExported", + enableExportRename: false, + doc: &ast.CommentGroup{ + List: []*ast.Comment{ + {Text: "//export DifferentName"}, + }, + }, + fullName: "pkg.HandleInterrupt", + inPkgName: "HandleInterrupt", + wantExported: false, + // Without enableExportRename, it goes through normal flow which expects same name + // The symbol "DifferentName" won't be found, so no export happens + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Without enableExportRename, export with different name will panic + if !tt.wantExported && !tt.enableExportRename { + defer func() { + if r := recover(); r == nil { + t.Error("expected panic for export with different name when enableExportRename=false") + } + }() + } + + // Save and restore global state + oldEnableExportRename := enableExportRename + defer func() { + EnableExportRename(oldEnableExportRename) + }() + EnableExportRename(tt.enableExportRename) + + // Setup context + prog := llssa.NewProgram(nil) + pkg := prog.NewPackage("test", "test") + ctx := &context{ + prog: prog, + pkg: pkg, + } + + // Call initLinknameByDoc + ctx.initLinknameByDoc(tt.doc, tt.fullName, tt.inPkgName, false) + + // Verify export behavior + exports := pkg.ExportFuncs() + if tt.wantExported { + // Should have exported the symbol with different name + if export, ok := exports[tt.fullName]; !ok || export != tt.wantExport { + t.Errorf("export = %q (ok=%v), want %q", export, ok, tt.wantExport) + } + // Check linkname was also set + if link, ok := prog.Linkname(tt.fullName); !ok || link != tt.wantLinkname { + t.Errorf("linkname = %q (ok=%v), want %q", link, ok, tt.wantLinkname) + } + } + }) + } +} + +func TestInitLinkExportDiffNames(t *testing.T) { + tests := []struct { + name string + enableExportRename bool + line string + wantPanic bool + }{ + { + name: "ExportDiffNames_Enabled_NoError", + enableExportRename: true, + line: "//export IRQ_Handler", + wantPanic: false, + }, + { + name: "ExportDiffNames_Disabled_Panic", + enableExportRename: false, + line: "//export IRQ_Handler", + wantPanic: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Error("expected panic but didn't panic") + } + }() + } + + oldEnableExportRename := enableExportRename + defer func() { + EnableExportRename(oldEnableExportRename) + }() + EnableExportRename(tt.enableExportRename) + + prog := llssa.NewProgram(nil) + pkg := prog.NewPackage("test", "test") + ctx := &context{ + prog: prog, + pkg: pkg, + } + + ctx.initLinkname(tt.line, func(inPkgName string, isExport bool) (fullName string, isVar, ok bool) { + // Simulate initLinknames scenario: symbol not found (like in decl packages) + return "", false, false + }) + }) + } +} diff --git a/cl/compile.go b/cl/compile.go index d9381137..5acd9bcc 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -53,6 +53,11 @@ var ( enableDbg bool enableDbgSyms bool disableInline bool + + // enableExportRename enables //export to use different C symbol names than Go function names. + // This is for TinyGo compatibility when using -target flag for embedded targets. + // Currently, using -target implies TinyGo embedded target mode. + enableExportRename bool ) // SetDebug sets debug flags. @@ -73,6 +78,12 @@ func EnableTrace(b bool) { enableCallTracing = b } +// EnableExportRename enables or disables //export with different C symbol names. +// This is enabled when using -target flag for TinyGo compatibility. +func EnableExportRename(b bool) { + enableExportRename = b +} + // ----------------------------------------------------------------------------- type instrOrValue interface { diff --git a/cl/import.go b/cl/import.go index 88e21394..1b8222f6 100644 --- a/cl/import.go +++ b/cl/import.go @@ -73,7 +73,7 @@ func (p *pkgSymInfo) initLinknames(ctx *context) { lines := bytes.Split(b, sep) for _, line := range lines { if bytes.HasPrefix(line, commentPrefix) { - ctx.initLinkname(string(line), func(inPkgName string) (fullName string, isVar, ok bool) { + ctx.initLinkname(string(line), func(inPkgName string, isExport bool) (fullName string, isVar, ok bool) { if sym, ok := p.syms[inPkgName]; ok && file == sym.file { return sym.fullName, sym.isVar, true } @@ -277,8 +277,8 @@ func (p *context) initLinknameByDoc(doc *ast.CommentGroup, fullName, inPkgName s if doc != nil { for n := len(doc.List) - 1; n >= 0; n-- { line := doc.List[n].Text - ret := p.initLinkname(line, func(name string) (_ string, _, ok bool) { - return fullName, isVar, name == inPkgName + ret := p.initLinkname(line, func(name string, isExport bool) (_ string, _, ok bool) { + return fullName, isVar, name == inPkgName || (isExport && enableExportRename) }) if ret != unknownDirective { return ret == hasLinkname @@ -294,7 +294,7 @@ const ( unknownDirective = -1 ) -func (p *context) initLinkname(line string, f func(inPkgName string) (fullName string, isVar, ok bool)) int { +func (p *context) initLinkname(line string, f func(inPkgName string, isExport bool) (fullName string, isVar, ok bool)) int { const ( linkname = "//go:linkname " llgolink = "//llgo:link " @@ -324,17 +324,24 @@ func (p *context) initLinkname(line string, f func(inPkgName string) (fullName s return noDirective } -func (p *context) initLink(line string, prefix int, export bool, f func(inPkgName string) (fullName string, isVar, ok bool)) { +func (p *context) initLink(line string, prefix int, export bool, f func(inPkgName string, isExport bool) (fullName string, isVar, ok bool)) { text := strings.TrimSpace(line[prefix:]) if idx := strings.IndexByte(text, ' '); idx > 0 { inPkgName := text[:idx] - if fullName, _, ok := f(inPkgName); ok { + if fullName, _, ok := f(inPkgName, export); ok { link := strings.TrimLeft(text[idx+1:], " ") p.prog.SetLinkname(fullName, link) if export { p.pkg.SetExport(fullName, link) } } else { + // Export with different names already processed by initLinknameByDoc + if export && enableExportRename { + return + } + if export { + panic(fmt.Sprintf("export comment has wrong name %q", inPkgName)) + } fmt.Fprintln(os.Stderr, "==>", line) fmt.Fprintf(os.Stderr, "llgo: linkname %s not found and ignored\n", inPkgName) } diff --git a/internal/build/build.go b/internal/build/build.go index 73c7c9dd..6021e9f7 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -220,6 +220,11 @@ func Do(args []string, conf *Config) ([]Package, error) { conf.Goarch = export.GOARCH } + // Enable different export names for TinyGo compatibility when using -target + if conf.Target != "" { + cl.EnableExportRename(true) + } + verbose := conf.Verbose patterns := args tags := "llgo,math_big_pure_go"