Commit f87b443af1

Alex Rønne Petersen <alex@alexrp.com>
2025-02-16 06:29:08
link.MachO: Add support for the -x flag (discard local symbols).
This can also be extended to ELF later as it means roughly the same thing there. This addresses the main issue in #21721 but as I don't have a macOS machine to do further testing on, I can't confirm whether zig cc is able to pass the entire cgo test suite after this commit. It can, however, cross-compile a basic program that uses cgo to x86_64-macos-none which previously failed due to lack of -x support. Unlike previously, the resulting symbol table does not contain local symbols (such as C static functions). I believe this satisfies the related donor bounty: https://ziglang.org/news/second-donor-bounty
1 parent a7467b9
lib/std/Build/Step/Compile.zig
@@ -160,6 +160,9 @@ dead_strip_dylibs: bool = false,
 /// (Darwin) Force load all members of static archives that implement an Objective-C class or category
 force_load_objc: bool = false,
 
+/// Whether local symbols should be discarded from the symbol table.
+discard_local_symbols: bool = false,
+
 /// Position Independent Executable
 pie: ?bool = null,
 
@@ -1555,6 +1558,9 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
     if (compile.force_load_objc) {
         try zig_args.append("-ObjC");
     }
+    if (compile.discard_local_symbols) {
+        try zig_args.append("--discard-all");
+    }
 
     try addFlag(&zig_args, "compiler-rt", compile.bundle_compiler_rt);
     try addFlag(&zig_args, "dll-export-fns", compile.dll_export_fns);
