From f814c12fc2b2ad7f08cd127ca735480d5df262c0 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Jul 2026 00:21:43 +0800 Subject: [PATCH 1/9] build: support funcinfo relocations in ELF shared libraries --- internal/build/funcinfo_table.go | 23 +++++++++++++++-------- internal/build/funcinfo_table_test.go | 21 +++++++++++++++------ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/internal/build/funcinfo_table.go b/internal/build/funcinfo_table.go index c69a4cc76d..a3e0c05c9e 100644 --- a/internal/build/funcinfo_table.go +++ b/internal/build/funcinfo_table.go @@ -346,6 +346,11 @@ func emitFuncInfoTable(ctx *context, pkg llssa.Package, records []funcInfoRecord symbolIndexPtr := llvm.AddGlobal(mod, llvm.PointerType(symbolIndexRecordType, 0), funcInfoSymbolIndexSymbol) count := llvm.AddGlobal(mod, countType, funcInfoCountSymbol) symbolIndexCount := llvm.AddGlobal(mod, countType, funcInfoSymbolIndexCountSymbol) + // ELF metadata records contain absolute addresses for these two globals. + // Keep them non-preemptible so shared-library links can lower the pointers + // to relative dynamic relocations in the writable metadata section. + symbolIndexPtr.SetVisibility(llvm.HiddenVisibility) + symbolIndexCount.SetVisibility(llvm.HiddenVisibility) stubIndexesPtr := llvm.AddGlobal(mod, llvm.PointerType(i32Type, 0), funcInfoStubIndexesSymbol) stubCount := llvm.AddGlobal(mod, countType, funcInfoStubCountSymbol) pcLineCount := llvm.AddGlobal(mod, countType, pcLineCountSymbol) @@ -624,12 +629,14 @@ func shouldEmitRuntimeMachOSites(ctx *context) bool { // shouldEmitRuntimeSites reports whether the target object format has a // DCE-safe section story for metadata site records. ELF uses SHF_LINK_ORDER -// associated sections (honored by --gc-sections). Mach-O uses live_support -// sections: under ld64/lld -dead_strip a live_support atom survives only if -// the atom it references (the anchor inside the function body) is live, which -// is the same records-follow-function semantics. Sites are additionally -// gated per Program: debug builds keep the funcinfo tables but drop the -// body-embedded site records (see Program.EnableFuncInfoSites). +// associated sections (honored by --gc-sections). The ELF sections are also +// writable because shared libraries need dynamic relative relocations for the +// absolute pointers stored in each record. Mach-O uses live_support sections: +// under ld64/lld -dead_strip a live_support atom survives only if the atom it +// references (the anchor inside the function body) is live, which is the same +// records-follow-function semantics. Sites are additionally gated per Program: +// debug builds keep the funcinfo tables but drop the body-embedded site records +// (see Program.EnableFuncInfoSites). func shouldEmitRuntimeSites(ctx *context) bool { if ctx == nil || ctx.prog == nil || !ctx.prog.FuncInfoSitesEnabled() { return false @@ -662,7 +669,7 @@ func (s siteSectionInfo) push(machO bool, anchor string) string { if machO { return ".pushsection " + s.machO + ",regular,live_support" } - return ".pushsection " + s.elf + ",\"ao\",@progbits," + anchor + return ".pushsection " + s.elf + ",\"awo\",@progbits," + anchor } // recordSymbol returns the extra label line each Mach-O record needs: the @@ -680,7 +687,7 @@ func (s siteSectionInfo) retain(machO bool) string { if machO { return ".section " + s.machO + ",regular,live_support" } - return ".section " + s.elf + ",\"aR\",@progbits" + return ".section " + s.elf + ",\"awR\",@progbits" } // retainSymbol returns the label lines that pin the zero record under diff --git a/internal/build/funcinfo_table_test.go b/internal/build/funcinfo_table_test.go index e7a58751d3..ea00d7d240 100644 --- a/internal/build/funcinfo_table_test.go +++ b/internal/build/funcinfo_table_test.go @@ -64,9 +64,9 @@ func TestFuncInfoTableMaterializesMetadataWithoutFunctionPointers(t *testing.T) "@__llgo_funcinfo_string_offsets = global ptr", "@__llgo_funcinfo_string_count = global i64 5", "@__llgo_funcinfo_hash = global ptr", - "@__llgo_funcinfo_symbol_index = global ptr", + "@__llgo_funcinfo_symbol_index = hidden global ptr", "@__llgo_funcinfo_count = global i64 1", - "@__llgo_funcinfo_symbol_index_count = global i64 1", + "@__llgo_funcinfo_symbol_index_count = hidden global i64 1", "@__llgo_funcinfo_entry_start = global ptr @__start_llgo_funcinfo_entry", "@__llgo_funcinfo_entry_end = global ptr @__stop_llgo_funcinfo_entry", "@__llgo_funcinfo_stub_indexes = global ptr null", @@ -282,8 +282,8 @@ func TestFuncInfoTableMaterializesClosureStubIndexes(t *testing.T) { for _, want := range []string{ "@__llgo_funcinfo_stub_indexes = global ptr", "@__llgo_funcinfo_stub_count = global i64 1", - "@__llgo_funcinfo_symbol_index = global ptr", - "@__llgo_funcinfo_symbol_index_count = global i64 2", + "@__llgo_funcinfo_symbol_index = hidden global ptr", + "@__llgo_funcinfo_symbol_index_count = hidden global i64 2", "@__llgo_funcinfo_stubsite_start = global ptr @__start_llgo_funcinfo_stubsite", "@__llgo_funcinfo_stubsite_end = global ptr @__stop_llgo_funcinfo_stubsite", `@"__llgo_funcinfo_stub_indexes$data" = private unnamed_addr constant [1 x i32]`, @@ -445,9 +445,9 @@ func TestFuncInfoTableEmptyDefinitions(t *testing.T) { "@__llgo_funcinfo_string_offsets = global ptr null", "@__llgo_funcinfo_string_count = global i64 0", "@__llgo_funcinfo_hash = global ptr null", - "@__llgo_funcinfo_symbol_index = global ptr null", + "@__llgo_funcinfo_symbol_index = hidden global ptr null", "@__llgo_funcinfo_count = global i64 0", - "@__llgo_funcinfo_symbol_index_count = global i64 0", + "@__llgo_funcinfo_symbol_index_count = hidden global i64 0", "@__llgo_funcinfo_entry_start = global ptr null", "@__llgo_funcinfo_entry_end = global ptr null", "@__llgo_funcinfo_stub_indexes = global ptr null", @@ -576,6 +576,15 @@ func TestAsmQuoteELFSymbol(t *testing.T) { } } +func TestELFFuncInfoSiteSectionsAllowSharedLibraryRelocations(t *testing.T) { + if got, want := entrySiteSectionInfo.push(false, "anchor"), `.pushsection llgo_funcinfo_entry,"awo",@progbits,anchor`; got != want { + t.Fatalf("ELF site section = %q, want %q", got, want) + } + if got, want := entrySiteSectionInfo.retain(false), `.section llgo_funcinfo_entry,"awR",@progbits`; got != want { + t.Fatalf("ELF retained section = %q, want %q", got, want) + } +} + // Empty encoded tables must materialize null initializers (the ~20-line // branch in emitFuncInfoTable that only fires for funcinfo-less programs). func TestFuncInfoTableEmptyEncodedInitializers(t *testing.T) { From d90a233586b71d4192743df5b0aa868f053e8f64 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 18:36:14 +0800 Subject: [PATCH 2/9] test: cover ELF c-shared funcinfo relocations --- _demo/go/export/test.sh | 22 +++-- internal/build/funcinfo_table_test.go | 129 ++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 6 deletions(-) diff --git a/_demo/go/export/test.sh b/_demo/go/export/test.sh index 751d2178fb..d950833d53 100755 --- a/_demo/go/export/test.sh +++ b/_demo/go/export/test.sh @@ -94,9 +94,10 @@ print_status "Starting C header generation tests..." print_status "Working directory: $SCRIPT_DIR" echo "" +build_failures=0 # Test 1: c-shared mode -print_status "=== Test 2: Building with -buildmode c-shared ===" +print_status "=== Test 1: Building with -buildmode c-shared ===" if $LLGO_SCRIPT build -buildmode c-shared -o export .; then print_status "Build succeeded" @@ -140,10 +141,11 @@ if $LLGO_SCRIPT build -buildmode c-shared -o export .; then cleanup "$SHARED_LIB" "libexport.h" else print_error "Build failed for c-shared mode" + build_failures=$((build_failures + 1)) fi # Test 2: c-archive mode -print_status "=== Test 1: Building with -buildmode c-archive ===" +print_status "=== Test 2: Building with -buildmode c-archive ===" if $LLGO_SCRIPT build -buildmode c-archive -o export .; then print_status "Build succeeded" @@ -178,6 +180,7 @@ if $LLGO_SCRIPT build -buildmode c-archive -o export .; then # cleanup "libexport.a" "libexport.h" else print_error "Build failed for c-archive mode" + build_failures=$((build_failures + 1)) fi echo "" @@ -243,11 +246,11 @@ echo "" # Final summary print_status "=== Test Summary ===" -if [[ -f "libexport.a" ]] && [[ -f "libexport.h" ]]; then - print_status "All tests completed successfully:" +if [[ "$build_failures" -eq 0 ]] && [[ -f "libexport.a" ]] && [[ -f "libexport.h" ]]; then + print_status "All required build-mode tests completed successfully:" print_status " ✅ Go export demo execution with assertions" print_status " ✅ C header generation (c-archive and c-shared modes)" - print_status " ✅ C demo compilation and execution" + print_status " ℹ️ C consumer compilation and execution checks remain non-gating" print_status " ✅ Cross-platform symbol renaming" print_status " ✅ Init function export and calling" print_status " ✅ Function callback types with proper typedef syntax" @@ -256,7 +259,9 @@ if [[ -f "libexport.a" ]] && [[ -f "libexport.h" ]]; then print_status "Final files available:" print_status " - libexport.a (static library)" print_status " - libexport.h (C header file)" - print_status " - use/main.out (C demo executable)" + if [[ -f "use/main.out" ]]; then + print_status " - use/main.out (C demo executable)" + fi echo "" echo "===================" @@ -275,4 +280,9 @@ if [[ -f "libexport.h" ]]; then print_status "Header file lines: $LINES" fi +if [[ "$build_failures" -ne 0 ]]; then + print_error "$build_failures build-mode test(s) failed" + exit 1 +fi + print_status "C header generation and demo tests completed!" diff --git a/internal/build/funcinfo_table_test.go b/internal/build/funcinfo_table_test.go index ea00d7d240..99cd7b03eb 100644 --- a/internal/build/funcinfo_table_test.go +++ b/internal/build/funcinfo_table_test.go @@ -17,6 +17,11 @@ package build import ( + "debug/elf" + "encoding/binary" + "os" + "os/exec" + "path/filepath" "strings" "testing" @@ -585,6 +590,130 @@ func TestELFFuncInfoSiteSectionsAllowSharedLibraryRelocations(t *testing.T) { } } +func TestELFFuncInfoMetadataLinksIntoSharedLibrary(t *testing.T) { + linker, err := exec.LookPath("ld.lld") + if err != nil { + t.Skip("ld.lld is required for the ELF shared-library regression test") + } + + prog := llssa.NewProgram(&llssa.Target{GOOS: "linux", GOARCH: "amd64"}) + defer prog.Dispose() + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeCShared, + Goos: "linux", + Goarch: "amd64", + }, + } + + src := prog.NewPackage("example.com/p", "example.com/p") + src.EmitFuncInfo("example.com/p.live", "example.com/p.Live", "live.go", 17, 3) + fn := src.NewFunc("example.com/p.live", llssa.NoArgsNoRet, llssa.InGo) + fn.MakeBody(1).Return() + records := collectFuncInfo([]Package{{LPkg: src}}) + emitFuncInfoEntrySites(ctx, src) + + metadata := prog.NewPackage("example.com/runtime", "example.com/runtime") + emitFuncInfoTable(ctx, metadata, records, nil, nil) + + dir := t.TempDir() + writeObject := func(name string, mod llvm.Module) string { + t.Helper() + buf, err := prog.TargetMachine().EmitToMemoryBuffer(mod, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit %s: %v", name, err) + } + defer buf.Dispose() + path := filepath.Join(dir, name) + if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil { + t.Fatalf("write %s: %v", name, err) + } + return path + } + srcObj := writeObject("src.o", src.Module()) + metadataObj := writeObject("metadata.o", metadata.Module()) + shared := filepath.Join(dir, "libfuncinfo.so") + cmd := exec.Command(linker, "-shared", "-z", "text", "--gc-sections", "-o", shared, srcObj, metadataObj) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("link ELF shared library: %v\n%s", err, out) + } + + ef, err := elf.Open(shared) + if err != nil { + t.Fatalf("open linked ELF: %v", err) + } + defer ef.Close() + entry := ef.Section(entrySiteSectionInfo.elf) + if entry == nil { + t.Fatalf("linked ELF is missing %s", entrySiteSectionInfo.elf) + } + const shfGNURetain elf.SectionFlag = 0x200000 + wantFlags := elf.SHF_WRITE | elf.SHF_ALLOC | elf.SHF_LINK_ORDER | shfGNURetain + if got := entry.Flags & wantFlags; got != wantFlags { + t.Fatalf("%s flags = %v, want at least %v", entry.Name, entry.Flags, wantFlags) + } + if entry.Flags&elf.SHF_EXECINSTR != 0 { + t.Fatalf("%s must not be executable: flags=%v", entry.Name, entry.Flags) + } + + rela := ef.Section(".rela.dyn") + if rela == nil { + t.Fatal("linked ELF is missing .rela.dyn") + } + relaData, err := rela.Data() + if err != nil { + t.Fatalf("read .rela.dyn: %v", err) + } + const rela64Size = 24 + relative := 0 + for off := 0; off+rela64Size <= len(relaData); off += rela64Size { + relocOff := binary.LittleEndian.Uint64(relaData[off:]) + if relocOff < entry.Addr || relocOff >= entry.Addr+entry.Size { + continue + } + info := binary.LittleEndian.Uint64(relaData[off+8:]) + if typ := elf.R_X86_64(uint32(info)); typ != elf.R_X86_64_RELATIVE { + t.Fatalf("%s relocation at %#x has type %v, want R_X86_64_RELATIVE", entry.Name, relocOff, typ) + } + relative++ + } + if relative != 3 { + t.Fatalf("%s has %d relative relocations, want 3", entry.Name, relative) + } + + symbols, err := ef.Symbols() + if err != nil { + t.Fatalf("read ELF symbols: %v", err) + } + for _, name := range []string{funcInfoSymbolIndexSymbol, funcInfoSymbolIndexCountSymbol} { + found := false + for _, sym := range symbols { + if sym.Name != name { + continue + } + found = true + if vis := elf.ST_VISIBILITY(sym.Other); vis != elf.STV_HIDDEN { + t.Fatalf("%s visibility = %v, want hidden", name, vis) + } + } + if !found { + t.Fatalf("linked ELF is missing %s", name) + } + } + dynamicSymbols, err := ef.DynamicSymbols() + if err != nil { + t.Fatalf("read ELF dynamic symbols: %v", err) + } + for _, sym := range dynamicSymbols { + if sym.Name == funcInfoSymbolIndexSymbol || sym.Name == funcInfoSymbolIndexCountSymbol { + t.Fatalf("internal funcinfo symbol %s must not be dynamically exported", sym.Name) + } + } +} + // Empty encoded tables must materialize null initializers (the ~20-line // branch in emitFuncInfoTable that only fires for funcinfo-less programs). func TestFuncInfoTableEmptyEncodedInitializers(t *testing.T) { From 28b8be6ba25d3e6ec57b1438ae9718dcd4bdf9b3 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 18:47:00 +0800 Subject: [PATCH 3/9] ci: skip export build modes on unsupported Go --- _demo/go/export/test.sh | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/_demo/go/export/test.sh b/_demo/go/export/test.sh index d950833d53..8ea069dafe 100755 --- a/_demo/go/export/test.sh +++ b/_demo/go/export/test.sh @@ -95,6 +95,23 @@ print_status "Working directory: $SCRIPT_DIR" echo "" build_failures=0 +run_build_mode_tests=true + +# The LLGo build wrapper loads the repository module, so these tests require a +# Go toolchain new enough for the go directive in the root go.mod. Older Go +# versions remain in the CI matrix for compatibility testing of installed LLGo. +CURRENT_GO_VERSION="$(go env GOVERSION)" +CURRENT_GO_VERSION="${CURRENT_GO_VERSION#go}" +REQUIRED_GO_VERSION="$(awk '$1 == "go" { print $2; exit }' ../../../go.mod)" +IFS=. read -r current_go_major current_go_minor _ <<< "$CURRENT_GO_VERSION" +IFS=. read -r required_go_major required_go_minor _ <<< "$REQUIRED_GO_VERSION" +if (( current_go_major < required_go_major || + (current_go_major == required_go_major && current_go_minor < required_go_minor) )); then + run_build_mode_tests=false + print_warning "Skipping c-shared/c-archive tests: Go $CURRENT_GO_VERSION is older than the module requirement $REQUIRED_GO_VERSION" +fi + +if [[ "$run_build_mode_tests" == true ]]; then # Test 1: c-shared mode print_status "=== Test 1: Building with -buildmode c-shared ===" @@ -183,6 +200,8 @@ else build_failures=$((build_failures + 1)) fi +fi + echo "" # TODO(lijie): Uncomment if https://github.com/goplus/llgo/pull/1268 merged @@ -246,7 +265,9 @@ echo "" # Final summary print_status "=== Test Summary ===" -if [[ "$build_failures" -eq 0 ]] && [[ -f "libexport.a" ]] && [[ -f "libexport.h" ]]; then +if [[ "$run_build_mode_tests" != true ]]; then + print_status "Build-mode tests were skipped because the active Go toolchain is older than the module requirement" +elif [[ "$build_failures" -eq 0 ]] && [[ -f "libexport.a" ]] && [[ -f "libexport.h" ]]; then print_status "All required build-mode tests completed successfully:" print_status " ✅ Go export demo execution with assertions" print_status " ✅ C header generation (c-archive and c-shared modes)" @@ -269,13 +290,13 @@ else print_error "Some tests may have failed. Check the output above." fi -# Show file sizes for reference -if [[ -f "libexport.a" ]]; then +# Show file sizes for reference when the build-mode tests ran. +if [[ "$run_build_mode_tests" == true ]] && [[ -f "libexport.a" ]]; then SIZE=$(wc -c < libexport.a) print_status "Static library size: $SIZE bytes" fi -if [[ -f "libexport.h" ]]; then +if [[ "$run_build_mode_tests" == true ]] && [[ -f "libexport.h" ]]; then LINES=$(wc -l < libexport.h) print_status "Header file lines: $LINES" fi From a0291020ddd991785f35377b24bca0ef44cc1dfa Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 20:09:00 +0800 Subject: [PATCH 4/9] build: make C export libraries linkable --- cl/caller_frame_test.go | 2 +- cl/instr.go | 2 +- internal/build/build.go | 109 ++++++++++++++++++++++++++- internal/build/build_test.go | 138 +++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+), 3 deletions(-) diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go index 9879a39585..42707e9986 100644 --- a/cl/caller_frame_test.go +++ b/cl/caller_frame_test.go @@ -557,7 +557,7 @@ func leaf() {} `!"caller_frame_compile.go"`, "__llgo_pcsite_", "${:uid}", - `.pushsection llgo_pcline`, + `.pushsection llgo_pcline,\22awo\22,@progbits`, `.quad __llgo_pcsite_`, } { if !strings.Contains(ir, want) { diff --git a/cl/instr.go b/cl/instr.go index 686db88458..99098347c0 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1581,7 +1581,7 @@ func (p *context) emitPCLineLabel(b llssa.Builder, pos token.Pos) { // SHF_LINK_ORDER (honored by --gc-sections); Mach-O uses a live_support // section plus one linker-private atom symbol per record so -dead_strip // keeps a record exactly when the function containing its label is live. - pushSection := ".pushsection llgo_pcline,\"ao\",@progbits," + asmQuoteSymbol(p.fn.Name()) + pushSection := ".pushsection llgo_pcline,\"awo\",@progbits," + asmQuoteSymbol(p.fn.Name()) recordSymbol := "" if target.GOOS == "darwin" { pushSection = ".pushsection __DATA,__llgo_pcl,regular,live_support" diff --git a/internal/build/build.go b/internal/build/build.go index c7496f7508..47c2f6f673 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -1190,6 +1190,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa } } } + linkArgs = append(linkArgs, cSharedExportArgs(ctx, linkedOrder)...) err = linkObjFiles(ctx, outputPath, linkInputs, linkArgs, verbose) if err != nil { @@ -1228,7 +1229,7 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose printCmds := ctx.shouldPrintCommands(verbose) // Handle c-archive mode differently - use ar tool instead of linker if ctx.buildConf.BuildMode == BuildModeCArchive { - return ctx.createArchiveFile(app, objFiles, printCmds) + return ctx.createMergedArchiveFile(app, objFiles, printCmds) } buildArgs := []string{"-o", app} @@ -1282,6 +1283,40 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose return cmd.Link(buildArgs...) } +// cSharedExportArgs keeps //export functions as shared-library link roots. The +// functions live in package archives and otherwise remain unreferenced, so the +// linker can omit both their object files and dynamic symbols. +func cSharedExportArgs(ctx *context, pkgs []*aPackage) []string { + if ctx == nil || ctx.buildConf == nil || ctx.buildConf.BuildMode != BuildModeCShared { + return nil + } + exports := make(map[string]none) + for _, pkg := range pkgs { + if pkg == nil || pkg.LPkg == nil { + continue + } + for _, name := range pkg.LPkg.ExportFuncs() { + if name != "" { + exports[name] = none{} + } + } + } + names := make([]string, 0, len(exports)) + for name := range exports { + names = append(names, name) + } + slices.Sort(names) + args := make([]string, 0, len(names)) + for _, name := range names { + if ctx.buildConf.Goos == "darwin" { + args = append(args, "-Wl,-u,_"+name) + } else { + args = append(args, "-Wl,--undefined="+name) + } + } + return args +} + func needsLinuxNoPIE(ctx *context, linkArgs []string) bool { if ctx.buildConf.Target != "" || ctx.buildConf.Goos != "linux" { return false @@ -1336,6 +1371,78 @@ func (c *context) archiver() string { return "ar" } +// archiveMerger returns an archiver with MRI support, which is required to +// flatten package archives into the final c-archive instead of nesting .a +// files as members. LLVM is already a required LLGo toolchain dependency. +func (c *context) archiveMerger() (string, error) { + if ar := os.Getenv("LLGO_AR"); ar != "" { + return ar, nil + } + if c.crossCompile.CC != "" { + llvmAr := filepath.Join(filepath.Dir(c.crossCompile.CC), "llvm-ar") + if _, err := os.Stat(llvmAr); err == nil { + return llvmAr, nil + } + } + if llvmAr, err := exec.LookPath("llvm-ar"); err == nil { + return llvmAr, nil + } + return "", errors.New("llvm-ar is required to create a flat c-archive") +} + +// createMergedArchiveFile combines object files and package archives into one +// flat archive. A regular `ar rcs output.a input.a` stores input.a as a nested +// member, which C linkers cannot search or load. +func (c *context) createMergedArchiveFile(archivePath string, inputs []string, verbose ...bool) error { + if len(inputs) == 0 { + return fmt.Errorf("no inputs provided for archive %s", archivePath) + } + if err := os.MkdirAll(filepath.Dir(archivePath), 0o755); err != nil { + return err + } + tmp, err := os.CreateTemp(filepath.Dir(archivePath), filepath.Base(archivePath)+".tmp-*") + if err != nil { + return err + } + tmpName := tmp.Name() + if err := tmp.Close(); err != nil { + os.Remove(tmpName) + return err + } + _ = os.Remove(tmpName) + + var script strings.Builder + fmt.Fprintf(&script, "CREATE %s\n", strconv.Quote(tmpName)) + for _, input := range inputs { + command := "ADDMOD" + if strings.HasSuffix(strings.ToLower(input), ".a") { + command = "ADDLIB" + } + fmt.Fprintf(&script, "%s %s\n", command, strconv.Quote(input)) + } + script.WriteString("SAVE\nEND\n") + + arCmd, err := c.archiveMerger() + if err != nil { + return err + } + cmd := exec.Command(arCmd, "-M") + cmd.Stdin = strings.NewReader(script.String()) + printCmds := c.shouldPrintCommands(len(verbose) > 0 && verbose[0]) + if printCmds { + fmt.Fprintf(os.Stderr, "%s -M\n%s", filepath.Base(arCmd), script.String()) + } + if output, err := cmd.CombinedOutput(); err != nil { + os.Remove(tmpName) + return fmt.Errorf("merge archive %s: %w\n%s", archivePath, err, output) + } + if err := os.Rename(tmpName, archivePath); err != nil { + os.Remove(tmpName) + return fmt.Errorf("publish archive %s: %w", archivePath, err) + } + return nil +} + // createArchiveFile builds an archive at archivePath atomically to avoid races when // multiple builds target the same output concurrently. func (c *context) createArchiveFile(archivePath string, objFiles []string, verbose ...bool) error { diff --git a/internal/build/build_test.go b/internal/build/build_test.go index c3cd300411..3d1a9ab1ea 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -15,6 +15,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strings" "testing" @@ -612,6 +613,143 @@ func TestArchiverAllowsLLGOAROverrideForLTO(t *testing.T) { } } +func TestCSharedExportArgs(t *testing.T) { + if got := cSharedExportArgs(nil, nil); got != nil { + t.Fatalf("nil cSharedExportArgs = %v, want nil", got) + } + prog := llssa.NewProgram(nil) + lpkg := prog.NewPackage("example.com/p", "example.com/p") + lpkg.SetExport("example.com/p.Z", "Zed") + lpkg.SetExport("example.com/p.A", "Add") + pkgs := []*aPackage{{LPkg: lpkg}} + + ctx := &context{buildConf: &Config{BuildMode: BuildModeCShared, Goos: "linux"}} + if got, want := strings.Join(cSharedExportArgs(ctx, pkgs), " "), "-Wl,--undefined=Add -Wl,--undefined=Zed"; got != want { + t.Fatalf("linux cSharedExportArgs = %q, want %q", got, want) + } + ctx.buildConf.Goos = "darwin" + if got, want := strings.Join(cSharedExportArgs(ctx, pkgs), " "), "-Wl,-u,_Add -Wl,-u,_Zed"; got != want { + t.Fatalf("darwin cSharedExportArgs = %q, want %q", got, want) + } + ctx.buildConf.BuildMode = BuildModeExe + if got := cSharedExportArgs(ctx, pkgs); got != nil { + t.Fatalf("executable cSharedExportArgs = %v, want nil", got) + } +} + +func TestArchiveMergerSelection(t *testing.T) { + t.Run("override", func(t *testing.T) { + t.Setenv("LLGO_AR", "custom-llvm-ar") + got, err := (&context{}).archiveMerger() + if err != nil || got != "custom-llvm-ar" { + t.Fatalf("archiveMerger() = %q, %v, want custom-llvm-ar", got, err) + } + }) + + t.Run("next to compiler", func(t *testing.T) { + t.Setenv("LLGO_AR", "") + t.Setenv("PATH", "") + td := t.TempDir() + llvmAr := filepath.Join(td, "llvm-ar") + if err := os.WriteFile(llvmAr, nil, 0o755); err != nil { + t.Fatal(err) + } + ctx := &context{} + ctx.crossCompile.CC = filepath.Join(td, "clang") + got, err := ctx.archiveMerger() + if err != nil || got != llvmAr { + t.Fatalf("archiveMerger() = %q, %v, want %q", got, err, llvmAr) + } + }) + + t.Run("path", func(t *testing.T) { + t.Setenv("LLGO_AR", "") + td := t.TempDir() + llvmAr := filepath.Join(td, "llvm-ar") + if err := os.WriteFile(llvmAr, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", td) + got, err := (&context{}).archiveMerger() + if err != nil || got != llvmAr { + t.Fatalf("archiveMerger() = %q, %v, want %q", got, err, llvmAr) + } + }) + + t.Run("missing", func(t *testing.T) { + t.Setenv("LLGO_AR", "") + t.Setenv("PATH", "") + if got, err := (&context{}).archiveMerger(); err == nil || got != "" { + t.Fatalf("archiveMerger() = %q, %v, want missing-tool error", got, err) + } + }) +} + +func TestCreateMergedArchiveFileFlattensInputs(t *testing.T) { + llvmAr, err := exec.LookPath("llvm-ar") + if err != nil { + t.Skip("llvm-ar is not installed") + } + t.Setenv("LLGO_AR", llvmAr) + + td := filepath.Join(t.TempDir(), "archive with spaces") + if err := os.MkdirAll(td, 0o755); err != nil { + t.Fatal(err) + } + direct := filepath.Join(td, "direct.o") + nestedOne := filepath.Join(td, "nested-one.o") + nestedTwo := filepath.Join(td, "nested-two.o") + for path, content := range map[string]string{ + direct: "direct", + nestedOne: "nested one", + nestedTwo: "nested two", + } { + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + nested := filepath.Join(td, "nested.a") + if output, err := exec.Command(llvmAr, "rcs", nested, nestedOne, nestedTwo).CombinedOutput(); err != nil { + t.Fatalf("create nested archive: %v\n%s", err, output) + } + + out := filepath.Join(td, "combined.a") + ctx := &context{buildConf: &Config{}} + if err := ctx.createMergedArchiveFile(out, []string{direct, nested}, true); err != nil { + t.Fatal(err) + } + output, err := exec.Command(llvmAr, "t", out).CombinedOutput() + if err != nil { + t.Fatalf("list merged archive: %v\n%s", err, output) + } + members := strings.Fields(string(output)) + slices.Sort(members) + if got, want := strings.Join(members, " "), "direct.o nested-one.o nested-two.o"; got != want { + t.Fatalf("merged archive members = %q, want %q", got, want) + } +} + +func TestCreateMergedArchiveFileErrors(t *testing.T) { + ctx := &context{buildConf: &Config{}} + if err := ctx.createMergedArchiveFile(filepath.Join(t.TempDir(), "empty.a"), nil); err == nil { + t.Fatal("createMergedArchiveFile with no inputs succeeded") + } + + td := t.TempDir() + failingAr := filepath.Join(td, "llvm-ar") + if err := os.WriteFile(failingAr, []byte("#!/bin/sh\necho merge failed >&2\nexit 7\n"), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("LLGO_AR", failingAr) + input := filepath.Join(td, "input.o") + if err := os.WriteFile(input, []byte("object"), 0o644); err != nil { + t.Fatal(err) + } + if err := ctx.createMergedArchiveFile(filepath.Join(td, "failed.a"), []string{input}); err == nil || !strings.Contains(err.Error(), "merge failed") { + t.Fatalf("createMergedArchiveFile error = %v, want archiver output", err) + } +} + func TestDevLTOGlobalDCEDefaultsToFullLTO(t *testing.T) { tests := []struct { name string From a10b5d8dd005c1b0d25a14921ecc962cfc298152 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 20:09:00 +0800 Subject: [PATCH 5/9] test: require shared and static C consumer execution --- _demo/go/export/test.sh | 14 +++++++++----- _demo/go/export/use/main.c | 11 ++++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/_demo/go/export/test.sh b/_demo/go/export/test.sh index 8ea069dafe..56fb915366 100755 --- a/_demo/go/export/test.sh +++ b/_demo/go/export/test.sh @@ -144,10 +144,12 @@ if $LLGO_SCRIPT build -buildmode c-shared -o export .; then if LINK_TYPE=shared make run; then print_status "C demo execution succeeded with shared library" else - print_warning "C demo execution failed with shared library" + print_error "C demo execution failed with shared library" + build_failures=$((build_failures + 1)) fi else - print_warning "C demo build failed with shared library" + print_error "C demo build failed with shared library" + build_failures=$((build_failures + 1)) fi cd .. else @@ -183,10 +185,12 @@ if $LLGO_SCRIPT build -buildmode c-archive -o export .; then if make run; then print_status "C demo execution succeeded with static library" else - print_warning "C demo execution failed with static library" + print_error "C demo execution failed with static library" + build_failures=$((build_failures + 1)) fi else - print_warning "C demo build failed with static library" + print_error "C demo build failed with static library" + build_failures=$((build_failures + 1)) fi cd .. else @@ -271,7 +275,7 @@ elif [[ "$build_failures" -eq 0 ]] && [[ -f "libexport.a" ]] && [[ -f "libexport print_status "All required build-mode tests completed successfully:" print_status " ✅ Go export demo execution with assertions" print_status " ✅ C header generation (c-archive and c-shared modes)" - print_status " ℹ️ C consumer compilation and execution checks remain non-gating" + print_status " ✅ C consumer compilation and execution with shared and static libraries" print_status " ✅ Cross-platform symbol renaming" print_status " ✅ Init function export and calling" print_status " ✅ Function callback types with proper typedef syntax" diff --git a/_demo/go/export/use/main.c b/_demo/go/export/use/main.c index 4a5f0656ef..0494abbd1e 100644 --- a/_demo/go/export/use/main.c +++ b/_demo/go/export/use/main.c @@ -18,9 +18,13 @@ int main() { // Test small struct main_SmallStruct small = CreateSmallStruct(5, 1); // 1 for true + assert(small.ID == 5); + assert(small.Flag == 1); printf("Small struct: %d %d\n", small.ID, small.Flag); main_SmallStruct processed = ProcessSmallStruct(small); + assert(processed.ID == 6); + assert(processed.Flag == 0); printf("Processed small: %d %d\n", processed.ID, processed.Flag); main_SmallStruct* ptrSmall = ProcessSmallStructPtr(&small); @@ -31,6 +35,7 @@ int main() { // Test large struct - create GoString for name parameter GoString name = {"test_large", 10}; // name and length main_LargeStruct large = CreateLargeStruct(12345, name); + assert(large.ID == 12345); printf("Large struct ID: %" PRId64 "\n", large.ID); int64_t total = ProcessLargeStruct(large); @@ -208,6 +213,7 @@ int main() { printf("About to call CreateMatrix1D()...\n"); fflush(stdout); Array_int32_t_4 matrix1d = CreateMatrix1D(); + assert(matrix1d.data[0] == 1); printf("CreateMatrix1D() call completed\n"); printf("CreateMatrix1D() returned struct, first element: %d\n", matrix1d.data[0]); @@ -215,15 +221,18 @@ int main() { printf("About to call CreateMatrix2D()...\n"); fflush(stdout); Array_int32_t_3_4 matrix2d = CreateMatrix2D(); + assert(matrix2d.data[0][0] == 1); printf("CreateMatrix2D() call completed\n"); printf("CreateMatrix2D() returned struct, first element: %d\n", matrix2d.data[0][0]); // Test CreateMatrix3D() which returns Array_uint8_t_2_3_4 Array_uint8_t_2_3_4 cube = CreateMatrix3D(); + assert(cube.data[0][0][0] == 1); printf("CreateMatrix3D() returned struct, first element: %u\n", cube.data[0][0][0]); // Test CreateGrid5x4() which returns Array_double_5_4 Array_double_5_4 grid = CreateGrid5x4(); + assert(grid.data[0][0] == 1.0); printf("CreateGrid5x4() returned struct, first element: %f\n", grid.data[0][0]); // Test NoReturn function @@ -234,4 +243,4 @@ int main() { printf("C demo completed!\n"); return 0; -} \ No newline at end of file +} From dad42bd15ff6da944fbd50dfc10e1a4a721eff7c Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 21:02:04 +0800 Subject: [PATCH 6/9] header: preserve array value ABI in C declarations --- _demo/go/export/libexport.h.want | 12 ++++--- _demo/go/export/use/main.c | 20 ++++++------ internal/header/header.go | 54 +++----------------------------- internal/header/header_test.go | 8 ++--- 4 files changed, 28 insertions(+), 66 deletions(-) diff --git a/_demo/go/export/libexport.h.want b/_demo/go/export/libexport.h.want index 056d5375ed..cec6fe9a5d 100644 --- a/_demo/go/export/libexport.h.want +++ b/_demo/go/export/libexport.h.want @@ -80,6 +80,10 @@ typedef struct { _Bool Flag; } C_XType; +typedef struct { + intptr_t data[5]; +} Array_intptr_t_5; + typedef intptr_t main_MyInt; typedef GoString main_MyString; @@ -166,7 +170,7 @@ double ProcessFloat64(double x); double -ProcessGrid5x4(double grid[5][4]); +ProcessGrid5x4(Array_double_5_4 grid); intptr_t ProcessInt(intptr_t x); @@ -184,7 +188,7 @@ int8_t ProcessInt8(int8_t x); intptr_t -ProcessIntArray(intptr_t* arr); +ProcessIntArray(Array_intptr_t_5 arr); intptr_t ProcessIntChannel(GoChan ch); @@ -202,10 +206,10 @@ main_LargeStruct* ProcessLargeStructPtr(main_LargeStruct* ls); int32_t -ProcessMatrix2D(int32_t matrix[3][4]); +ProcessMatrix2D(Array_int32_t_3_4 matrix); uint32_t -ProcessMatrix3D(uint8_t cube[2][3][4]); +ProcessMatrix3D(Array_uint8_t_2_3_4 cube); main_MyInt ProcessMyInt(main_MyInt x); diff --git a/_demo/go/export/use/main.c b/_demo/go/export/use/main.c index 0494abbd1e..43603da36b 100644 --- a/_demo/go/export/use/main.c +++ b/_demo/go/export/use/main.c @@ -113,8 +113,10 @@ int main() { printf("MyInt: %ld\n", (long)myInt); // Test arrays - intptr_t arr[5] = {1, 2, 3, 4, 5}; - printf("Array sum: %ld\n", ProcessIntArray(arr)); + Array_intptr_t_5 arr = {.data = {1, 2, 3, 4, 5}}; + intptr_t arr_sum = ProcessIntArray(arr); + assert(arr_sum == 15); + printf("Array sum: %ld\n", (long)arr_sum); // Test complex data with multidimensional arrays main_ComplexData complex = CreateComplexData(); @@ -170,22 +172,22 @@ int main() { printf("Testing 2D matrix functions...\n"); // Create a test 2D matrix [3][4]int32 - int32_t test_matrix[3][4] = { + Array_int32_t_3_4 test_matrix = {.data = { {1, 2, 3, 4}, - {5, 6, 7, 8}, + {5, 6, 7, 8}, {9, 10, 11, 12} - }; + }}; int32_t matrix_sum = ProcessMatrix2D(test_matrix); assert(matrix_sum == 78); // Sum of 1+2+3+...+12 = 78 printf("Matrix2D sum: %d\n", matrix_sum); // Create a test 3D cube [2][3][4]uint8 - uint8_t test_cube[2][3][4]; + Array_uint8_t_2_3_4 test_cube; uint8_t val = 1; for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { for (int k = 0; k < 4; k++) { - test_cube[i][j][k] = val++; + test_cube.data[i][j][k] = val++; } } } @@ -194,11 +196,11 @@ int main() { printf("Matrix3D (cube) sum: %u\n", cube_sum); // Create a test 5x4 grid [5][4]double - double test_grid[5][4]; + Array_double_5_4 test_grid; double grid_val = 1.0; for (int i = 0; i < 5; i++) { for (int j = 0; j < 4; j++) { - test_grid[i][j] = grid_val; + test_grid.data[i][j] = grid_val; grid_val += 0.5; } } diff --git a/internal/header/header.go b/internal/header/header.go index 458173b61f..23d3ba5c5a 100644 --- a/internal/header/header.go +++ b/internal/header/header.go @@ -384,41 +384,11 @@ func (hw *cheaderWriter) generateParameterDeclaration(paramType types.Type, para switch typ := paramType.(type) { case *types.Array: - // Handle multidimensional arrays by collecting all dimensions - var dimensions []int64 - baseType := types.Type(typ) - - // Traverse all array dimensions - for { - if arr, ok := baseType.(*types.Array); ok { - dimensions = append(dimensions, arr.Len()) - baseType = arr.Elem() - } else { - break - } - } - - // Get base element type - elemType := hw.goCTypeName(baseType) - - // For parameters, preserve all array dimensions - // In C, array parameters need special handling for syntax - cType = elemType - - // Store dimensions for later use with parameter name - var dimStr strings.Builder - for _, dim := range dimensions { - dimStr.WriteString(fmt.Sprintf("[%d]", dim)) - } - - // For single dimension, we can use pointer syntax - if len(dimensions) == 1 { - cType = elemType + "*" - } else { - // For multi-dimensional, we need to handle it when adding parameter name - // Store the dimension info in a special way - cType = elemType + "ARRAY_DIMS" + dimStr.String() - } + // C array parameters decay to pointers and therefore do not preserve + // Go's by-value array ABI on architectures such as amd64. Use the same + // generated wrapper struct as array return values so C passes the whole + // value with the platform aggregate ABI. + cType = hw.ensureArrayStruct(typ) case *types.Pointer: pointeeType := hw.goCTypeName(typ.Elem()) cType = pointeeType + "*" @@ -427,20 +397,6 @@ func (hw *cheaderWriter) generateParameterDeclaration(paramType types.Type, para cType = hw.goCTypeName(paramType) } - // Handle special array dimension syntax - if strings.Contains(cType, "ARRAY_DIMS") { - parts := strings.Split(cType, "ARRAY_DIMS") - elemType := parts[0] - dimStr := parts[1] - - if paramName == "" { - // For unnamed parameters, keep dimension info: type[dim1][dim2] - return elemType + dimStr - } - // For named parameters, use proper array syntax: type name[dim1][dim2] - return elemType + " " + paramName + dimStr - } - if paramName == "" { return cType } diff --git a/internal/header/header_test.go b/internal/header/header_test.go index 0a9261a8de..dcae531a82 100644 --- a/internal/header/header_test.go +++ b/internal/header/header_test.go @@ -609,25 +609,25 @@ func TestGenerateParameterDeclaration(t *testing.T) { name: "array type with name", paramType: types.NewArray(types.Typ[types.Int], 5), paramName: "arr", - expected: "intptr_t* arr", + expected: "Array_intptr_t_5 arr", }, { name: "array type without name", paramType: types.NewArray(types.Typ[types.Int], 5), paramName: "", - expected: "intptr_t*", + expected: "Array_intptr_t_5", }, { name: "multidimensional array with name", paramType: types.NewArray(types.NewArray(types.Typ[types.Int], 4), 3), paramName: "matrix", - expected: "intptr_t matrix[3][4]", + expected: "Array_intptr_t_3_4 matrix", }, { name: "multidimensional array without name", paramType: types.NewArray(types.NewArray(types.Typ[types.Int], 4), 3), paramName: "", - expected: "intptr_t[3][4]", + expected: "Array_intptr_t_3_4", }, { name: "pointer type with name", From 8cb460e5406958c5b9493d988300919726d1bc32 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 22:34:45 +0800 Subject: [PATCH 7/9] build: make runtime-backed C libraries linkable --- internal/build/build.go | 43 ++++++++++++-- internal/build/build_test.go | 61 ++++++++++++++++++++ internal/header/header.go | 18 +++++- internal/header/header_test.go | 3 + runtime/internal/clite/libuv/_wrap/libuv.c | 10 ---- runtime/internal/clite/libuv/wrap_llgo.go | 6 -- runtime/internal/lib/runtime/_wrap/runtime.c | 15 +++++ 7 files changed, 133 insertions(+), 23 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 47c2f6f673..8453b95f06 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -285,6 +285,7 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, fmt.Errorf("failed to setup crosscompile: %w", err) } + applyBuildModeCompileFlags(conf.BuildMode, &export) // Update GOOS/GOARCH from export if target was used if conf.Target != "" && export.GOOS != "" { conf.Goos = export.GOOS @@ -528,12 +529,7 @@ func Do(args []string, conf *Config) ([]Package, error) { if ctx.buildConf.BuildMode == BuildModeCArchive || ctx.buildConf.BuildMode == BuildModeCShared { libname := strings.TrimSuffix(filepath.Base(outFmts.Out), conf.AppExt) headerPath := filepath.Join(filepath.Dir(outFmts.Out), libname) + ".h" - pkgs := make([]llssa.Package, 0, len(allPkgs)) - for _, p := range allPkgs { - if p.LPkg != nil { - pkgs = append(pkgs, p.LPkg) - } - } + pkgs := cHeaderPackages(allPkgs) headerErr := header.GenHeaderFile(prog, pkgs, libname, headerPath, verbose) if headerErr != nil { return nil, headerErr @@ -598,6 +594,41 @@ func Do(args []string, conf *Config) ([]Package, error) { return allPkgs, nil } +// cHeaderPackages excludes the patched standard runtime implementation. Its +// //export callbacks are linker implementation details and may use internal C +// types that are deliberately not representable in a public generated header. +func cHeaderPackages(allPkgs []*aPackage) []llssa.Package { + pkgs := make([]llssa.Package, 0, len(allPkgs)) + for _, pkg := range allPkgs { + if pkg == nil || pkg.LPkg == nil || pkg.Package == nil || pkg.PkgPath == "runtime" || isRuntimePkg(pkg.PkgPath) || !hasLocalCExports(pkg.LPkg) { + continue + } + pkgs = append(pkgs, pkg.LPkg) + } + return pkgs +} + +func hasLocalCExports(pkg llssa.Package) bool { + if pkg == nil { + return false + } + for name := range pkg.ExportFuncs() { + if !strings.Contains(name, ".") || strings.HasPrefix(name, pkg.Path()+".") { + return true + } + } + return false +} + +// applyBuildModeCompileFlags adds code-generation flags that must be present +// while package C/C++ sources are compiled. Passing -fPIC only to the final +// shared-library link is too late for objects containing global references. +func applyBuildModeCompileFlags(mode BuildMode, export *crosscompile.Export) { + if mode == BuildModeCShared && export != nil && !slices.Contains(export.CCFLAGS, "-fPIC") { + export.CCFLAGS = append(export.CCFLAGS, "-fPIC") + } +} + func allowMissingFunctionBodies(initial []*packages.Package) { for _, pkg := range initial { hasMissingBody := false diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 3d1a9ab1ea..c2cb9857bf 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -20,6 +20,7 @@ import ( "testing" "github.com/goplus/llgo/internal/buildenv" + "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/lto" "github.com/goplus/llgo/internal/mockable" "github.com/goplus/llgo/internal/packages" @@ -637,6 +638,66 @@ func TestCSharedExportArgs(t *testing.T) { } } +func TestApplyBuildModeCompileFlags(t *testing.T) { + tests := []struct { + name string + mode BuildMode + in []string + want string + }{ + {name: "shared adds PIC", mode: BuildModeCShared, want: "-fPIC"}, + {name: "shared preserves flags", mode: BuildModeCShared, in: []string{"-O2"}, want: "-O2 -fPIC"}, + {name: "shared does not duplicate PIC", mode: BuildModeCShared, in: []string{"-fPIC"}, want: "-fPIC"}, + {name: "archive remains unchanged", mode: BuildModeCArchive, in: []string{"-O2"}, want: "-O2"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + export := crosscompile.Export{CCFLAGS: slices.Clone(tt.in)} + applyBuildModeCompileFlags(tt.mode, &export) + if got := strings.Join(export.CCFLAGS, " "); got != tt.want { + t.Fatalf("CCFLAGS = %q, want %q", got, tt.want) + } + }) + } + + applyBuildModeCompileFlags(BuildModeCShared, nil) +} + +func TestCHeaderPackagesExcludesStandardRuntime(t *testing.T) { + prog := llssa.NewProgram(nil) + defer prog.Dispose() + userLPkg := prog.NewPackage("example.com/p", "example.com/p") + userLPkg.SetExport("example.com/p.Export", "Export") + runtimeLPkg := prog.NewPackage("runtime", "runtime") + llgoRuntimeLPkg := prog.NewPackage("github.com/goplus/llgo/runtime/internal/lib/runtime", "github.com/goplus/llgo/runtime/internal/lib/runtime") + dependencyLPkg := prog.NewPackage("example.com/dep", "example.com/dep") + pkgs := []*aPackage{ + {Package: &packages.Package{PkgPath: "example.com/p"}, LPkg: userLPkg}, + {Package: &packages.Package{PkgPath: "runtime"}, LPkg: runtimeLPkg}, + {Package: &packages.Package{PkgPath: "github.com/goplus/llgo/runtime/internal/lib/runtime"}, LPkg: llgoRuntimeLPkg}, + {Package: &packages.Package{PkgPath: "example.com/dep"}, LPkg: dependencyLPkg}, + nil, + } + got := cHeaderPackages(pkgs) + if len(got) != 1 || got[0] != userLPkg { + t.Fatalf("cHeaderPackages = %v, want only user package", got) + } + + if hasLocalCExports(nil) { + t.Fatal("hasLocalCExports(nil) = true, want false") + } + unqualifiedLPkg := prog.NewPackage("example.com/unqualified", "example.com/unqualified") + unqualifiedLPkg.SetExport("Export", "Export") + if !hasLocalCExports(unqualifiedLPkg) { + t.Fatal("hasLocalCExports(unqualified) = false, want true") + } + foreignOnlyLPkg := prog.NewPackage("example.com/foreign", "example.com/foreign") + foreignOnlyLPkg.SetExport("runtime.Export", "Export") + if hasLocalCExports(foreignOnlyLPkg) { + t.Fatal("hasLocalCExports(foreign only) = true, want false") + } +} + func TestArchiveMergerSelection(t *testing.T) { t.Run("override", func(t *testing.T) { t.Setenv("LLGO_AR", "custom-llvm-ar") diff --git a/internal/header/header.go b/internal/header/header.go index 23d3ba5c5a..1c5dca41d9 100644 --- a/internal/header/header.go +++ b/internal/header/header.go @@ -648,6 +648,13 @@ func genHeader(p ssa.Program, pkgs []ssa.Package, w io.Writer) error { // Sort functions for testing exportNames := make([]string, 0, len(exports)) for name := range exports { + // Imported //export metadata may be recorded on the importing + // package as well. Only declarations owned by this package belong + // in its public header; otherwise internal runtime callbacks can + // leak into user headers or fail type generation entirely. + if !exportBelongsToPackage(pkg.Path(), name) { + continue + } exportNames = append(exportNames, name) } sort.Strings(exportNames) @@ -656,7 +663,7 @@ func genHeader(p ssa.Program, pkgs []ssa.Package, w io.Writer) error { link := exports[name] // link is cName fn := pkg.FuncOf(link) if fn == nil { - return fmt.Errorf("function %s not found", link) + return fmt.Errorf("function %s not found in package %s", link, pkg.Path()) } // Write function declaration with proper C types @@ -680,6 +687,15 @@ func genHeader(p ssa.Program, pkgs []ssa.Package, w io.Writer) error { return hw.writeTo(w) } +func exportBelongsToPackage(pkgPath, name string) bool { + if !strings.Contains(name, ".") { + // Preserve support for callers that register the historical + // unqualified form via ssa.Package.SetExport. + return true + } + return strings.HasPrefix(name, pkgPath+".") +} + func GenHeaderFile(p ssa.Program, pkgs []ssa.Package, libName, headerPath string, verbose bool) error { // Write header file w, err := os.Create(headerPath) diff --git a/internal/header/header_test.go b/internal/header/header_test.go index dcae531a82..d94c726cb9 100644 --- a/internal/header/header_test.go +++ b/internal/header/header_test.go @@ -62,6 +62,9 @@ func TestGenCHeaderExport(t *testing.T) { mainPkg.SetExport("HelloWorld", "HelloWorld") mainPkg.SetExport("UseFooPtr", "UseFooPtr") mainPkg.SetExport("UseFoo", "UseFoo") + // Imported export metadata must not be treated as a declaration owned by + // the main package. There is intentionally no such function in mainPkg. + mainPkg.SetExport("runtime.llgo_runtime_signalCallback", "llgo_runtime_signalCallback") // Create package C cPkgPath := "github.com/goplus/llgo/test_buildmode/bar" diff --git a/runtime/internal/clite/libuv/_wrap/libuv.c b/runtime/internal/clite/libuv/_wrap/libuv.c index 8ef3216547..98ae7b1cf2 100644 --- a/runtime/internal/clite/libuv/_wrap/libuv.c +++ b/runtime/internal/clite/libuv/_wrap/libuv.c @@ -3,8 +3,6 @@ extern void llgo_runtime_timerEvent(uv_async_t* handle); extern void llgo_runtime_timerCallback(uv_timer_t* handle); -extern void llgo_time_timerEvent(uv_async_t* handle); -extern void llgo_time_timerCallback(uv_timer_t* handle); extern void llgo_runtime_signalCallback(uv_signal_t* handle, int signum); static void llgo_uv_async_noop(uv_async_t* handle) { @@ -19,18 +17,10 @@ int llgo_uv_async_init_runtime(uv_loop_t* loop, uv_async_t* async) { return uv_async_init(loop, async, llgo_runtime_timerEvent); } -int llgo_uv_async_init_time(uv_loop_t* loop, uv_async_t* async) { - return uv_async_init(loop, async, llgo_time_timerEvent); -} - int llgo_uv_timer_start_runtime(uv_timer_t* timer, uint64_t timeout, uint64_t repeat) { return uv_timer_start(timer, llgo_runtime_timerCallback, timeout, repeat); } -int llgo_uv_timer_start_time(uv_timer_t* timer, uint64_t timeout, uint64_t repeat) { - return uv_timer_start(timer, llgo_time_timerCallback, timeout, repeat); -} - int llgo_uv_signal_start_runtime(uv_signal_t* handle, int signum) { return uv_signal_start(handle, llgo_runtime_signalCallback, signum); } diff --git a/runtime/internal/clite/libuv/wrap_llgo.go b/runtime/internal/clite/libuv/wrap_llgo.go index 147d0a6699..30b2c08727 100644 --- a/runtime/internal/clite/libuv/wrap_llgo.go +++ b/runtime/internal/clite/libuv/wrap_llgo.go @@ -12,15 +12,9 @@ func AsyncInitNoop(loop *Loop, async *Async) c.Int //go:linkname AsyncInitRuntime C.llgo_uv_async_init_runtime func AsyncInitRuntime(loop *Loop, async *Async) c.Int -//go:linkname AsyncInitTime C.llgo_uv_async_init_time -func AsyncInitTime(loop *Loop, async *Async) c.Int - //go:linkname TimerStartRuntime C.llgo_uv_timer_start_runtime func TimerStartRuntime(timer *Timer, timeoutMs uint64, repeat uint64) c.Int -//go:linkname TimerStartTime C.llgo_uv_timer_start_time -func TimerStartTime(timer *Timer, timeoutMs uint64, repeat uint64) c.Int - //go:linkname SignalStartRuntime C.llgo_uv_signal_start_runtime func SignalStartRuntime(handle *Signal, signum c.Int) c.Int diff --git a/runtime/internal/lib/runtime/_wrap/runtime.c b/runtime/internal/lib/runtime/_wrap/runtime.c index cd5af9a95e..7b03d95b65 100644 --- a/runtime/internal/lib/runtime/_wrap/runtime.c +++ b/runtime/internal/lib/runtime/_wrap/runtime.c @@ -1,4 +1,19 @@ #include +#include + +#if defined(__APPLE__) +// The patched runtime provides os.runtime_args even when package os is not +// linked. Darwin's external linker diagnoses its reference to +// os.executablePath before dead stripping the unused function. Provide a weak, +// zero-valued Go string so runtime-only shared/static libraries remain +// linkable; package os supplies the strong definition when it is present. +struct llgo_go_string { + const char *p; + intptr_t n; +}; +struct llgo_go_string llgo_os_executable_path __asm("_os.executablePath") + __attribute__((weak)); +#endif int llgo_maxprocs() { From e62235955dc2a5e42bbe178f2a9e0dc4690c9a5a Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 22:34:53 +0800 Subject: [PATCH 8/9] test: exercise runtime and C ABI exports --- _demo/go/export/export.go | 90 ++++++++++++++++++++++++++++++++ _demo/go/export/libexport.h.want | 66 +++++++++++------------ _demo/go/export/use/Makefile | 5 +- _demo/go/export/use/main.c | 86 +++++++++++++++++++++++++++--- 4 files changed, 206 insertions(+), 41 deletions(-) diff --git a/_demo/go/export/export.go b/_demo/go/export/export.go index 8ac32567a0..2dd5392704 100644 --- a/_demo/go/export/export.go +++ b/_demo/go/export/export.go @@ -1,6 +1,7 @@ package main import ( + "runtime" "unsafe" C "github.com/goplus/llgo/_demo/go/export/c" @@ -47,6 +48,21 @@ type Node struct { type MyInt int type MyString string +// FuncInfoResult exposes the information resolved from a Callers PC so the C +// consumer can verify that funcinfo remains usable after loading a c-shared +// library (and from the equivalent c-archive output). +type FuncInfoResult struct { + CallersCount int32 + FramePC uintptr + FrameFunction string + FrameFile string + FrameLine int32 + FuncName string + FuncEntry uintptr + FuncFile string + FuncLine int32 +} + // Function types for callbacks // //llgo:type C @@ -71,6 +87,53 @@ func HelloWorld() { println("Hello, World!") } +//go:noinline +func captureCFuncInfo() FuncInfoResult { + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) + result := FuncInfoResult{CallersCount: int32(n)} + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function == "main.captureCFuncInfo" { + result.FramePC = frame.PC + result.FrameFunction = frame.Function + result.FrameFile = frame.File + result.FrameLine = int32(frame.Line) + + // Callers PCs are return addresses. Apply Go's pc-1 convention + // before asking FuncForPC for the containing function. + if frame.PC > 0 { + if fn := runtime.FuncForPC(frame.PC - 1); fn != nil { + result.FuncName = fn.Name() + result.FuncEntry = fn.Entry() + result.FuncFile, frame.Line = fn.FileLine(frame.PC - 1) + result.FuncLine = int32(frame.Line) + } + } + break + } + if !more { + break + } + } + return result +} + +//export GetFuncInfo +func GetFuncInfo() FuncInfoResult { + return captureCFuncInfo() +} + +//export RunGoroutine +func RunGoroutine(value int) int { + ch := make(chan int, 1) + go func() { + ch <- value + 1 + }() + return <-ch +} + // Functions with small struct parameters and return values //export CreateSmallStruct @@ -382,6 +445,11 @@ func ProcessIntSlice(slice []int) int { return total } +//export CreateIntSlice +func CreateIntSlice() []int { + return []int{2, 4, 6, 8} +} + //export ProcessStringMap func ProcessStringMap(m map[string]int) int { total := 0 @@ -391,6 +459,11 @@ func ProcessStringMap(m map[string]int) int { return total } +//export CreateStringMap +func CreateStringMap() map[string]int { + return map[string]int{"one": 10, "two": 20, "three": 30} +} + //export ProcessIntChannel func ProcessIntChannel(ch chan int) int { select { @@ -401,6 +474,13 @@ func ProcessIntChannel(ch chan int) int { } } +//export CreateIntChannel +func CreateIntChannel() chan int { + ch := make(chan int, 1) + ch <- 321 + return ch +} + // Functions with function callbacks //export ProcessWithIntCallback @@ -451,6 +531,16 @@ func ProcessInterface(i interface{}) int { } } +//export CreateIntInterface +func CreateIntInterface() interface{} { + return 23 +} + +//export CreateStringInterface +func CreateStringInterface() interface{} { + return "llgo" +} + // Functions with various parameter counts //export NoParams diff --git a/_demo/go/export/libexport.h.want b/_demo/go/export/libexport.h.want index cec6fe9a5d..67deee32be 100644 --- a/_demo/go/export/libexport.h.want +++ b/_demo/go/export/libexport.h.want @@ -80,6 +80,18 @@ typedef struct { _Bool Flag; } C_XType; +typedef struct { + int32_t CallersCount; + uintptr_t FramePC; + GoString FrameFunction; + GoString FrameFile; + int32_t FrameLine; + GoString FuncName; + uintptr_t FuncEntry; + GoString FuncFile; + int32_t FuncLine; +} main_FuncInfoResult; + typedef struct { intptr_t data[5]; } Array_intptr_t_5; @@ -115,6 +127,15 @@ CreateComplexData(void); Array_double_5_4 CreateGrid5x4(void); +GoChan +CreateIntChannel(void); + +GoInterface +CreateIntInterface(void); + +GoSlice +CreateIntSlice(void); + main_LargeStruct CreateLargeStruct(int64_t id, GoString name); @@ -133,9 +154,18 @@ CreateNode(intptr_t data); main_SmallStruct CreateSmallStruct(int8_t id, _Bool flag); +GoInterface +CreateStringInterface(void); + +GoMap +CreateStringMap(void); + C_XType CreateXType(int32_t id, GoString name, double value, _Bool flag); +main_FuncInfoResult +GetFuncInfo(void); + void HelloWorld(void); @@ -268,6 +298,9 @@ ProcessXType(C_XType x); C_XType* ProcessXTypePtr(C_XType* x); +intptr_t +RunGoroutine(intptr_t value); + double ThreeParams(int32_t a, double b, _Bool c); @@ -280,39 +313,6 @@ TwoParams(intptr_t a, GoString b); void github_com_goplus_llgo__demo_go_export_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/_demo/go/export.init") -void -github_com_goplus_llgo_runtime_abi_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/abi.init") - -void -github_com_goplus_llgo_runtime_internal_clite_bdwgc_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/bdwgc.init") - -void -github_com_goplus_llgo_runtime_internal_clite_bitcast_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/bitcast.init") - -void -github_com_goplus_llgo_runtime_internal_clite_debug_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/debug.init") - -void -github_com_goplus_llgo_runtime_internal_clite_pthread_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/pthread.init") - -void -github_com_goplus_llgo_runtime_internal_clite_pthread_sync_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/pthread/sync.init") - -void -github_com_goplus_llgo_runtime_internal_clite_signal_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/signal.init") - -void -github_com_goplus_llgo_runtime_internal_clite_tls_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/tls.init") - -void -github_com_goplus_llgo_runtime_internal_runtime_goarch_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/runtime/goarch.init") - -void -github_com_goplus_llgo_runtime_internal_runtime_math_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/runtime/math.init") - -void -github_com_goplus_llgo_runtime_internal_runtime_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/runtime.init") - #ifdef __cplusplus diff --git a/_demo/go/export/use/Makefile b/_demo/go/export/use/Makefile index 2b739907fd..3ed3e93d44 100644 --- a/_demo/go/export/use/Makefile +++ b/_demo/go/export/use/Makefile @@ -9,6 +9,7 @@ INCLUDES = -I.. TARGET = main.out SOURCES = main.c HEADER = ../libexport.h +RUNTIME_LIBS = -lpthread -lm $(shell pkg-config --libs bdw-gc || echo -lgc) $(shell pkg-config --libs libuv || echo -luv) # Default to static linking LINK_TYPE ?= static @@ -27,13 +28,13 @@ endif ifeq ($(LINK_TYPE),shared) BUILDMODE = c-shared LIBRARY = ../libexport.$(SHARED_EXT) - LDFLAGS = -L.. -lexport -lpthread -lm $(shell pkg-config --libs bdw-gc || echo -lgc) $(PLATFORM_LIBS) + LDFLAGS = -L.. -lexport $(RUNTIME_LIBS) $(PLATFORM_LIBS) BUILD_MSG = "Building Go shared library..." LINK_MSG = "Linking with shared library..." else BUILDMODE = c-archive LIBRARY = ../libexport.a - LDFLAGS = $(LIBRARY) -lpthread -lm $(shell pkg-config --libs bdw-gc || echo -lgc) $(PLATFORM_LIBS) + LDFLAGS = $(LIBRARY) $(RUNTIME_LIBS) $(PLATFORM_LIBS) BUILD_MSG = "Building Go static library..." LINK_MSG = "Linking with static library..." endif diff --git a/_demo/go/export/use/main.c b/_demo/go/export/use/main.c index 43603da36b..4e1a911b45 100644 --- a/_demo/go/export/use/main.c +++ b/_demo/go/export/use/main.c @@ -2,8 +2,34 @@ #include #include #include +#include #include "../libexport.h" +static int go_string_equals(GoString got, const char *want) { + size_t want_len = strlen(want); + return got.n == (intptr_t)want_len && memcmp(got.p, want, want_len) == 0; +} + +static int go_string_has_suffix(GoString got, const char *suffix) { + size_t suffix_len = strlen(suffix); + return got.n >= (intptr_t)suffix_len && + memcmp(got.p + got.n - suffix_len, suffix, suffix_len) == 0; +} + +static intptr_t int_callback(intptr_t value) { + return value + 700; +} + +static GoString string_callback(GoString value) { + return value; +} + +static int void_callback_count; + +static void void_callback(void) { + void_callback_count++; +} + int main() { printf("=== C Export Demo ===\n"); fflush(stdout); // Force output @@ -12,6 +38,23 @@ int main() { github_com_goplus_llgo__demo_go_export_c_init(); github_com_goplus_llgo__demo_go_export_init(); + // Verify that funcinfo is not merely linkable: runtime.Callers must yield + // a symbolized frame and runtime.FuncForPC must resolve that PC's details. + main_FuncInfoResult func_info = GetFuncInfo(); + assert(func_info.CallersCount > 0); + assert(func_info.FramePC != 0); + assert(go_string_equals(func_info.FrameFunction, "main.captureCFuncInfo")); + assert(go_string_has_suffix(func_info.FrameFile, "export.go")); + assert(func_info.FrameLine > 0); + assert(go_string_equals(func_info.FuncName, "main.captureCFuncInfo")); + assert(func_info.FuncEntry != 0); + assert(go_string_has_suffix(func_info.FuncFile, "export.go")); + assert(func_info.FuncLine == func_info.FrameLine); + printf("FuncInfo: %.*s %.*s:%d\n", + (int)func_info.FuncName.n, func_info.FuncName.p, + (int)func_info.FuncFile.n, func_info.FuncFile.p, + func_info.FuncLine); + // Test HelloWorld HelloWorld(); printf("\n"); @@ -103,6 +146,15 @@ int main() { assert(f64_result > 7.84 && f64_result < 7.86); // ProcessFloat64(x) returns x * 2.5 ≈ 7.85 printf("Float64: %f\n", f64_result); + GoString raw_string = {"value", 5}; + GoString processed_string = ProcessString(raw_string); + assert(go_string_equals(processed_string, "processed_value")); + assert(RunGoroutine(41) == 42); + assert(go_string_equals(ProcessMyString(raw_string), "modified_value")); + assert(go_string_equals(TwoParams(65, (GoString){"bc", 2}), "Abc")); + assert(go_string_equals(MultipleParams(1, 2, 3, 4, 5.0f, 6.0, + (GoString){"multi", 5}, 1), "multi_B23_true_4_5_6")); + // Test unsafe pointer int test_val = 42; void* ptr_result = ProcessUnsafePointer(&test_val); @@ -120,10 +172,29 @@ int main() { // Test complex data with multidimensional arrays main_ComplexData complex = CreateComplexData(); + assert(ProcessComplexData(complex) == 78); + assert(complex.IntArray[0] == 10 && complex.IntArray[4] == 50); + assert(complex.Slices.len == 1); + assert(complex.DataList.len == 1); + assert(((double*)complex.DataList.data)[0] == 1.0); printf("Complex data matrix sum: %" PRId32 "\n", ProcessComplexData(complex)); - // Test interface - this is more complex in C, we'll skip for now - printf("Interface test skipped (complex in C)\n"); + intptr_t c_slice_values[] = {3, 6, 9}; + GoSlice c_slice = {c_slice_values, 3, 3}; + assert(ProcessIntSlice(c_slice) == 18); + GoSlice int_slice = CreateIntSlice(); + assert(int_slice.len == 4); + assert(ProcessIntSlice(int_slice) == 20); + GoMap string_map = CreateStringMap(); + assert(string_map.data != NULL); + assert(ProcessStringMap(string_map) == 60); + GoChan int_channel = CreateIntChannel(); + assert(int_channel.data != NULL); + assert(ProcessIntChannel(int_channel) == 321); + GoInterface int_interface = CreateIntInterface(); + GoInterface string_interface = CreateStringInterface(); + assert(ProcessInterface(int_interface) == 123); + assert(ProcessInterface(string_interface) == 40); // Test various parameter counts assert(NoParams() == 42); // NoParams() always returns 42 @@ -141,6 +212,11 @@ int main() { assert(unnamed_result == 22.5); // (10 + 5) * 1.5 = 22.5 printf("ProcessThreeUnnamedParams: %f\n", unnamed_result); + assert(ProcessWithIntCallback(23, int_callback) == 723); + assert(go_string_equals(ProcessWithStringCallback(raw_string, string_callback), "value")); + assert(ProcessWithVoidCallback(void_callback) == 123); + assert(void_callback_count == 1); + // Test ProcessWithVoidCallback - now returns int int void_callback_result = ProcessWithVoidCallback(NULL); assert(void_callback_result == 456); // Returns 456 when callback is nil @@ -237,10 +313,8 @@ int main() { assert(grid.data[0][0] == 1.0); printf("CreateGrid5x4() returned struct, first element: %f\n", grid.data[0][0]); - // Test NoReturn function - // Note: This function takes a string parameter which is complex to pass from C - // We'll skip it for now or pass a simple string if the binding allows - printf("NoReturn test skipped (string parameter)\n"); + // Test a void function with a string parameter. + NoReturn((GoString){"called from C", 13}); printf("C demo completed!\n"); From 7858ab9e11e39779ab06233b642d7ec19d1dc046 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 16 Jul 2026 00:16:12 +0800 Subject: [PATCH 9/9] test: cover missing C header exports --- internal/header/header_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/header/header_test.go b/internal/header/header_test.go index d94c726cb9..1080b2944a 100644 --- a/internal/header/header_test.go +++ b/internal/header/header_test.go @@ -115,6 +115,18 @@ func TestGenCHeaderExport(t *testing.T) { } } +func TestGenHeaderMissingExport(t *testing.T) { + prog := ssa.NewProgram(nil) + pkgPath := "github.com/goplus/llgo/test_buildmode/missing" + pkg := prog.NewPackage("missing", pkgPath) + pkg.SetExport("Missing", "Missing") + + err := genHeader(prog, []ssa.Package{pkg}, &bytes.Buffer{}) + if err == nil || err.Error() != "function Missing not found in package "+pkgPath { + t.Fatalf("genHeader() error = %v", err) + } +} + func TestCheaderWriterTypes(t *testing.T) { prog := ssa.NewProgram(nil) hw := newCHeaderWriter(prog)