//! Zig wrapper for LLD (LLVM Linker) functionality. //! Provides a high-level interface for linking object files into executables. //! Supports ELF, COFF, MachO, and WebAssembly targets. const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const base = @import("base"); const Allocators = base.Allocators; const libc_finder = @import("libc_finder.zig"); const RocTarget = @import("roc_target").RocTarget; /// External C functions from zig_llvm.cpp - only available when LLVM is enabled const llvm_available = if (@import("builtin").is_test) false else @import("config").llvm; // External C functions from zig_llvm.cpp - only available when LLVM is enabled const llvm_externs = if (llvm_available) struct { extern fn ZigLLDLinkCOFF(argc: c_int, argv: [*]const [*:0]const u8, can_exit_early: bool, disable_output: bool) bool; extern fn ZigLLDLinkELF(argc: c_int, argv: [*]const [*:0]const u8, can_exit_early: bool, disable_output: bool) bool; extern fn ZigLLDLinkMachO(argc: c_int, argv: [*]const [*:0]const u8, can_exit_early: bool, disable_output: bool) bool; extern fn ZigLLDLinkWasm(argc: c_int, argv: [*]const [*:0]const u8, can_exit_early: bool, disable_output: bool) bool; } else struct {}; /// Supported target formats for linking pub const TargetFormat = enum { elf, coff, macho, wasm, /// Automatically detect target format based on the current system pub fn detectFromSystem() TargetFormat { return switch (builtin.target.os.tag) { .windows => .coff, .macos, .ios, .watchos, .tvos => .macho, .freestanding => .wasm, else => .elf, }; } /// Detect target format from OS tag pub fn detectFromOs(os: std.Target.Os.Tag) TargetFormat { return switch (os) { .windows => .coff, .macos, .ios, .watchos, .tvos => .macho, .freestanding => .wasm, else => .elf, }; } }; /// Target ABI for runtime-configurable linking pub const TargetAbi = enum { musl, gnu, /// Convert from RocTarget to TargetAbi pub fn fromRocTarget(roc_target: RocTarget) TargetAbi { return if (roc_target.isStatic()) .musl else .gnu; } }; /// Default WASM initial memory: 64MB pub const DEFAULT_WASM_INITIAL_MEMORY: usize = 64 * 1024 * 1024; /// Default WASM stack size: 8MB pub const DEFAULT_WASM_STACK_SIZE: usize = 8 * 1024 * 1024; /// Configuration for the linker, specifying target format, ABI, paths, and linking options. pub const LinkConfig = struct { /// Target format to use for linking target_format: TargetFormat = TargetFormat.detectFromSystem(), /// Target ABI - determines static vs dynamic linking strategy target_abi: ?TargetAbi = null, // null means detect from system /// Target OS tag - for cross-compilation support target_os: ?std.Target.Os.Tag = null, // null means detect from system /// Target CPU architecture - for cross-compilation support target_arch: ?std.Target.Cpu.Arch = null, // null means detect from system /// Output executable path output_path: []const u8, /// Input object files to link object_files: []const []const u8, /// Platform-provided files to link before object files (e.g., Scrt1.o, crti.o, host.o) platform_files_pre: []const []const u8 = &.{}, /// Platform-provided files to link after object files (e.g., crtn.o) platform_files_post: []const []const u8 = &.{}, /// Additional linker flags extra_args: []const []const u8 = &.{}, /// Whether to allow LLD to exit early on errors can_exit_early: bool = false, /// Whether to disable linker output disable_output: bool = false, /// Initial memory size for WASM targets (bytes). This is the amount of linear memory /// available to the WASM module at runtime. Must be a multiple of 64KB (WASM page size). wasm_initial_memory: usize = DEFAULT_WASM_INITIAL_MEMORY, /// Stack size for WASM targets (bytes). This is the amount of memory reserved for the /// call stack within the WASM linear memory. Must be a multiple of 16 (stack alignment). wasm_stack_size: usize = DEFAULT_WASM_STACK_SIZE, }; /// Errors that can occur during linking pub const LinkError = error{ LinkFailed, OutOfMemory, InvalidArguments, LLVMNotAvailable, WindowsSDKNotFound, } || std.zig.system.DetectError; /// Build the linker command arguments for the given configuration. /// Returns the args array that would be passed to LLD. /// This is used both by link() and formatLinkCommand(). fn buildLinkArgs(allocs: *Allocators, config: LinkConfig) LinkError!std.array_list.Managed([]const u8) { // Use arena allocator for all temporary allocations // Pre-allocate capacity to avoid reallocations (typical command has 20-40 args) var args = std.array_list.Managed([]const u8).initCapacity(allocs.arena, 64) catch return LinkError.OutOfMemory; // Add platform-specific linker name and arguments // Use target OS if provided, otherwise fall back to host OS const target_os = config.target_os orelse builtin.target.os.tag; const target_arch = config.target_arch orelse builtin.target.cpu.arch; switch (target_os) { .macos => { // Add linker name for macOS try args.append("ld64.lld"); // Add output argument try args.append("-o"); try args.append(config.output_path); // Suppress LLD warnings try args.append("-w"); // Add architecture flag try args.append("-arch"); switch (target_arch) { .aarch64 => try args.append("arm64"), .x86_64 => try args.append("x86_64"), else => try args.append("arm64"), // default to arm64 } // Add platform version - use a conservative minimum that works across macOS versions try args.append("-platform_version"); try args.append("macos"); try args.append("13.0"); // minimum deployment target try args.append("13.0"); // SDK version // Add SDK path try args.append("-syslibroot"); try args.append("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"); // Link against system libraries on macOS try args.append("-lSystem"); }, .linux => { // Add linker name for Linux try args.append("ld.lld"); // Add output argument try args.append("-o"); try args.append(config.output_path); // Prevent hidden linker behaviour -- only explicit platfor mdependencies try args.append("-nostdlib"); // Remove unused sections to reduce binary size try args.append("--gc-sections"); // TODO make the confirugable instead of using comments // Suppress linker warnings try args.append("-w"); // Verbose linker for debugging (uncomment as needed) // try args.append("--verbose"); // try args.append("--print-map"); // try args.append("--error-limit=0"); // Determine target ABI const target_abi = config.target_abi orelse if (builtin.target.abi == .musl) TargetAbi.musl else TargetAbi.gnu; switch (target_abi) { .musl => { // Static musl linking try args.append("-static"); }, .gnu => { // Dynamic GNU linking - dynamic linker path is handled by caller // for cross-compilation. Only detect locally for native builds if (config.extra_args.len == 0) { // Native build - try to detect dynamic linker if (libc_finder.findLibc(allocs)) |libc_info| { // We need to copy the path since args holds references try args.append("-dynamic-linker"); try args.append(libc_info.dynamic_linker); } else |err| { // Fallback to hardcoded path based on architecture std.log.warn("Failed to detect libc: {}, using fallback", .{err}); try args.append("-dynamic-linker"); const fallback_ld = switch (builtin.target.cpu.arch) { .x86_64 => "/lib64/ld-linux-x86-64.so.2", .aarch64 => "/lib/ld-linux-aarch64.so.1", .x86 => "/lib/ld-linux.so.2", else => "/lib/ld-linux.so.2", }; try args.append(fallback_ld); } } // Otherwise, dynamic linker is set via extra_args from caller }, } }, .windows => { // Add linker name for Windows COFF try args.append("lld-link"); const query = std.Target.Query{ .cpu_arch = target_arch, .os_tag = .windows, .abi = .msvc, .ofmt = .coff, }; const target = try std.zig.system.resolveTargetQuery(query); const native_libc = std.zig.LibCInstallation.findNative(.{ .allocator = allocs.arena, .target = &target, }) catch return error.WindowsSDKNotFound; if (native_libc.crt_dir) |lib_dir| { const lib_arg = try std.fmt.allocPrint(allocs.arena, "/libpath:{s}", .{lib_dir}); try args.append(lib_arg); } else return error.WindowsSDKNotFound; if (native_libc.msvc_lib_dir) |lib_dir| { const lib_arg = try std.fmt.allocPrint(allocs.arena, "/libpath:{s}", .{lib_dir}); try args.append(lib_arg); } else return error.WindowsSDKNotFound; if (native_libc.kernel32_lib_dir) |lib_dir| { const lib_arg = try std.fmt.allocPrint(allocs.arena, "/libpath:{s}", .{lib_dir}); try args.append(lib_arg); } else return error.WindowsSDKNotFound; // Add output argument using Windows style const out_arg = try std.fmt.allocPrint(allocs.arena, "/out:{s}", .{config.output_path}); try args.append(out_arg); // Add subsystem flag (console by default) try args.append("/subsystem:console"); // Add machine type based on target architecture switch (target_arch) { .x86_64 => try args.append("/machine:x64"), .x86 => try args.append("/machine:x86"), .aarch64 => try args.append("/machine:arm64"), else => try args.append("/machine:x64"), // default to x64 } // These are part of the core Windows OS and are available on all Windows systems try args.append("/defaultlib:kernel32"); try args.append("/defaultlib:ntdll"); try args.append("/defaultlib:msvcrt"); // Suppress warnings using Windows style try args.append("/ignore:4217"); // Ignore locally defined symbol imported warnings try args.append("/ignore:4049"); // Ignore locally defined symbol imported warnings }, .freestanding => { // WebAssembly linker (wasm-ld) for freestanding wasm32 target try args.append("wasm-ld"); // Add output argument try args.append("-o"); try args.append(config.output_path); // Don't look for _start or _main entry point - we export specific functions try args.append("--no-entry"); // Export all symbols (the Roc app exports its entrypoints) try args.append("--export-all"); // Disable garbage collection to preserve host-defined exports (init, handleEvent, update) // Without this, wasm-ld removes symbols that aren't referenced by the Roc app try args.append("--no-gc-sections"); // Allow undefined symbols (imports from host environment) try args.append("--allow-undefined"); // Set initial memory size (configurable, default 64MB) // Must be a multiple of 64KB (WASM page size) const initial_memory_str = std.fmt.allocPrint(allocs.arena, "--initial-memory={d}", .{config.wasm_initial_memory}) catch return LinkError.OutOfMemory; try args.append(initial_memory_str); // Set stack size (configurable, default 8MB) // Must be a multiple of 16 (stack alignment) const stack_size_str = std.fmt.allocPrint(allocs.arena, "stack-size={d}", .{config.wasm_stack_size}) catch return LinkError.OutOfMemory; try args.append("-z"); try args.append(stack_size_str); }, else => { // Generic ELF linker try args.append("ld.lld"); // Add output argument try args.append("-o"); try args.append(config.output_path); // Suppress LLD warnings try args.append("-w"); }, } // Add platform-provided files that come before object files // Use --whole-archive to include all members from static libraries (e.g., libhost.a) // This ensures host-exported functions like init, handleEvent, update are included // even though they're not referenced by the Roc app's compiled code if (config.platform_files_pre.len > 0) { try args.append("--whole-archive"); for (config.platform_files_pre) |platform_file| { try args.append(platform_file); } try args.append("--no-whole-archive"); } // Add object files (Roc shim libraries - don't need --whole-archive) for (config.object_files) |obj_file| { try args.append(obj_file); } // Add platform-provided files that come after object files // Also use --whole-archive in case there are static libs here too if (config.platform_files_post.len > 0) { try args.append("--whole-archive"); for (config.platform_files_post) |platform_file| { try args.append(platform_file); } try args.append("--no-whole-archive"); } // Add any extra arguments for (config.extra_args) |extra_arg| { try args.append(extra_arg); } return args; } /// Link object files into an executable using LLD pub fn link(allocs: *Allocators, config: LinkConfig) LinkError!void { // Check if LLVM is available at compile time if (comptime !llvm_available) { return LinkError.LLVMNotAvailable; } const args = try buildLinkArgs(allocs, config); // Debug: Print the linker command std.log.debug("Linker command:", .{}); for (args.items) |arg| { std.log.debug(" {s}", .{arg}); } // Convert to null-terminated strings for C API // Arena allocator will clean up all these temporary allocations var c_args = allocs.arena.alloc([*:0]const u8, args.items.len) catch return LinkError.OutOfMemory; for (args.items, 0..) |arg, i| { c_args[i] = (allocs.arena.dupeZ(u8, arg) catch return LinkError.OutOfMemory).ptr; } // Call appropriate LLD function based on target format const success = switch (config.target_format) { .elf => llvm_externs.ZigLLDLinkELF( @intCast(c_args.len), c_args.ptr, config.can_exit_early, config.disable_output, ), .coff => llvm_externs.ZigLLDLinkCOFF( @intCast(c_args.len), c_args.ptr, config.can_exit_early, config.disable_output, ), .macho => llvm_externs.ZigLLDLinkMachO( @intCast(c_args.len), c_args.ptr, config.can_exit_early, config.disable_output, ), .wasm => llvm_externs.ZigLLDLinkWasm( @intCast(c_args.len), c_args.ptr, config.can_exit_early, config.disable_output, ), }; if (!success) { return LinkError.LinkFailed; } } /// Format link configuration as a shell command string for manual reproduction. /// Useful for debugging linking issues by allowing users to run the linker manually. pub fn formatLinkCommand(allocs: *Allocators, config: LinkConfig) LinkError![]const u8 { const args = try buildLinkArgs(allocs, config); // Join args with spaces, quoting paths that contain spaces or special chars var result = std.array_list.Managed(u8).init(allocs.arena); for (args.items, 0..) |arg, i| { if (i > 0) result.append(' ') catch return LinkError.OutOfMemory; // Quote if contains spaces or shell metacharacters const needs_quoting = std.mem.indexOfAny(u8, arg, " \t'\"\\$`") != null; if (needs_quoting) { result.append('\'') catch return LinkError.OutOfMemory; // Escape single quotes within the string for (arg) |c| { if (c == '\'') { result.appendSlice("'\\''") catch return LinkError.OutOfMemory; } else { result.append(c) catch return LinkError.OutOfMemory; } } result.append('\'') catch return LinkError.OutOfMemory; } else { result.appendSlice(arg) catch return LinkError.OutOfMemory; } } return result.toOwnedSlice() catch return LinkError.OutOfMemory; } /// Convenience function to link two object files into an executable pub fn linkTwoObjects(allocs: *Allocators, obj1: []const u8, obj2: []const u8, output: []const u8) LinkError!void { if (comptime !llvm_available) { return LinkError.LLVMNotAvailable; } const config = LinkConfig{ .output_path = output, .object_files = &.{ obj1, obj2 }, }; return link(allocs, config); } /// Convenience function to link multiple object files into an executable pub fn linkObjects(allocs: *Allocators, object_files: []const []const u8, output: []const u8) LinkError!void { if (comptime !llvm_available) { return LinkError.LLVMNotAvailable; } const config = LinkConfig{ .output_path = output, .object_files = object_files, }; return link(allocs, config); } test "link config creation" { const config = LinkConfig{ .output_path = "test_output", .object_files = &.{ "file1.o", "file2.o" }, }; try std.testing.expect(config.target_format == TargetFormat.detectFromSystem()); try std.testing.expectEqualStrings("test_output", config.output_path); try std.testing.expectEqual(@as(usize, 2), config.object_files.len); try std.testing.expectEqual(@as(usize, 0), config.platform_files_pre.len); try std.testing.expectEqual(@as(usize, 0), config.platform_files_post.len); } test "target format detection" { const detected = TargetFormat.detectFromSystem(); // Should detect a valid format switch (detected) { .elf, .coff, .macho, .wasm => {}, } } test "link error when LLVM not available" { if (comptime !llvm_available) { var allocs: Allocators = undefined; allocs.initInPlace(std.testing.allocator); defer allocs.deinit(); const config = LinkConfig{ .output_path = "test_output", .object_files = &.{ "file1.o", "file2.o" }, }; const result = link(&allocs, config); try std.testing.expectError(LinkError.LLVMNotAvailable, result); } }