mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-23 08:48:03 +00:00
511 lines
20 KiB
Zig
511 lines
20 KiB
Zig
//! 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);
|
|
}
|
|
}
|