This branch introduced an arena allocator for temporary allocations in Compilation.update. Almost every implementation of flush() inside the linker code was already creating a local arena that had the lifetime of the function call. This commit passes the update arena so that all those local ones can be deleted, resulting in slightly more efficient memory usage with every compilation update. While at it, this commit also removes the Compilation parameter from the linker flush function API since a reference to the Compilation is now already stored in `link.File`.
1010 lines
37 KiB
Zig
1010 lines
37 KiB
Zig
const std = @import("std");
|
|
const build_options = @import("build_options");
|
|
const builtin = @import("builtin");
|
|
const assert = std.debug.assert;
|
|
const fs = std.fs;
|
|
const mem = std.mem;
|
|
const log = std.log.scoped(.link);
|
|
const trace = @import("tracy.zig").trace;
|
|
const wasi_libc = @import("wasi_libc.zig");
|
|
|
|
const Air = @import("Air.zig");
|
|
const Allocator = std.mem.Allocator;
|
|
const Cache = std.Build.Cache;
|
|
const Compilation = @import("Compilation.zig");
|
|
const LibCInstallation = @import("libc_installation.zig").LibCInstallation;
|
|
const Liveness = @import("Liveness.zig");
|
|
const Module = @import("Module.zig");
|
|
const InternPool = @import("InternPool.zig");
|
|
const Type = @import("type.zig").Type;
|
|
const TypedValue = @import("TypedValue.zig");
|
|
const LlvmObject = @import("codegen/llvm.zig").Object;
|
|
|
|
/// When adding a new field, remember to update `hashAddSystemLibs`.
|
|
/// These are *always* dynamically linked. Static libraries will be
|
|
/// provided as positional arguments.
|
|
pub const SystemLib = struct {
|
|
needed: bool,
|
|
weak: bool,
|
|
/// This can be null in two cases right now:
|
|
/// 1. Windows DLLs that zig ships such as advapi32.
|
|
/// 2. extern "foo" fn declarations where we find out about libraries too late
|
|
/// TODO: make this non-optional and resolve those two cases somehow.
|
|
path: ?[]const u8,
|
|
};
|
|
|
|
pub fn hashAddSystemLibs(
|
|
man: *Cache.Manifest,
|
|
hm: std.StringArrayHashMapUnmanaged(SystemLib),
|
|
) !void {
|
|
const keys = hm.keys();
|
|
man.hash.addListOfBytes(keys);
|
|
for (hm.values()) |value| {
|
|
man.hash.add(value.needed);
|
|
man.hash.add(value.weak);
|
|
if (value.path) |p| _ = try man.addFile(p, null);
|
|
}
|
|
}
|
|
|
|
pub const producer_string = if (builtin.is_test) "zig test" else "zig " ++ build_options.version;
|
|
|
|
pub const File = struct {
|
|
tag: Tag,
|
|
|
|
/// The owner of this output File.
|
|
comp: *Compilation,
|
|
emit: Compilation.Emit,
|
|
|
|
file: ?fs.File,
|
|
/// When linking with LLD, this linker code will output an object file only at
|
|
/// this location, and then this path can be placed on the LLD linker line.
|
|
zcu_object_sub_path: ?[]const u8 = null,
|
|
disable_lld_caching: bool,
|
|
gc_sections: bool,
|
|
print_gc_sections: bool,
|
|
build_id: std.zig.BuildId,
|
|
rpath_list: []const []const u8,
|
|
allow_shlib_undefined: bool,
|
|
stack_size: u64,
|
|
|
|
/// Prevents other processes from clobbering files in the output directory
|
|
/// of this linking operation.
|
|
lock: ?Cache.Lock = null,
|
|
child_pid: ?std.ChildProcess.Id = null,
|
|
|
|
pub const OpenOptions = struct {
|
|
symbol_count_hint: u64 = 32,
|
|
program_code_size_hint: u64 = 256 * 1024,
|
|
|
|
/// This may depend on what symbols are found during the linking process.
|
|
entry: Entry,
|
|
/// Virtual address of the entry point procedure relative to image base.
|
|
entry_addr: ?u64,
|
|
stack_size: ?u64,
|
|
image_base: ?u64,
|
|
emit_relocs: bool,
|
|
z_nodelete: bool,
|
|
z_notext: bool,
|
|
z_defs: bool,
|
|
z_origin: bool,
|
|
z_nocopyreloc: bool,
|
|
z_now: bool,
|
|
z_relro: bool,
|
|
z_common_page_size: ?u64,
|
|
z_max_page_size: ?u64,
|
|
tsaware: bool,
|
|
nxcompat: bool,
|
|
dynamicbase: bool,
|
|
compress_debug_sections: Elf.CompressDebugSections,
|
|
bind_global_refs_locally: bool,
|
|
import_symbols: bool,
|
|
import_table: bool,
|
|
export_table: bool,
|
|
initial_memory: ?u64,
|
|
max_memory: ?u64,
|
|
export_symbol_names: []const []const u8,
|
|
global_base: ?u64,
|
|
each_lib_rpath: bool,
|
|
build_id: std.zig.BuildId,
|
|
disable_lld_caching: bool,
|
|
hash_style: Elf.HashStyle,
|
|
sort_section: ?Elf.SortSection,
|
|
major_subsystem_version: ?u16,
|
|
minor_subsystem_version: ?u16,
|
|
gc_sections: ?bool,
|
|
allow_shlib_undefined: ?bool,
|
|
subsystem: ?std.Target.SubSystem,
|
|
linker_script: ?[]const u8,
|
|
version_script: ?[]const u8,
|
|
soname: ?[]const u8,
|
|
print_gc_sections: bool,
|
|
print_icf_sections: bool,
|
|
print_map: bool,
|
|
|
|
/// Use a wrapper function for symbol. Any undefined reference to symbol
|
|
/// will be resolved to __wrap_symbol. Any undefined reference to
|
|
/// __real_symbol will be resolved to symbol. This can be used to provide a
|
|
/// wrapper for a system function. The wrapper function should be called
|
|
/// __wrap_symbol. If it wishes to call the system function, it should call
|
|
/// __real_symbol.
|
|
symbol_wrap_set: std.StringArrayHashMapUnmanaged(void),
|
|
|
|
compatibility_version: ?std.SemanticVersion,
|
|
|
|
// TODO: remove this. libraries are resolved by the frontend.
|
|
lib_dirs: []const []const u8,
|
|
rpath_list: []const []const u8,
|
|
|
|
/// (Zig compiler development) Enable dumping of linker's state as JSON.
|
|
enable_link_snapshots: bool,
|
|
|
|
/// (Darwin) Install name for the dylib
|
|
install_name: ?[]const u8,
|
|
/// (Darwin) Path to entitlements file
|
|
entitlements: ?[]const u8,
|
|
/// (Darwin) size of the __PAGEZERO segment
|
|
pagezero_size: ?u64,
|
|
/// (Darwin) set minimum space for future expansion of the load commands
|
|
headerpad_size: ?u32,
|
|
/// (Darwin) set enough space as if all paths were MATPATHLEN
|
|
headerpad_max_install_names: bool,
|
|
/// (Darwin) remove dylibs that are unreachable by the entry point or exported symbols
|
|
dead_strip_dylibs: bool,
|
|
frameworks: []const MachO.Framework,
|
|
darwin_sdk_layout: ?MachO.SdkLayout,
|
|
|
|
/// (Windows) PDB source path prefix to instruct the linker how to resolve relative
|
|
/// paths when consolidating CodeView streams into a single PDB file.
|
|
pdb_source_path: ?[]const u8,
|
|
/// (Windows) PDB output path
|
|
pdb_out_path: ?[]const u8,
|
|
/// (Windows) .def file to specify when linking
|
|
module_definition_file: ?[]const u8,
|
|
|
|
pub const Entry = union(enum) {
|
|
default,
|
|
disabled,
|
|
enabled,
|
|
named: []const u8,
|
|
};
|
|
};
|
|
|
|
/// Attempts incremental linking, if the file already exists. If
|
|
/// incremental linking fails, falls back to truncating the file and
|
|
/// rewriting it. A malicious file is detected as incremental link failure
|
|
/// and does not cause Illegal Behavior. This operation is not atomic.
|
|
/// `arena` is used for allocations with the same lifetime as the created File.
|
|
pub fn open(
|
|
arena: Allocator,
|
|
comp: *Compilation,
|
|
emit: Compilation.Emit,
|
|
options: OpenOptions,
|
|
) !*File {
|
|
const tag = Tag.fromObjectFormat(comp.root_mod.resolved_target.result.ofmt);
|
|
switch (tag) {
|
|
.c => {
|
|
const ptr = try C.open(arena, comp, emit, options);
|
|
return &ptr.base;
|
|
},
|
|
inline else => |t| {
|
|
if (build_options.only_c) unreachable;
|
|
const ptr = try t.Type().open(arena, comp, emit, options);
|
|
return &ptr.base;
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn createEmpty(
|
|
arena: Allocator,
|
|
comp: *Compilation,
|
|
emit: Compilation.Emit,
|
|
options: OpenOptions,
|
|
) !*File {
|
|
const tag = Tag.fromObjectFormat(comp.root_mod.resolved_target.result.ofmt);
|
|
switch (tag) {
|
|
.c => {
|
|
const ptr = try C.createEmpty(arena, comp, emit, options);
|
|
return &ptr.base;
|
|
},
|
|
inline else => |t| {
|
|
if (build_options.only_c) unreachable;
|
|
const ptr = try t.Type().createEmpty(arena, comp, emit, options);
|
|
return &ptr.base;
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn cast(base: *File, comptime T: type) ?*T {
|
|
if (base.tag != T.base_tag)
|
|
return null;
|
|
|
|
return @fieldParentPtr(T, "base", base);
|
|
}
|
|
|
|
pub fn makeWritable(base: *File) !void {
|
|
const comp = base.comp;
|
|
const gpa = comp.gpa;
|
|
switch (base.tag) {
|
|
.coff, .elf, .macho, .plan9, .wasm => {
|
|
if (build_options.only_c) unreachable;
|
|
if (base.file != null) return;
|
|
const emit = base.emit;
|
|
if (base.child_pid) |pid| {
|
|
if (builtin.os.tag == .windows) {
|
|
base.cast(Coff).?.ptraceAttach(pid) catch |err| {
|
|
log.warn("attaching failed with error: {s}", .{@errorName(err)});
|
|
};
|
|
} else {
|
|
// If we try to open the output file in write mode while it is running,
|
|
// it will return ETXTBSY. So instead, we copy the file, atomically rename it
|
|
// over top of the exe path, and then proceed normally. This changes the inode,
|
|
// avoiding the error.
|
|
const tmp_sub_path = try std.fmt.allocPrint(gpa, "{s}-{x}", .{
|
|
emit.sub_path, std.crypto.random.int(u32),
|
|
});
|
|
defer gpa.free(tmp_sub_path);
|
|
try emit.directory.handle.copyFile(emit.sub_path, emit.directory.handle, tmp_sub_path, .{});
|
|
try emit.directory.handle.rename(tmp_sub_path, emit.sub_path);
|
|
switch (builtin.os.tag) {
|
|
.linux => std.os.ptrace(std.os.linux.PTRACE.ATTACH, pid, 0, 0) catch |err| {
|
|
log.warn("ptrace failure: {s}", .{@errorName(err)});
|
|
},
|
|
.macos => base.cast(MachO).?.ptraceAttach(pid) catch |err| {
|
|
log.warn("attaching failed with error: {s}", .{@errorName(err)});
|
|
},
|
|
.windows => unreachable,
|
|
else => return error.HotSwapUnavailableOnHostOperatingSystem,
|
|
}
|
|
}
|
|
}
|
|
const use_lld = build_options.have_llvm and comp.config.use_lld;
|
|
const output_mode = comp.config.output_mode;
|
|
const link_mode = comp.config.link_mode;
|
|
base.file = try emit.directory.handle.createFile(emit.sub_path, .{
|
|
.truncate = false,
|
|
.read = true,
|
|
.mode = determineMode(use_lld, output_mode, link_mode),
|
|
});
|
|
},
|
|
.c, .spirv, .nvptx => {},
|
|
}
|
|
}
|
|
|
|
pub fn makeExecutable(base: *File) !void {
|
|
const comp = base.comp;
|
|
const output_mode = comp.config.output_mode;
|
|
const link_mode = comp.config.link_mode;
|
|
const use_lld = build_options.have_llvm and comp.config.use_lld;
|
|
|
|
switch (output_mode) {
|
|
.Obj => return,
|
|
.Lib => switch (link_mode) {
|
|
.Static => return,
|
|
.Dynamic => {},
|
|
},
|
|
.Exe => {},
|
|
}
|
|
switch (base.tag) {
|
|
.elf => if (base.file) |f| {
|
|
if (build_options.only_c) unreachable;
|
|
if (base.zcu_object_sub_path != null and use_lld) {
|
|
// The file we have open is not the final file that we want to
|
|
// make executable, so we don't have to close it.
|
|
return;
|
|
}
|
|
f.close();
|
|
base.file = null;
|
|
|
|
if (base.child_pid) |pid| {
|
|
switch (builtin.os.tag) {
|
|
.linux => std.os.ptrace(std.os.linux.PTRACE.DETACH, pid, 0, 0) catch |err| {
|
|
log.warn("ptrace failure: {s}", .{@errorName(err)});
|
|
},
|
|
else => return error.HotSwapUnavailableOnHostOperatingSystem,
|
|
}
|
|
}
|
|
},
|
|
.coff, .macho, .plan9, .wasm => if (base.file) |f| {
|
|
if (build_options.only_c) unreachable;
|
|
if (base.zcu_object_sub_path != null) {
|
|
// The file we have open is not the final file that we want to
|
|
// make executable, so we don't have to close it.
|
|
return;
|
|
}
|
|
f.close();
|
|
base.file = null;
|
|
|
|
if (base.child_pid) |pid| {
|
|
switch (builtin.os.tag) {
|
|
.macos => base.cast(MachO).?.ptraceDetach(pid) catch |err| {
|
|
log.warn("detaching failed with error: {s}", .{@errorName(err)});
|
|
},
|
|
.windows => base.cast(Coff).?.ptraceDetach(pid),
|
|
else => return error.HotSwapUnavailableOnHostOperatingSystem,
|
|
}
|
|
}
|
|
},
|
|
.c, .spirv, .nvptx => {},
|
|
}
|
|
}
|
|
|
|
pub const UpdateDeclError = error{
|
|
OutOfMemory,
|
|
Overflow,
|
|
Underflow,
|
|
FileTooBig,
|
|
InputOutput,
|
|
FilesOpenedWithWrongFlags,
|
|
IsDir,
|
|
NoSpaceLeft,
|
|
Unseekable,
|
|
PermissionDenied,
|
|
SwapFile,
|
|
CorruptedData,
|
|
SystemResources,
|
|
OperationAborted,
|
|
BrokenPipe,
|
|
ConnectionResetByPeer,
|
|
ConnectionTimedOut,
|
|
SocketNotConnected,
|
|
NotOpenForReading,
|
|
WouldBlock,
|
|
AccessDenied,
|
|
Unexpected,
|
|
DiskQuota,
|
|
NotOpenForWriting,
|
|
AnalysisFail,
|
|
CodegenFail,
|
|
EmitFail,
|
|
NameTooLong,
|
|
CurrentWorkingDirectoryUnlinked,
|
|
LockViolation,
|
|
NetNameDeleted,
|
|
DeviceBusy,
|
|
InvalidArgument,
|
|
HotSwapUnavailableOnHostOperatingSystem,
|
|
};
|
|
|
|
/// Called from within the CodeGen to lower a local variable instantion as an unnamed
|
|
/// constant. Returns the symbol index of the lowered constant in the read-only section
|
|
/// of the final binary.
|
|
pub fn lowerUnnamedConst(base: *File, tv: TypedValue, decl_index: InternPool.DeclIndex) UpdateDeclError!u32 {
|
|
if (build_options.only_c) @compileError("unreachable");
|
|
switch (base.tag) {
|
|
.spirv => unreachable,
|
|
.c => unreachable,
|
|
.nvptx => unreachable,
|
|
inline else => |t| {
|
|
return @fieldParentPtr(t.Type(), "base", base).lowerUnnamedConst(tv, decl_index);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Called from within CodeGen to retrieve the symbol index of a global symbol.
|
|
/// If no symbol exists yet with this name, a new undefined global symbol will
|
|
/// be created. This symbol may get resolved once all relocatables are (re-)linked.
|
|
/// Optionally, it is possible to specify where to expect the symbol defined if it
|
|
/// is an import.
|
|
pub fn getGlobalSymbol(base: *File, name: []const u8, lib_name: ?[]const u8) UpdateDeclError!u32 {
|
|
if (build_options.only_c) @compileError("unreachable");
|
|
log.debug("getGlobalSymbol '{s}' (expected in '{?s}')", .{ name, lib_name });
|
|
switch (base.tag) {
|
|
.plan9 => unreachable,
|
|
.spirv => unreachable,
|
|
.c => unreachable,
|
|
.nvptx => unreachable,
|
|
inline else => |t| {
|
|
return @fieldParentPtr(t.Type(), "base", base).getGlobalSymbol(name, lib_name);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// May be called before or after updateExports for any given Decl.
|
|
pub fn updateDecl(base: *File, module: *Module, decl_index: InternPool.DeclIndex) UpdateDeclError!void {
|
|
const decl = module.declPtr(decl_index);
|
|
assert(decl.has_tv);
|
|
switch (base.tag) {
|
|
.c => {
|
|
return @fieldParentPtr(C, "base", base).updateDecl(module, decl_index);
|
|
},
|
|
inline else => |tag| {
|
|
if (build_options.only_c) unreachable;
|
|
return @fieldParentPtr(tag.Type(), "base", base).updateDecl(module, decl_index);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// May be called before or after updateExports for any given Decl.
|
|
pub fn updateFunc(
|
|
base: *File,
|
|
module: *Module,
|
|
func_index: InternPool.Index,
|
|
air: Air,
|
|
liveness: Liveness,
|
|
) UpdateDeclError!void {
|
|
switch (base.tag) {
|
|
.c => {
|
|
return @fieldParentPtr(C, "base", base).updateFunc(module, func_index, air, liveness);
|
|
},
|
|
inline else => |tag| {
|
|
if (build_options.only_c) unreachable;
|
|
return @fieldParentPtr(tag.Type(), "base", base).updateFunc(module, func_index, air, liveness);
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn updateDeclLineNumber(base: *File, module: *Module, decl_index: InternPool.DeclIndex) UpdateDeclError!void {
|
|
const decl = module.declPtr(decl_index);
|
|
assert(decl.has_tv);
|
|
switch (base.tag) {
|
|
.spirv, .nvptx => {},
|
|
.c => {
|
|
return @fieldParentPtr(C, "base", base).updateDeclLineNumber(module, decl_index);
|
|
},
|
|
inline else => |tag| {
|
|
if (build_options.only_c) unreachable;
|
|
return @fieldParentPtr(tag.Type(), "base", base).updateDeclLineNumber(module, decl_index);
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn releaseLock(self: *File) void {
|
|
if (self.lock) |*lock| {
|
|
lock.release();
|
|
self.lock = null;
|
|
}
|
|
}
|
|
|
|
pub fn toOwnedLock(self: *File) Cache.Lock {
|
|
const lock = self.lock.?;
|
|
self.lock = null;
|
|
return lock;
|
|
}
|
|
|
|
pub fn destroy(base: *File) void {
|
|
base.releaseLock();
|
|
if (base.file) |f| f.close();
|
|
switch (base.tag) {
|
|
.c => @fieldParentPtr(C, "base", base).deinit(),
|
|
|
|
inline else => |tag| {
|
|
if (build_options.only_c) unreachable;
|
|
@fieldParentPtr(tag.Type(), "base", base).deinit();
|
|
},
|
|
}
|
|
}
|
|
|
|
/// TODO audit this error set. most of these should be collapsed into one error,
|
|
/// and ErrorFlags should be updated to convey the meaning to the user.
|
|
pub const FlushError = error{
|
|
CacheUnavailable,
|
|
CurrentWorkingDirectoryUnlinked,
|
|
DivisionByZero,
|
|
DllImportLibraryNotFound,
|
|
ExpectedFuncType,
|
|
FailedToEmit,
|
|
FileSystem,
|
|
FilesOpenedWithWrongFlags,
|
|
FlushFailure,
|
|
FunctionSignatureMismatch,
|
|
GlobalTypeMismatch,
|
|
HotSwapUnavailableOnHostOperatingSystem,
|
|
InvalidCharacter,
|
|
InvalidEntryKind,
|
|
InvalidFeatureSet,
|
|
InvalidFormat,
|
|
InvalidIndex,
|
|
InvalidInitFunc,
|
|
InvalidMagicByte,
|
|
InvalidWasmVersion,
|
|
LLDCrashed,
|
|
LLDReportedFailure,
|
|
LLD_LinkingIsTODO_ForSpirV,
|
|
LibCInstallationMissingCRTDir,
|
|
LibCInstallationNotAvailable,
|
|
LinkingWithoutZigSourceUnimplemented,
|
|
MalformedArchive,
|
|
MalformedDwarf,
|
|
MalformedSection,
|
|
MemoryTooBig,
|
|
MemoryTooSmall,
|
|
MissAlignment,
|
|
MissingEndForBody,
|
|
MissingEndForExpression,
|
|
MissingSymbol,
|
|
MissingTableSymbols,
|
|
ModuleNameMismatch,
|
|
NoObjectsToLink,
|
|
NotObjectFile,
|
|
NotSupported,
|
|
OutOfMemory,
|
|
Overflow,
|
|
PermissionDenied,
|
|
StreamTooLong,
|
|
SwapFile,
|
|
SymbolCollision,
|
|
SymbolMismatchingType,
|
|
TODOImplementPlan9Objs,
|
|
TODOImplementWritingLibFiles,
|
|
UnableToSpawnSelf,
|
|
UnableToSpawnWasm,
|
|
UnableToWriteArchive,
|
|
UndefinedLocal,
|
|
UndefinedSymbol,
|
|
Underflow,
|
|
UnexpectedRemainder,
|
|
UnexpectedTable,
|
|
UnexpectedValue,
|
|
UnknownFeature,
|
|
Unseekable,
|
|
UnsupportedCpuArchitecture,
|
|
UnsupportedVersion,
|
|
} ||
|
|
fs.File.WriteFileError ||
|
|
fs.File.OpenError ||
|
|
std.ChildProcess.SpawnError ||
|
|
fs.Dir.CopyFileError;
|
|
|
|
/// Commit pending changes and write headers. Takes into account final output mode
|
|
/// and `use_lld`, not only `effectiveOutputMode`.
|
|
/// `arena` has the lifetime of the call to `Compilation.update`.
|
|
pub fn flush(base: *File, arena: Allocator, prog_node: *std.Progress.Node) FlushError!void {
|
|
if (build_options.only_c) {
|
|
assert(base.tag == .c);
|
|
return @fieldParentPtr(C, "base", base).flush(arena, prog_node);
|
|
}
|
|
const comp = base.comp;
|
|
if (comp.clang_preprocessor_mode == .yes) {
|
|
const gpa = comp.gpa;
|
|
const emit = base.emit;
|
|
// TODO: avoid extra link step when it's just 1 object file (the `zig cc -c` case)
|
|
// Until then, we do `lld -r -o output.o input.o` even though the output is the same
|
|
// as the input. For the preprocessing case (`zig cc -E -o foo`) we copy the file
|
|
// to the final location. See also the corresponding TODO in Coff linking.
|
|
const full_out_path = try emit.directory.join(gpa, &[_][]const u8{emit.sub_path});
|
|
defer gpa.free(full_out_path);
|
|
assert(comp.c_object_table.count() == 1);
|
|
const the_key = comp.c_object_table.keys()[0];
|
|
const cached_pp_file_path = the_key.status.success.object_path;
|
|
try fs.cwd().copyFile(cached_pp_file_path, fs.cwd(), full_out_path, .{});
|
|
return;
|
|
}
|
|
|
|
const use_lld = build_options.have_llvm and comp.config.use_lld;
|
|
const output_mode = comp.config.output_mode;
|
|
const link_mode = comp.config.link_mode;
|
|
if (use_lld and output_mode == .Lib and link_mode == .Static) {
|
|
return base.linkAsArchive(arena, prog_node);
|
|
}
|
|
switch (base.tag) {
|
|
inline else => |tag| {
|
|
return @fieldParentPtr(tag.Type(), "base", base).flush(arena, prog_node);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Commit pending changes and write headers. Works based on `effectiveOutputMode`
|
|
/// rather than final output mode.
|
|
pub fn flushModule(base: *File, arena: Allocator, prog_node: *std.Progress.Node) FlushError!void {
|
|
switch (base.tag) {
|
|
.c => {
|
|
return @fieldParentPtr(C, "base", base).flushModule(arena, prog_node);
|
|
},
|
|
inline else => |tag| {
|
|
if (build_options.only_c) unreachable;
|
|
return @fieldParentPtr(tag.Type(), "base", base).flushModule(arena, prog_node);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Called when a Decl is deleted from the Module.
|
|
pub fn freeDecl(base: *File, decl_index: InternPool.DeclIndex) void {
|
|
switch (base.tag) {
|
|
.c => {
|
|
@fieldParentPtr(C, "base", base).freeDecl(decl_index);
|
|
},
|
|
inline else => |tag| {
|
|
if (build_options.only_c) unreachable;
|
|
@fieldParentPtr(tag.Type(), "base", base).freeDecl(decl_index);
|
|
},
|
|
}
|
|
}
|
|
|
|
pub const UpdateExportsError = error{
|
|
OutOfMemory,
|
|
AnalysisFail,
|
|
};
|
|
|
|
/// This is called for every exported thing. `exports` is almost always
|
|
/// a list of size 1, meaning that `exported` is exported once. However, it is possible
|
|
/// to export the same thing with multiple different symbol names (aliases).
|
|
/// May be called before or after updateDecl for any given Decl.
|
|
pub fn updateExports(
|
|
base: *File,
|
|
module: *Module,
|
|
exported: Module.Exported,
|
|
exports: []const *Module.Export,
|
|
) UpdateExportsError!void {
|
|
switch (base.tag) {
|
|
.c => {
|
|
return @fieldParentPtr(C, "base", base).updateExports(module, exported, exports);
|
|
},
|
|
inline else => |tag| {
|
|
if (build_options.only_c) unreachable;
|
|
return @fieldParentPtr(tag.Type(), "base", base).updateExports(module, exported, exports);
|
|
},
|
|
}
|
|
}
|
|
|
|
pub const RelocInfo = struct {
|
|
parent_atom_index: u32,
|
|
offset: u64,
|
|
addend: u32,
|
|
};
|
|
|
|
/// Get allocated `Decl`'s address in virtual memory.
|
|
/// The linker is passed information about the containing atom, `parent_atom_index`, and offset within it's
|
|
/// memory buffer, `offset`, so that it can make a note of potential relocation sites, should the
|
|
/// `Decl`'s address was not yet resolved, or the containing atom gets moved in virtual memory.
|
|
/// May be called before or after updateFunc/updateDecl therefore it is up to the linker to allocate
|
|
/// the block/atom.
|
|
pub fn getDeclVAddr(base: *File, decl_index: InternPool.DeclIndex, reloc_info: RelocInfo) !u64 {
|
|
if (build_options.only_c) @compileError("unreachable");
|
|
switch (base.tag) {
|
|
.c => unreachable,
|
|
.spirv => unreachable,
|
|
.nvptx => unreachable,
|
|
inline else => |tag| {
|
|
return @fieldParentPtr(tag.Type(), "base", base).getDeclVAddr(decl_index, reloc_info);
|
|
},
|
|
}
|
|
}
|
|
|
|
pub const LowerResult = @import("codegen.zig").Result;
|
|
|
|
pub fn lowerAnonDecl(
|
|
base: *File,
|
|
decl_val: InternPool.Index,
|
|
decl_align: InternPool.Alignment,
|
|
src_loc: Module.SrcLoc,
|
|
) !LowerResult {
|
|
if (build_options.only_c) @compileError("unreachable");
|
|
switch (base.tag) {
|
|
.c => unreachable,
|
|
.spirv => unreachable,
|
|
.nvptx => unreachable,
|
|
inline else => |tag| {
|
|
return @fieldParentPtr(tag.Type(), "base", base).lowerAnonDecl(decl_val, decl_align, src_loc);
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn getAnonDeclVAddr(base: *File, decl_val: InternPool.Index, reloc_info: RelocInfo) !u64 {
|
|
if (build_options.only_c) @compileError("unreachable");
|
|
switch (base.tag) {
|
|
.c => unreachable,
|
|
.spirv => unreachable,
|
|
.nvptx => unreachable,
|
|
inline else => |tag| {
|
|
return @fieldParentPtr(tag.Type(), "base", base).getAnonDeclVAddr(decl_val, reloc_info);
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn deleteDeclExport(
|
|
base: *File,
|
|
decl_index: InternPool.DeclIndex,
|
|
name: InternPool.NullTerminatedString,
|
|
) !void {
|
|
if (build_options.only_c) @compileError("unreachable");
|
|
switch (base.tag) {
|
|
.plan9,
|
|
.c,
|
|
.spirv,
|
|
.nvptx,
|
|
=> {},
|
|
|
|
inline else => |tag| {
|
|
return @fieldParentPtr(tag.Type(), "base", base).deleteDeclExport(decl_index, name);
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn linkAsArchive(base: *File, arena: Allocator, prog_node: *std.Progress.Node) FlushError!void {
|
|
const tracy = trace(@src());
|
|
defer tracy.end();
|
|
|
|
const comp = base.comp;
|
|
const gpa = comp.gpa;
|
|
|
|
const directory = base.emit.directory; // Just an alias to make it shorter to type.
|
|
const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path});
|
|
const full_out_path_z = try arena.dupeZ(u8, full_out_path);
|
|
const opt_zcu = comp.module;
|
|
|
|
// If there is no Zig code to compile, then we should skip flushing the output file
|
|
// because it will not be part of the linker line anyway.
|
|
const zcu_obj_path: ?[]const u8 = if (opt_zcu != null) blk: {
|
|
try base.flushModule(arena, prog_node);
|
|
|
|
const dirname = fs.path.dirname(full_out_path_z) orelse ".";
|
|
break :blk try fs.path.join(arena, &.{ dirname, base.zcu_object_sub_path.? });
|
|
} else null;
|
|
|
|
log.debug("zcu_obj_path={s}", .{if (zcu_obj_path) |s| s else "(null)"});
|
|
|
|
const compiler_rt_path: ?[]const u8 = if (comp.include_compiler_rt)
|
|
comp.compiler_rt_obj.?.full_object_path
|
|
else
|
|
null;
|
|
|
|
// This function follows the same pattern as link.Elf.linkWithLLD so if you want some
|
|
// insight as to what's going on here you can read that function body which is more
|
|
// well-commented.
|
|
|
|
const id_symlink_basename = "llvm-ar.id";
|
|
|
|
var man: Cache.Manifest = undefined;
|
|
defer if (!base.disable_lld_caching) man.deinit();
|
|
|
|
const objects = comp.objects;
|
|
|
|
var digest: [Cache.hex_digest_len]u8 = undefined;
|
|
|
|
if (!base.disable_lld_caching) {
|
|
man = comp.cache_parent.obtain();
|
|
|
|
// We are about to obtain this lock, so here we give other processes a chance first.
|
|
base.releaseLock();
|
|
|
|
for (objects) |obj| {
|
|
_ = try man.addFile(obj.path, null);
|
|
man.hash.add(obj.must_link);
|
|
man.hash.add(obj.loption);
|
|
}
|
|
for (comp.c_object_table.keys()) |key| {
|
|
_ = try man.addFile(key.status.success.object_path, null);
|
|
}
|
|
if (!build_options.only_core_functionality) {
|
|
for (comp.win32_resource_table.keys()) |key| {
|
|
_ = try man.addFile(key.status.success.res_path, null);
|
|
}
|
|
}
|
|
try man.addOptionalFile(zcu_obj_path);
|
|
try man.addOptionalFile(compiler_rt_path);
|
|
|
|
// We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
|
|
_ = try man.hit();
|
|
digest = man.final();
|
|
|
|
var prev_digest_buf: [digest.len]u8 = undefined;
|
|
const prev_digest: []u8 = Cache.readSmallFile(
|
|
directory.handle,
|
|
id_symlink_basename,
|
|
&prev_digest_buf,
|
|
) catch |err| b: {
|
|
log.debug("archive new_digest={s} readFile error: {s}", .{ std.fmt.fmtSliceHexLower(&digest), @errorName(err) });
|
|
break :b prev_digest_buf[0..0];
|
|
};
|
|
if (mem.eql(u8, prev_digest, &digest)) {
|
|
log.debug("archive digest={s} match - skipping invocation", .{std.fmt.fmtSliceHexLower(&digest)});
|
|
base.lock = man.toOwnedLock();
|
|
return;
|
|
}
|
|
|
|
// We are about to change the output file to be different, so we invalidate the build hash now.
|
|
directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
|
|
error.FileNotFound => {},
|
|
else => |e| return e,
|
|
};
|
|
}
|
|
|
|
const win32_resource_table_len = if (build_options.only_core_functionality) 0 else comp.win32_resource_table.count();
|
|
const num_object_files = objects.len + comp.c_object_table.count() + win32_resource_table_len + 2;
|
|
var object_files = try std.ArrayList([*:0]const u8).initCapacity(gpa, num_object_files);
|
|
defer object_files.deinit();
|
|
|
|
for (objects) |obj| {
|
|
object_files.appendAssumeCapacity(try arena.dupeZ(u8, obj.path));
|
|
}
|
|
for (comp.c_object_table.keys()) |key| {
|
|
object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.object_path));
|
|
}
|
|
if (!build_options.only_core_functionality) {
|
|
for (comp.win32_resource_table.keys()) |key| {
|
|
object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.res_path));
|
|
}
|
|
}
|
|
if (zcu_obj_path) |p| {
|
|
object_files.appendAssumeCapacity(try arena.dupeZ(u8, p));
|
|
}
|
|
if (compiler_rt_path) |p| {
|
|
object_files.appendAssumeCapacity(try arena.dupeZ(u8, p));
|
|
}
|
|
|
|
if (comp.verbose_link) {
|
|
std.debug.print("ar rcs {s}", .{full_out_path_z});
|
|
for (object_files.items) |arg| {
|
|
std.debug.print(" {s}", .{arg});
|
|
}
|
|
std.debug.print("\n", .{});
|
|
}
|
|
|
|
const llvm_bindings = @import("codegen/llvm/bindings.zig");
|
|
const Builder = @import("codegen/llvm/Builder.zig");
|
|
const llvm = @import("codegen/llvm.zig");
|
|
const target = comp.root_mod.resolved_target.result;
|
|
Builder.initializeLLVMTarget(target.cpu.arch);
|
|
const os_tag = llvm.targetOs(target.os.tag);
|
|
const bad = llvm_bindings.WriteArchive(full_out_path_z, object_files.items.ptr, object_files.items.len, os_tag);
|
|
if (bad) return error.UnableToWriteArchive;
|
|
|
|
if (!base.disable_lld_caching) {
|
|
Cache.writeSmallFile(directory.handle, id_symlink_basename, &digest) catch |err| {
|
|
log.warn("failed to save archive hash digest file: {s}", .{@errorName(err)});
|
|
};
|
|
|
|
if (man.have_exclusive_lock) {
|
|
man.writeManifest() catch |err| {
|
|
log.warn("failed to write cache manifest when archiving: {s}", .{@errorName(err)});
|
|
};
|
|
}
|
|
|
|
base.lock = man.toOwnedLock();
|
|
}
|
|
}
|
|
|
|
pub const Tag = enum {
|
|
coff,
|
|
elf,
|
|
macho,
|
|
c,
|
|
wasm,
|
|
spirv,
|
|
plan9,
|
|
nvptx,
|
|
|
|
pub fn Type(comptime tag: Tag) type {
|
|
return switch (tag) {
|
|
.coff => Coff,
|
|
.elf => Elf,
|
|
.macho => MachO,
|
|
.c => C,
|
|
.wasm => Wasm,
|
|
.spirv => SpirV,
|
|
.plan9 => Plan9,
|
|
.nvptx => NvPtx,
|
|
};
|
|
}
|
|
|
|
pub fn fromObjectFormat(ofmt: std.Target.ObjectFormat) Tag {
|
|
return switch (ofmt) {
|
|
.coff => .coff,
|
|
.elf => .elf,
|
|
.macho => .macho,
|
|
.wasm => .wasm,
|
|
.plan9 => .plan9,
|
|
.c => .c,
|
|
.spirv => .spirv,
|
|
.nvptx => .nvptx,
|
|
.hex => @panic("TODO implement hex object format"),
|
|
.raw => @panic("TODO implement raw object format"),
|
|
.dxcontainer => @panic("TODO implement dxcontainer object format"),
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const ErrorFlags = struct {
|
|
no_entry_point_found: bool = false,
|
|
missing_libc: bool = false,
|
|
};
|
|
|
|
pub const ErrorMsg = struct {
|
|
msg: []const u8,
|
|
notes: []ErrorMsg = &.{},
|
|
|
|
pub fn deinit(self: *ErrorMsg, gpa: Allocator) void {
|
|
for (self.notes) |*note| {
|
|
note.deinit(gpa);
|
|
}
|
|
gpa.free(self.notes);
|
|
gpa.free(self.msg);
|
|
}
|
|
};
|
|
|
|
pub const LazySymbol = struct {
|
|
pub const Kind = enum { code, const_data };
|
|
|
|
kind: Kind,
|
|
ty: Type,
|
|
|
|
pub fn initDecl(kind: Kind, decl: ?InternPool.DeclIndex, mod: *Module) LazySymbol {
|
|
return .{ .kind = kind, .ty = if (decl) |decl_index|
|
|
mod.declPtr(decl_index).val.toType()
|
|
else
|
|
Type.anyerror };
|
|
}
|
|
|
|
pub fn getDecl(self: LazySymbol, mod: *Module) InternPool.OptionalDeclIndex {
|
|
return InternPool.OptionalDeclIndex.init(self.ty.getOwnerDeclOrNull(mod));
|
|
}
|
|
};
|
|
|
|
pub fn effectiveOutputMode(
|
|
use_lld: bool,
|
|
output_mode: std.builtin.OutputMode,
|
|
) std.builtin.OutputMode {
|
|
return if (use_lld) .Obj else output_mode;
|
|
}
|
|
|
|
pub fn determineMode(
|
|
use_lld: bool,
|
|
output_mode: std.builtin.OutputMode,
|
|
link_mode: std.builtin.LinkMode,
|
|
) fs.File.Mode {
|
|
// On common systems with a 0o022 umask, 0o777 will still result in a file created
|
|
// with 0o755 permissions, but it works appropriately if the system is configured
|
|
// more leniently. As another data point, C's fopen seems to open files with the
|
|
// 666 mode.
|
|
const executable_mode = if (builtin.target.os.tag == .windows) 0 else 0o777;
|
|
switch (effectiveOutputMode(use_lld, output_mode)) {
|
|
.Lib => return switch (link_mode) {
|
|
.Dynamic => executable_mode,
|
|
.Static => fs.File.default_mode,
|
|
},
|
|
.Exe => return executable_mode,
|
|
.Obj => return fs.File.default_mode,
|
|
}
|
|
}
|
|
|
|
pub fn isStatic(self: File) bool {
|
|
return self.comp.config.link_mode == .Static;
|
|
}
|
|
|
|
pub fn isObject(self: File) bool {
|
|
const output_mode = self.comp.config.output_mode;
|
|
return output_mode == .Obj;
|
|
}
|
|
|
|
pub fn isExe(self: File) bool {
|
|
const output_mode = self.comp.config.output_mode;
|
|
return output_mode == .Exe;
|
|
}
|
|
|
|
pub fn isStaticLib(self: File) bool {
|
|
const output_mode = self.comp.config.output_mode;
|
|
return output_mode == .Lib and self.isStatic();
|
|
}
|
|
|
|
pub fn isRelocatable(self: File) bool {
|
|
return self.isObject() or self.isStaticLib();
|
|
}
|
|
|
|
pub fn isDynLib(self: File) bool {
|
|
const output_mode = self.comp.config.output_mode;
|
|
return output_mode == .Lib and !self.isStatic();
|
|
}
|
|
|
|
pub fn emitLlvmObject(
|
|
base: File,
|
|
arena: Allocator,
|
|
llvm_object: *LlvmObject,
|
|
prog_node: *std.Progress.Node,
|
|
) !void {
|
|
return base.comp.emitLlvmObject(arena, base.emit, .{
|
|
.directory = null,
|
|
.basename = base.zcu_object_sub_path.?,
|
|
}, llvm_object, prog_node);
|
|
}
|
|
|
|
pub const C = @import("link/C.zig");
|
|
pub const Coff = @import("link/Coff.zig");
|
|
pub const Plan9 = @import("link/Plan9.zig");
|
|
pub const Elf = @import("link/Elf.zig");
|
|
pub const MachO = @import("link/MachO.zig");
|
|
pub const SpirV = @import("link/SpirV.zig");
|
|
pub const Wasm = @import("link/Wasm.zig");
|
|
pub const NvPtx = @import("link/NvPtx.zig");
|
|
pub const Dwarf = @import("link/Dwarf.zig");
|
|
};
|