diff --git a/_demo/go/export/test.sh b/_demo/go/export/test.sh index 56fb915366..f4db85a947 100755 --- a/_demo/go/export/test.sh +++ b/_demo/go/export/test.sh @@ -47,6 +47,54 @@ check_file() { fi } +# Check that explicitly enabled LLGo DWARF is usable for a Go function reached +# through the C library. Darwin uses LLDB because Mach-O debug maps may refer +# back to archive members; ELF keeps the DWARF in the linked artifact. +check_c_library_debug_info() { + local build_mode="$1" + local artifact="$2" + local executable="$3" + local output + + if [[ "$OSTYPE" == "darwin"* ]]; then + if ! output=$(DYLD_LIBRARY_PATH=.. lldb --batch \ + -o "settings set target.env-vars DYLD_LIBRARY_PATH=.." \ + -o "breakpoint set -n main" \ + -o run \ + -o "breakpoint set -r captureCFuncInfo" \ + -o continue \ + -o "frame info" \ + -o "process kill" \ + "./$(basename "$executable")" 2>&1); then + print_error "$build_mode: LLDB verification failed" + echo "$output" + return 1 + fi + if ! grep -Eq 'captureCFuncInfo at .*export\.go:[0-9]+' <<< "$output"; then + print_error "$build_mode: LLDB did not resolve the Go source location" + echo "$output" + return 1 + fi + else + if [[ "$build_mode" == "c-archive" ]]; then + artifact="$executable" + fi + if ! output=$(llvm-dwarfdump --regex --name captureCFuncInfo "$artifact" 2>&1); then + print_error "$build_mode: DWARF inspection failed" + echo "$output" + return 1 + fi + if ! grep -q 'DW_TAG_subprogram' <<< "$output" || \ + ! grep -q 'export.go' <<< "$output"; then + print_error "$build_mode: DWARF does not resolve the Go function and source file" + echo "$output" + return 1 + fi + fi + + print_status "$build_mode: debug information resolved captureCFuncInfo in export.go" +} + # Function to compare header with expected content compare_header() { local header_file="$1" @@ -139,7 +187,7 @@ if $LLGO_SCRIPT build -buildmode c-shared -o export .; then # Test C demo with shared library print_status "=== Testing C demo with shared library ===" if cd use; then - if LINK_TYPE=shared make clean && LINK_TYPE=shared make; then + if LINK_TYPE=shared make clean && LINK_TYPE=shared LLGOFLAGS=-ldflags=-w=false make; then print_status "C demo build succeeded with shared library" if LINK_TYPE=shared make run; then print_status "C demo execution succeeded with shared library" @@ -147,6 +195,9 @@ if $LLGO_SCRIPT build -buildmode c-shared -o export .; then print_error "C demo execution failed with shared library" build_failures=$((build_failures + 1)) fi + if ! check_c_library_debug_info "c-shared" "../$SHARED_LIB" "main.out"; then + build_failures=$((build_failures + 1)) + fi else print_error "C demo build failed with shared library" build_failures=$((build_failures + 1)) @@ -180,7 +231,7 @@ if $LLGO_SCRIPT build -buildmode c-archive -o export .; then # Test C demo with static library print_status "=== Testing C demo with static library ===" if cd use; then - if make clean && make; then + if make clean && LLGOFLAGS=-ldflags=-w=false make; then print_status "C demo build succeeded with static library" if make run; then print_status "C demo execution succeeded with static library" @@ -188,6 +239,9 @@ if $LLGO_SCRIPT build -buildmode c-archive -o export .; then print_error "C demo execution failed with static library" build_failures=$((build_failures + 1)) fi + if ! check_c_library_debug_info "c-archive" "../libexport.a" "main.out"; then + build_failures=$((build_failures + 1)) + fi else print_error "C demo build failed with static library" build_failures=$((build_failures + 1)) @@ -276,6 +330,7 @@ elif [[ "$build_failures" -eq 0 ]] && [[ -f "libexport.a" ]] && [[ -f "libexport 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 with shared and static libraries" + print_status " ✅ Explicit -w=false DWARF source lookup for c-archive and c-shared" 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/Makefile b/_demo/go/export/use/Makefile index 3ed3e93d44..a7c75c2722 100644 --- a/_demo/go/export/use/Makefile +++ b/_demo/go/export/use/Makefile @@ -13,6 +13,7 @@ RUNTIME_LIBS = -lpthread -lm $(shell pkg-config --libs bdw-gc || echo -lgc) $(sh # Default to static linking LINK_TYPE ?= static +LLGOFLAGS ?= # Platform detection UNAME_S := $(shell uname -s) @@ -46,7 +47,7 @@ all: build-go $(TARGET) # Build the Go library first build-go: @echo $(BUILD_MSG) - cd .. && ../../../dev/llgo.sh build -buildmode $(BUILDMODE) -o export . + cd .. && ../../../dev/llgo.sh build -buildmode $(BUILDMODE) $(LLGOFLAGS) -o export . # Build the C executable $(TARGET): $(SOURCES) $(LIBRARY) $(HEADER) diff --git a/internal/build/link_options.go b/internal/build/link_options.go index 2af056aa71..1a7c0140d8 100644 --- a/internal/build/link_options.go +++ b/internal/build/link_options.go @@ -106,9 +106,6 @@ func validateLinkOptions(conf *Config, target *crosscompile.Export) error { if !omitDWARFRequested(conf) { return nil } - if conf.BuildMode != BuildModeExe { - return fmt.Errorf("DWARF omission is not supported for -buildmode=%s", conf.BuildMode) - } if target.DebugInfo.AlwaysOmit { return nil } @@ -126,5 +123,10 @@ func dwarfLinkerArgs(conf *Config, target *crosscompile.Export) []string { if target.DebugInfo.AlwaysOmit || !effectiveOmitDWARF(conf, target) { return nil } + // c-archive has no final native link step. Omitting generated DWARF is + // sufficient; consumers decide how to link the archive later. + if conf.BuildMode == BuildModeCArchive { + return nil + } return slices.Clone(target.DebugInfo.OmitLinkFlags) } diff --git a/internal/build/link_options_test.go b/internal/build/link_options_test.go index ae44dc2146..b903487dfe 100644 --- a/internal/build/link_options_test.go +++ b/internal/build/link_options_test.go @@ -40,7 +40,11 @@ func TestDwarfLinkerArgs(t *testing.T) { want []string }{ {name: "default"}, - {name: "safe default", conf: Config{OmitDWARFByDefault: true}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, + {name: "safe default", conf: Config{BuildMode: BuildModeExe, OmitDWARFByDefault: true}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, + {name: "safe default c-shared", conf: Config{BuildMode: BuildModeCShared, OmitDWARFByDefault: true}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, + {name: "safe default c-archive", conf: Config{BuildMode: BuildModeCArchive, OmitDWARFByDefault: true}, target: configurableDebugInfo()}, + {name: "explicit c-shared w", conf: Config{BuildMode: BuildModeCShared, LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, + {name: "explicit c-archive w", conf: Config{BuildMode: BuildModeCArchive, LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: configurableDebugInfo()}, {name: "w", conf: Config{LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, {name: "s implies w", conf: Config{LinkOptions: LinkOptions{OmitSymbolTable: true}}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, {name: "explicit w false", conf: Config{LinkOptions: LinkOptions{OmitSymbolTable: true, DWARF: DWARFPreserve}}}, @@ -63,7 +67,9 @@ func TestEffectiveOmitDWARF(t *testing.T) { want bool }{ {name: "default"}, - {name: "safe default", conf: Config{OmitDWARFByDefault: true}, want: true}, + {name: "safe default", conf: Config{BuildMode: BuildModeExe, OmitDWARFByDefault: true}, want: true}, + {name: "safe default c-shared", conf: Config{BuildMode: BuildModeCShared, OmitDWARFByDefault: true}, want: true}, + {name: "safe default c-archive", conf: Config{BuildMode: BuildModeCArchive, OmitDWARFByDefault: true}, want: true}, {name: "requested", conf: Config{LinkOptions: LinkOptions{DWARF: DWARFOmit}}, want: true}, {name: "target baseline", target: alwaysOmitDebugInfo(), want: true}, {name: "explicit preserve", conf: Config{LinkOptions: LinkOptions{DWARF: DWARFPreserve}}}, @@ -86,7 +92,9 @@ func TestShouldEmitDebugInfo(t *testing.T) { want bool }{ {name: "linked default", conf: Config{Mode: ModeBuild}, want: true}, - {name: "linked safe default", conf: Config{Mode: ModeBuild, OmitDWARFByDefault: true}}, + {name: "linked safe default", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, OmitDWARFByDefault: true}}, + {name: "c-shared safe default", conf: Config{Mode: ModeBuild, BuildMode: BuildModeCShared, OmitDWARFByDefault: true}}, + {name: "c-archive safe default", conf: Config{Mode: ModeBuild, BuildMode: BuildModeCArchive, OmitDWARFByDefault: true}}, {name: "linked w", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFOmit}}}, {name: "linked s", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{OmitSymbolTable: true}}}, {name: "linked s w false", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{OmitSymbolTable: true, DWARF: DWARFPreserve}}, want: true}, @@ -115,8 +123,13 @@ func TestValidateLinkOptions(t *testing.T) { {name: "linux executable", conf: Config{Goos: "linux", BuildMode: BuildModeExe, LinkOptions: w}, target: configurableDebugInfo()}, {name: "linux executable safe default", conf: Config{Goos: "linux", BuildMode: BuildModeExe, OmitDWARFByDefault: true}, target: configurableDebugInfo()}, {name: "darwin executable", conf: Config{Goos: "darwin", BuildMode: BuildModeExe, LinkOptions: w}, target: configurableDebugInfo()}, + {name: "c-shared safe default", conf: Config{Goos: "linux", BuildMode: BuildModeCShared, OmitDWARFByDefault: true}, target: configurableDebugInfo()}, + {name: "c-archive safe default", conf: Config{Goos: "linux", BuildMode: BuildModeCArchive, OmitDWARFByDefault: true}, target: configurableDebugInfo()}, {name: "unsupported native OS", conf: Config{Goos: "windows", BuildMode: BuildModeExe, LinkOptions: w}, wantErr: true}, - {name: "unsupported build mode", conf: Config{Goos: "linux", BuildMode: BuildModeCShared, LinkOptions: w}, target: configurableDebugInfo(), wantErr: true}, + {name: "c-shared omit", conf: Config{Goos: "linux", BuildMode: BuildModeCShared, LinkOptions: w}, target: configurableDebugInfo()}, + {name: "c-archive omit", conf: Config{Goos: "linux", BuildMode: BuildModeCArchive, LinkOptions: w}, target: configurableDebugInfo()}, + {name: "c-shared preserve", conf: Config{Goos: "linux", BuildMode: BuildModeCShared, LinkOptions: wFalse}, target: configurableDebugInfo()}, + {name: "c-archive preserve", conf: Config{Goos: "linux", BuildMode: BuildModeCArchive, LinkOptions: wFalse}, target: configurableDebugInfo()}, {name: "fixed target omit", conf: Config{Target: "rp2040", Goos: "linux", BuildMode: BuildModeExe, LinkOptions: w}, target: alwaysOmitDebugInfo()}, {name: "fixed target explicit DWARF", conf: Config{Target: "rp2040", Goos: "linux", BuildMode: BuildModeExe, LinkOptions: wFalse}, target: alwaysOmitDebugInfo(), wantErr: true}, {name: "configurable WASI omit", conf: Config{Target: "wasi", Goos: "wasip1", BuildMode: BuildModeExe, LinkOptions: w}, target: configurableDebugInfo()}, diff --git a/internal/build/main_module.go b/internal/build/main_module.go index 67378f6e2e..5dbf0ad81f 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -51,8 +51,8 @@ type genConfig struct { // genMainModule generates the main entry module for an llgo program. // // The module contains argc/argv globals and, for executable build modes, -// the entry function that wires initialization and main. For C archive or -// shared library modes, only the globals are emitted. +// the entry function that wires initialization and main. C archive and shared +// library modes also get a constructor when the LLGo runtime is linked. func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *genConfig) Package { prog := ctx.prog mainPkg := prog.NewPackage("", pkg.ID+".main") @@ -78,6 +78,9 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g } if ctx.buildConf.BuildMode != BuildModeExe { + if cfg.rtInit { + defineLibraryRuntimeInit(mainPkg, declareNoArgFunc(mainPkg, rtPkgPath+".init")) + } return mainAPkg } @@ -127,6 +130,33 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g return mainAPkg } +// defineLibraryRuntimeInit arranges for the LLGo runtime to be initialized +// before a C program calls an exported Go function. llvm.global_ctors is +// lowered to the platform's native constructor mechanism for both shared +// libraries and archive members. +func defineLibraryRuntimeInit(pkg llssa.Package, rtInit llssa.Function) { + const ctorName = "__llgo_runtime_ctor" + ctor := pkg.NewFunc(ctorName, llssa.NoArgsNoRet, llssa.InC) + ctorValue := pkg.Module().NamedFunction(ctorName) + ctorValue.SetLinkage(llvm.InternalLinkage) + b := ctor.MakeBody(1) + b.Call(rtInit.Expr) + b.Return() + + mod := pkg.Module() + llvmCtx := mod.Context() + priority := llvm.ConstInt(llvmCtx.Int32Type(), 65535, false) + entry := llvmCtx.ConstStruct([]llvm.Value{ + priority, + ctorValue, + llvm.ConstNull(ctorValue.Type()), + }, false) + init := llvm.ConstArray(entry.Type(), []llvm.Value{entry}) + ctors := llvm.AddGlobal(mod, init.Type(), "llvm.global_ctors") + ctors.SetInitializer(init) + ctors.SetLinkage(llvm.AppendingLinkage) +} + func filterAbiSymbol(abiInit int, sym *llssa.AbiSymbol) bool { switch sym.Raw.(type) { case *types.Array: diff --git a/internal/build/main_module_test.go b/internal/build/main_module_test.go index 7e4f140419..f803182482 100644 --- a/internal/build/main_module_test.go +++ b/internal/build/main_module_test.go @@ -75,6 +75,42 @@ func TestGenMainModuleLibrary(t *testing.T) { if !strings.Contains(ir, "@__llgo_argc = global i32 0") { t.Fatalf("library mode missing argc global:\n%s", ir) } + if strings.Contains(ir, "@llvm.global_ctors") { + t.Fatalf("library mode without the runtime should not emit a constructor:\n%s", ir) + } +} + +func TestGenMainModuleLibraryInitializesRuntime(t *testing.T) { + llvm.InitializeAllTargets() + t.Setenv(llgoStdioNobuf, "") + for _, mode := range []BuildMode{BuildModeCArchive, BuildModeCShared} { + t.Run(string(mode), func(t *testing.T) { + ctx := &context{ + prog: llssa.NewProgram(nil), + buildConf: &Config{ + BuildMode: mode, + Goos: "linux", + Goarch: "amd64", + }, + } + pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"} + mod := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{rtInit: true}) + ir := mod.LPkg.String() + checks := []string{ + "@llvm.global_ctors = appending global", + "define internal void @__llgo_runtime_ctor()", + "call void @\"github.com/goplus/llgo/runtime/internal/runtime.init\"()", + } + for _, want := range checks { + if !strings.Contains(ir, want) { + t.Fatalf("library module IR missing %q:\n%s", want, ir) + } + } + if strings.Contains(ir, "define i32 @main") { + t.Fatalf("library mode should not emit main function:\n%s", ir) + } + }) + } } func assertInOrder(t *testing.T, s string, wants ...string) {