src/link/MachO/InternalObject.zig
@@ -583,6 +583,7 @@ pub fn calcSymtabSize(self: *InternalObject, macho_file: *MachO) void {
         const file = ref.getFile(macho_file) orelse continue;
         if (file.getIndex() != self.index) continue;
         if (sym.getName(macho_file).len == 0) continue;
+        if (macho_file.discard_local_symbols and sym.isLocal()) continue;
         sym.flags.output_symtab = true;
         if (sym.isLocal()) {
             sym.addExtra(.{ .symtab = self.output_symtab_ctx.nlocals }, macho_file);
src/link/MachO/Object.zig
@@ -1722,6 +1722,7 @@ pub fn calcSymtabSize(self: *Object, macho_file: *MachO) void {
         if (file.getIndex() != self.index) continue;
         if (sym.getAtom(macho_file)) |atom| if (!atom.isAlive()) continue;
         if (sym.isSymbolStab(macho_file)) continue;
+        if (macho_file.discard_local_symbols and sym.isLocal()) continue;
         const name = sym.getName(macho_file);
         if (name.len == 0) continue;
         // TODO in -r mode, we actually want to merge symbol names and emit only one
src/link/MachO/synthetic.zig
@@ -532,17 +532,23 @@ pub const Indsymtab = struct {
 
         for (macho_file.stubs.symbols.items) |ref| {
             const sym = ref.getSymbol(macho_file).?;
-            try writer.writeInt(u32, sym.getOutputSymtabIndex(macho_file).?, .little);
+            if (sym.getOutputSymtabIndex(macho_file)) |idx| {
+                try writer.writeInt(u32, idx, .little);
+            }
         }
 
         for (macho_file.got.symbols.items) |ref| {
             const sym = ref.getSymbol(macho_file).?;
-            try writer.writeInt(u32, sym.getOutputSymtabIndex(macho_file).?, .little);
+            if (sym.getOutputSymtabIndex(macho_file)) |idx| {
+                try writer.writeInt(u32, idx, .little);
+            }
         }
 
         for (macho_file.stubs.symbols.items) |ref| {
             const sym = ref.getSymbol(macho_file).?;
-            try writer.writeInt(u32, sym.getOutputSymtabIndex(macho_file).?, .little);
+            if (sym.getOutputSymtabIndex(macho_file)) |idx| {
+                try writer.writeInt(u32, idx, .little);
+            }
         }
     }
 };
src/link/MachO/ZigObject.zig
@@ -500,6 +500,7 @@ pub fn calcSymtabSize(self: *ZigObject, macho_file: *MachO) void {
         const file = ref.getFile(macho_file) orelse continue;
         if (file.getIndex() != self.index) continue;
         if (sym.getAtom(macho_file)) |atom| if (!atom.isAlive()) continue;
+        if (macho_file.discard_local_symbols and sym.isLocal()) continue;
         const name = sym.getName(macho_file);
         assert(name.len > 0);
         sym.flags.output_symtab = true;
src/link/MachO.zig
@@ -139,6 +139,8 @@ no_implicit_dylibs: bool = false,
 /// Whether the linker should parse and always force load objects containing ObjC in archives.
 // TODO: in Zig we currently take -ObjC as always on
 force_load_objc: bool = true,
+/// Whether local symbols should be discarded from the symbol table.
+discard_local_symbols: bool = false,
 
 /// Hot-code swapping state.
 hot_state: if (is_hot_update_compatible) HotUpdateState else struct {} = .{},
@@ -221,6 +223,7 @@ pub fn createEmpty(
         .lib_directories = options.lib_directories,
         .framework_dirs = options.framework_dirs,
         .force_load_objc = options.force_load_objc,
+        .discard_local_symbols = options.discard_local_symbols,
     };
     if (use_llvm and comp.config.have_zcu) {
         self.llvm_object = try LlvmObject.create(arena, comp);
@@ -720,6 +723,10 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void {
             try argv.append("-ObjC");
         }
 
+        if (self.discard_local_symbols) {
+            try argv.append("-x");
+        }
+
         if (self.entry_name) |entry_name| {
             try argv.appendSlice(&.{ "-e", entry_name });
         }
src/Compilation.zig
@@ -1160,6 +1160,8 @@ pub const CreateOptions = struct {
     dead_strip_dylibs: bool = false,
     /// (Darwin) Force load all members of static archives that implement an Objective-C class or category
     force_load_objc: bool = false,
+    /// Whether local symbols should be discarded from the symbol table.
+    discard_local_symbols: bool = false,
     libcxx_abi_version: libcxx.AbiVersion = libcxx.AbiVersion.default,
     /// (Windows) PDB source path prefix to instruct the linker how to resolve relative
     /// paths when consolidating CodeView streams into a single PDB file.
@@ -1585,6 +1587,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             .headerpad_max_install_names = options.headerpad_max_install_names,
             .dead_strip_dylibs = options.dead_strip_dylibs,
             .force_load_objc = options.force_load_objc,
+            .discard_local_symbols = options.discard_local_symbols,
             .pdb_source_path = options.pdb_source_path,
             .pdb_out_path = options.pdb_out_path,
             .entry_addr = null, // CLI does not expose this option (yet?)
@@ -2665,6 +2668,7 @@ fn addNonIncrementalStuffToCacheManifest(
     man.hash.add(opts.headerpad_max_install_names);
     man.hash.add(opts.dead_strip_dylibs);
     man.hash.add(opts.force_load_objc);
+    man.hash.add(opts.discard_local_symbols);
 
     // COFF specific stuff
     man.hash.addOptional(opts.subsystem);
src/link.zig
@@ -491,6 +491,8 @@ pub const File = struct {
         /// Force load all members of static archives that implement an
         /// Objective-C class or category
         force_load_objc: bool,
+        /// Whether local symbols should be discarded from the symbol table.
+        discard_local_symbols: bool,
 
         /// Windows-specific linker flags:
         /// PDB source path prefix to instruct the linker how to resolve relative
src/main.zig
@@ -918,6 +918,7 @@ fn buildOutputType(
     var headerpad_max_install_names: bool = false;
     var dead_strip_dylibs: bool = false;
     var force_load_objc: bool = false;
+    var discard_local_symbols: bool = false;
     var contains_res_file: bool = false;
     var reference_trace: ?u32 = null;
     var pdb_out_path: ?[]const u8 = null;
@@ -1151,6 +1152,8 @@ fn buildOutputType(
                         entry = .{ .named = arg["-fentry=".len..] };
                     } else if (mem.eql(u8, arg, "--force_undefined")) {
                         try force_undefined_symbols.put(arena, args_iter.nextOrFatal(), {});
+                    } else if (mem.eql(u8, arg, "--discard-all")) {
+                        discard_local_symbols = true;
                     } else if (mem.eql(u8, arg, "--stack")) {
                         stack_size = parseStackSize(args_iter.nextOrFatal());
                     } else if (mem.eql(u8, arg, "--image-base")) {
@@ -2510,6 +2513,8 @@ fn buildOutputType(
                     entry = .{ .named = linker_args_it.nextOrFatal() };
                 } else if (mem.eql(u8, arg, "-u")) {
                     try force_undefined_symbols.put(arena, linker_args_it.nextOrFatal(), {});
+                } else if (mem.eql(u8, arg, "-x") or mem.eql(u8, arg, "--discard-all")) {
+                    discard_local_symbols = true;
                 } else if (mem.eql(u8, arg, "--stack") or mem.eql(u8, arg, "-stack_size")) {
                     stack_size = parseStackSize(linker_args_it.nextOrFatal());
                 } else if (mem.eql(u8, arg, "--image-base")) {
@@ -3579,6 +3584,7 @@ fn buildOutputType(
         .headerpad_max_install_names = headerpad_max_install_names,
         .dead_strip_dylibs = dead_strip_dylibs,
         .force_load_objc = force_load_objc,
+        .discard_local_symbols = discard_local_symbols,
         .reference_trace = reference_trace,
         .pdb_out_path = pdb_out_path,
         .error_limit = error_limit,
test/link/macho.zig
@@ -62,6 +62,7 @@ pub fn testAll(b: *Build, build_opts: BuildOptions) *Step {
     macho_step.dependOn(testTlsLargeTbss(b, .{ .target = default_target }));
     macho_step.dependOn(testTlsZig(b, .{ .target = default_target }));
     macho_step.dependOn(testUndefinedFlag(b, .{ .target = default_target }));
+    macho_step.dependOn(testDiscardLocalSymbols(b, .{ .target = default_target }));
     macho_step.dependOn(testUnresolvedError(b, .{ .target = default_target }));
     macho_step.dependOn(testUnresolvedError2(b, .{ .target = default_target }));
     macho_step.dependOn(testUnwindInfo(b, .{ .target = default_target }));
@@ -2502,6 +2503,51 @@ fn testTwoLevelNamespace(b: *Build, opts: Options) *Step {
     return test_step;
 }
 
+fn testDiscardLocalSymbols(b: *Build, opts: Options) *Step {
+    const test_step = addTestStep(b, "discard-local-symbols", opts);
+
+    const obj = addObject(b, opts, .{ .name = "a", .c_source_bytes = "static int foo = 42;" });
+
+    const lib = addStaticLibrary(b, opts, .{ .name = "a" });
+    lib.addObject(obj);
+
+    const main_o = addObject(b, opts, .{ .name = "main", .c_source_bytes = "int main() { return 0; }" });
+
+    {
+        const exe = addExecutable(b, opts, .{ .name = "main3" });
+        exe.addObject(main_o);
+        exe.addObject(obj);
+        exe.discard_local_symbols = true;
+
+        const run = addRunArtifact(exe);
+        run.expectExitCode(0);
+        test_step.dependOn(&run.step);
+
+        const check = exe.checkObject();
+        check.checkInSymtab();
+        check.checkNotPresent("_foo");
+        test_step.dependOn(&check.step);
+    }
+
+    {
+        const exe = addExecutable(b, opts, .{ .name = "main4" });
+        exe.addObject(main_o);
+        exe.linkLibrary(lib);
+        exe.discard_local_symbols = true;
+
+        const run = addRunArtifact(exe);
+        run.expectExitCode(0);
+        test_step.dependOn(&run.step);
+
+        const check = exe.checkObject();
+        check.checkInSymtab();
+        check.checkNotPresent("_foo");
+        test_step.dependOn(&check.step);
+    }
+
+    return test_step;
+}
+
 fn testUndefinedFlag(b: *Build, opts: Options) *Step {
     const test_step = addTestStep(b, "undefined-flag", opts);