Merge pull request #1384 from luoliwoshang/feature/export-different-names-1378

cl: support //export with different symbol names on embedded targets
This commit is contained in:
xushiwei
2025-11-17 08:10:09 +08:00
committed by GitHub
7 changed files with 395 additions and 9 deletions

View File

@@ -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

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

@@ -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 {

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

View File

@@ -220,6 +220,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"