Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions _demo/go/export/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -139,14 +187,17 @@ 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"
else
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))
Expand Down Expand Up @@ -180,14 +231,17 @@ 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"
else
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))
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion _demo/go/export/use/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 5 additions & 3 deletions internal/build/link_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
}
21 changes: 17 additions & 4 deletions internal/build/link_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}},
Expand All @@ -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}}},
Expand All @@ -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},
Expand Down Expand Up @@ -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()},
Expand Down
34 changes: 32 additions & 2 deletions internal/build/main_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
}

Expand Down Expand Up @@ -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:
Expand Down
36 changes: 36 additions & 0 deletions internal/build/main_module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading