const std = @import("std"); const def = @import("def.zig"); const Allocator = std.mem.Allocator; // LLVM has some quirks/bugs around padding/size values. // Emulating those quirks made it much easier to test this implementation against the LLVM // implementation since we could just check if the .lib files are byte-for-byte identical. // This remains set to true out of an abundance of caution. const llvm_compat = true; pub const WriteCoffArchiveError = error{TooManyMembers} || std.Io.Writer.Error || std.mem.Allocator.Error; pub fn writeCoffArchive( allocator: std.mem.Allocator, writer: *std.Io.Writer, members: Members, ) WriteCoffArchiveError!void { // The second linker member of a COFF archive uses a 32-bit integer for the number of members field, // but only 16-bit integers for the "array of 1-based indexes that map symbol names to archive // member offsets." This means that the maximum number of *indexable* members is maxInt(u16) - 1. if (members.list.items.len > std.math.maxInt(u16) - 1) return error.TooManyMembers; try writer.writeAll(archive_start); const member_offsets = try allocator.alloc(usize, members.list.items.len); defer allocator.free(member_offsets); { var offset: usize = 0; for (member_offsets, 0..) |*elem, i| { elem.* = offset; offset += archive_header_len; offset += members.list.items[i].byteLenWithPadding(); } } var long_names: StringTable = .{}; defer long_names.deinit(allocator); var symbol_to_member_index = std.StringArrayHashMap(usize).init(allocator); defer symbol_to_member_index.deinit(); var string_table_len: usize = 0; var num_symbols: usize = 0; for (members.list.items, 0..) |member, i| { for (member.symbol_names_for_import_lib) |symbol_name| { const gop_result = try symbol_to_member_index.getOrPut(symbol_name); // When building the symbol map, ignore duplicate symbol names. // This can happen in cases like (using .def file syntax): // _foo // foo == _foo if (gop_result.found_existing) continue; gop_result.value_ptr.* = i; string_table_len += symbol_name.len + 1; num_symbols += 1; } if (member.needsLongName()) { _ = try long_names.put(allocator, member.name); } } const first_linker_member_len = 4 + (4 * num_symbols) + string_table_len; const second_linker_member_len = 4 + (4 * members.list.items.len) + 4 + (2 * num_symbols) + string_table_len; const long_names_len_including_header_and_padding = blk: { if (long_names.map.count() == 0) break :blk 0; break :blk archive_header_len + std.mem.alignForward(usize, long_names.data.items.len, 2); }; const first_member_offset = archive_start.len + archive_header_len + std.mem.alignForward(usize, first_linker_member_len, 2) + archive_header_len + std.mem.alignForward(usize, second_linker_member_len, 2) + long_names_len_including_header_and_padding; // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#first-linker-member try writeArchiveMemberHeader(writer, .linker_member, memberHeaderLen(first_linker_member_len), "0"); try writer.writeInt(u32, @intCast(num_symbols), .big); for (symbol_to_member_index.values()) |member_i| { const offset = member_offsets[member_i]; try writer.writeInt(u32, @intCast(first_member_offset + offset), .big); } for (symbol_to_member_index.keys()) |symbol_name| { try writer.writeAll(symbol_name); try writer.writeByte(0); } if (first_linker_member_len % 2 != 0) try writer.writeByte(if (llvm_compat) 0 else archive_pad_byte); // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#second-linker-member try writeArchiveMemberHeader(writer, .linker_member, memberHeaderLen(second_linker_member_len), "0"); try writer.writeInt(u32, @intCast(members.list.items.len), .little); for (member_offsets) |offset| { try writer.writeInt(u32, @intCast(first_member_offset + offset), .little); } try writer.writeInt(u32, @intCast(num_symbols), .little); // sort lexicographically const C = struct { keys: []const []const u8, pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { return std.mem.lessThan(u8, ctx.keys[a_index], ctx.keys[b_index]); } }; symbol_to_member_index.sortUnstable(C{ .keys = symbol_to_member_index.keys() }); for (symbol_to_member_index.values()) |member_i| { try writer.writeInt(u16, @intCast(member_i + 1), .little); } for (symbol_to_member_index.keys()) |symbol_name| { try writer.writeAll(symbol_name); try writer.writeByte(0); } if (first_linker_member_len % 2 != 0) try writer.writeByte(if (llvm_compat) 0 else archive_pad_byte); // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#longnames-member if (long_names.data.items.len != 0) { const written_len = long_names.data.items.len; try writeLongNamesMemberHeader(writer, memberHeaderLen(written_len)); try writer.writeAll(long_names.data.items); if (long_names.data.items.len % 2 != 0) try writer.writeByte(archive_pad_byte); } for (members.list.items) |member| { const name: MemberName = if (member.needsLongName()) .{ .longname = long_names.getOffset(member.name).? } else .{ .name = member.name }; try writeArchiveMemberHeader(writer, name, member.bytes.len, "644"); try writer.writeAll(member.bytes); if (member.bytes.len % 2 != 0) try writer.writeByte(archive_pad_byte); } try writer.flush(); } const archive_start = "!\n"; const archive_header_end = "`\n"; const archive_pad_byte = '\n'; const archive_header_len = 60; fn memberHeaderLen(len: usize) usize { return if (llvm_compat) // LLVM writes this with the padding byte included, likely a bug/mistake std.mem.alignForward(usize, len, 2) else len; } const MemberName = union(enum) { name: []const u8, linker_member, longnames_member, longname: usize, pub fn write(self: MemberName, writer: *std.Io.Writer) !void { switch (self) { .name => |name| { try writer.writeAll(name); try writer.writeByte('/'); try writer.splatByteAll(' ', 16 - (name.len + 1)); }, .linker_member => { try writer.writeAll("/ "); }, .longnames_member => { try writer.writeAll("// "); }, .longname => |offset| { try writer.print("/{d: <15}", .{offset}); }, } } }; fn writeLongNamesMemberHeader(writer: *std.Io.Writer, size: usize) !void { try (MemberName{ .longnames_member = {} }).write(writer); try writer.splatByteAll(' ', archive_header_len - 16 - 10 - archive_header_end.len); try writer.print("{d: <10}", .{size}); try writer.writeAll(archive_header_end); } fn writeArchiveMemberHeader(writer: *std.Io.Writer, name: MemberName, size: usize, mode: []const u8) !void { try name.write(writer); try writer.writeAll("0 "); // date try writer.writeAll("0 "); // user id try writer.writeAll("0 "); // group id try writer.print("{s: <8}", .{mode}); // mode try writer.print("{d: <10}", .{size}); try writer.writeAll(archive_header_end); } pub const Members = struct { list: std.ArrayList(Member) = .empty, arena: std.heap.ArenaAllocator, pub const Member = struct { bytes: []const u8, name: []const u8, symbol_names_for_import_lib: []const []const u8, pub fn byteLenWithPadding(self: Member) usize { return std.mem.alignForward(usize, self.bytes.len, 2); } pub fn needsLongName(self: Member) bool { return self.name.len >= 16; } }; pub fn deinit(self: *const Members) void { self.arena.deinit(); } }; const GetMembersError = GetImportDescriptorError || GetShortImportError; pub fn getMembers( allocator: std.mem.Allocator, module_def: def.ModuleDefinition, machine_type: std.coff.IMAGE.FILE.MACHINE, ) GetMembersError!Members { var members: Members = .{ .arena = std.heap.ArenaAllocator.init(allocator), }; const arena = members.arena.allocator(); errdefer members.deinit(); try members.list.ensureTotalCapacity(arena, 3 + module_def.exports.items.len); const module_import_name = try arena.dupe(u8, module_def.name orelse ""); const library = std.fs.path.stem(module_import_name); const import_descriptor_symbol_name = try std.mem.concat(arena, u8, &.{ import_descriptor_prefix, library, }); const null_thunk_symbol_name = try std.mem.concat(arena, u8, &.{ null_thunk_data_prefix, library, null_thunk_data_suffix, }); members.list.appendAssumeCapacity(try getImportDescriptor(arena, machine_type, module_import_name, import_descriptor_symbol_name, null_thunk_symbol_name)); members.list.appendAssumeCapacity(try getNullImportDescriptor(arena, machine_type, module_import_name)); members.list.appendAssumeCapacity(try getNullThunk(arena, machine_type, module_import_name, null_thunk_symbol_name)); const DeferredExport = struct { name: []const u8, e: *const def.ModuleDefinition.Export, }; var renames: std.ArrayList(DeferredExport) = .empty; defer renames.deinit(allocator); var regular_imports: std.StringArrayHashMapUnmanaged([]const u8) = .empty; defer regular_imports.deinit(allocator); for (module_def.exports.items) |*e| { if (e.private) continue; const maybe_mangled_name = e.mangled_symbol_name orelse e.name; const name = maybe_mangled_name; if (e.ext_name) |ext_name| { _ = ext_name; @panic("TODO"); // impossible if fixupForImportLibraryGeneration is called } var import_name_type: std.coff.ImportNameType = undefined; var export_name: ?[]const u8 = null; if (e.no_name) { import_name_type = .ORDINAL; } else if (e.export_as) |export_as| { import_name_type = .NAME_EXPORTAS; export_name = export_as; } else if (e.import_name) |import_name| { if (machine_type == .I386 and std.mem.eql(u8, applyNameType(.NAME_UNDECORATE, name), import_name)) { import_name_type = .NAME_UNDECORATE; } else if (machine_type == .I386 and std.mem.eql(u8, applyNameType(.NAME_NOPREFIX, name), import_name)) { import_name_type = .NAME_NOPREFIX; } else if (isArm64EC(machine_type)) { import_name_type = .NAME_EXPORTAS; export_name = import_name; } else if (std.mem.eql(u8, name, import_name)) { import_name_type = .NAME; } else { try renames.append(allocator, .{ .name = name, .e = e, }); continue; } } else { import_name_type = getNameType(maybe_mangled_name, e.name, machine_type, module_def.type); } try regular_imports.put(allocator, applyNameType(import_name_type, name), name); try members.list.append(arena, try getShortImport(arena, module_import_name, name, export_name, machine_type, e.ordinal, e.type, import_name_type)); } for (renames.items) |deferred| { const import_name = deferred.e.import_name.?; if (regular_imports.get(import_name)) |symbol| { if (deferred.e.type == .CODE) { try members.list.append(arena, try getWeakExternal(arena, module_import_name, symbol, deferred.name, .{ .imp_prefix = false, .machine_type = machine_type, })); } try members.list.append(arena, try getWeakExternal(arena, module_import_name, symbol, deferred.name, .{ .imp_prefix = true, .machine_type = machine_type, })); } else { try members.list.append(arena, try getShortImport( arena, module_import_name, deferred.name, deferred.e.import_name, machine_type, deferred.e.ordinal, deferred.e.type, .NAME_EXPORTAS, )); } } return members; } /// Returns a slice of `name` fn applyNameType(name_type: std.coff.ImportNameType, name: []const u8) []const u8 { switch (name_type) { .NAME_NOPREFIX, .NAME_UNDECORATE => { if (name.len == 0) return name; const unprefixed = switch (name[0]) { '?', '@', '_' => name[1..], else => name, }; if (name_type == .NAME_UNDECORATE) { var split = std.mem.splitScalar(u8, unprefixed, '@'); return split.first(); } else { return unprefixed; } }, else => return name, } } fn getNameType( symbol: []const u8, ext_name: []const u8, machine_type: std.coff.IMAGE.FILE.MACHINE, module_definition_type: def.ModuleDefinitionType, ) std.coff.ImportNameType { // A decorated stdcall function in MSVC is exported with the // type IMPORT_NAME, and the exported function name includes the // the leading underscore. In MinGW on the other hand, a decorated // stdcall function still omits the underscore (IMPORT_NAME_NOPREFIX). if (std.mem.startsWith(u8, ext_name, "_") and std.mem.indexOfScalar(u8, ext_name, '@') != null and module_definition_type != .mingw) return .NAME; if (!std.mem.eql(u8, symbol, ext_name)) return .NAME_UNDECORATE; if (machine_type == .I386 and std.mem.startsWith(u8, symbol, "_")) return .NAME_NOPREFIX; return .NAME; } fn is64Bit(machine_type: std.coff.IMAGE.FILE.MACHINE) bool { return switch (machine_type) { .AMD64, .ARM64, .ARM64EC, .ARM64X => true, else => false, }; } fn isArm64EC(machine_type: std.coff.IMAGE.FILE.MACHINE) bool { return switch (machine_type) { .ARM64EC, .ARM64X => true, else => false, }; } const null_import_descriptor_symbol_name = "__NULL_IMPORT_DESCRIPTOR"; const import_descriptor_prefix = "__IMPORT_DESCRIPTOR_"; const null_thunk_data_prefix = "\x7F"; const null_thunk_data_suffix = "_NULL_THUNK_DATA"; // past the string table length field const first_string_table_entry_offset = @sizeOf(u32); const first_string_table_entry = getNameBytesForStringTableOffset(first_string_table_entry_offset); const byte_size_of_relocation = 10; fn getNameBytesForStringTableOffset(offset: u32) [8]u8 { var bytes = [_]u8{0} ** 8; std.mem.writeInt(u32, bytes[4..8], offset, .little); return bytes; } const GetImportDescriptorError = error{UnsupportedMachineType} || std.mem.Allocator.Error; fn getImportDescriptor( allocator: std.mem.Allocator, machine_type: std.coff.IMAGE.FILE.MACHINE, module_import_name: []const u8, import_descriptor_symbol_name: []const u8, null_thunk_symbol_name: []const u8, ) GetImportDescriptorError!Members.Member { const number_of_sections = 2; const number_of_symbols = 7; const number_of_relocations = 3; const pointer_to_idata2_data = @sizeOf(std.coff.Header) + (@sizeOf(std.coff.SectionHeader) * number_of_sections); const pointer_to_idata6_data = pointer_to_idata2_data + @sizeOf(std.coff.ImportDirectoryEntry) + (byte_size_of_relocation * number_of_relocations); const pointer_to_symbol_table = pointer_to_idata6_data + module_import_name.len + 1; const string_table_byte_len = 4 + (import_descriptor_symbol_name.len + 1) + (null_import_descriptor_symbol_name.len + 1) + (null_thunk_symbol_name.len + 1); const total_byte_len = pointer_to_symbol_table + (std.coff.Symbol.sizeOf() * number_of_symbols) + string_table_byte_len; const bytes = try allocator.alloc(u8, total_byte_len); errdefer allocator.free(bytes); var writer: std.Io.Writer = .fixed(bytes); writer.writeStruct(std.coff.Header{ .machine = machine_type, .number_of_sections = number_of_sections, .time_date_stamp = 0, .pointer_to_symbol_table = @intCast(pointer_to_symbol_table), .number_of_symbols = number_of_symbols, .size_of_optional_header = 0, .flags = .{ .@"32BIT_MACHINE" = !is64Bit(machine_type) }, }, .little) catch unreachable; writer.writeStruct(std.coff.SectionHeader{ .name = ".idata$2".*, .virtual_size = 0, .virtual_address = 0, .size_of_raw_data = @sizeOf(std.coff.ImportDirectoryEntry), .pointer_to_raw_data = pointer_to_idata2_data, .pointer_to_relocations = pointer_to_idata2_data + @sizeOf(std.coff.ImportDirectoryEntry), .pointer_to_linenumbers = 0, .number_of_relocations = number_of_relocations, .number_of_linenumbers = 0, .flags = .{ .ALIGN = .@"4BYTES", .CNT_INITIALIZED_DATA = true, .MEM_WRITE = true, .MEM_READ = true, }, }, .little) catch unreachable; writer.writeStruct(std.coff.SectionHeader{ .name = ".idata$6".*, .virtual_size = 0, .virtual_address = 0, .size_of_raw_data = @intCast(module_import_name.len + 1), .pointer_to_raw_data = pointer_to_idata6_data, .pointer_to_relocations = 0, .pointer_to_linenumbers = 0, .number_of_relocations = 0, .number_of_linenumbers = 0, .flags = .{ .ALIGN = .@"2BYTES", .CNT_INITIALIZED_DATA = true, .MEM_WRITE = true, .MEM_READ = true, }, }, .little) catch unreachable; // .idata$2 writer.writeStruct(std.coff.ImportDirectoryEntry{ .forwarder_chain = 0, .import_address_table_rva = 0, .import_lookup_table_rva = 0, .name_rva = 0, .time_date_stamp = 0, }, .little) catch unreachable; const relocation_rva_type = rvaRelocationTypeIndicator(machine_type) orelse return error.UnsupportedMachineType; writeRelocation(&writer, .{ .virtual_address = @offsetOf(std.coff.ImportDirectoryEntry, "name_rva"), .symbol_table_index = 2, .type = relocation_rva_type, }) catch unreachable; writeRelocation(&writer, .{ .virtual_address = @offsetOf(std.coff.ImportDirectoryEntry, "import_lookup_table_rva"), .symbol_table_index = 3, .type = relocation_rva_type, }) catch unreachable; writeRelocation(&writer, .{ .virtual_address = @offsetOf(std.coff.ImportDirectoryEntry, "import_address_table_rva"), .symbol_table_index = 4, .type = relocation_rva_type, }) catch unreachable; // .idata$6 writer.writeAll(module_import_name) catch unreachable; writer.writeByte(0) catch unreachable; var string_table_offset: usize = first_string_table_entry_offset; writeSymbol(&writer, .{ .name = first_string_table_entry, .value = 0, .section_number = @enumFromInt(1), .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .EXTERNAL, .number_of_aux_symbols = 0, }) catch unreachable; string_table_offset += import_descriptor_symbol_name.len + 1; writeSymbol(&writer, .{ .name = ".idata$2".*, .value = 0, .section_number = @enumFromInt(1), .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .SECTION, .number_of_aux_symbols = 0, }) catch unreachable; writeSymbol(&writer, .{ .name = ".idata$6".*, .value = 0, .section_number = @enumFromInt(2), .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .STATIC, .number_of_aux_symbols = 0, }) catch unreachable; writeSymbol(&writer, .{ .name = ".idata$4".*, .value = 0, .section_number = .UNDEFINED, .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .SECTION, .number_of_aux_symbols = 0, }) catch unreachable; writeSymbol(&writer, .{ .name = ".idata$5".*, .value = 0, .section_number = .UNDEFINED, .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .SECTION, .number_of_aux_symbols = 0, }) catch unreachable; writeSymbol(&writer, .{ .name = getNameBytesForStringTableOffset(@intCast(string_table_offset)), .value = 0, .section_number = .UNDEFINED, .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .EXTERNAL, .number_of_aux_symbols = 0, }) catch unreachable; string_table_offset += null_import_descriptor_symbol_name.len + 1; writeSymbol(&writer, .{ .name = getNameBytesForStringTableOffset(@intCast(string_table_offset)), .value = 0, .section_number = .UNDEFINED, .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .EXTERNAL, .number_of_aux_symbols = 0, }) catch unreachable; string_table_offset += null_thunk_symbol_name.len + 1; // string table writer.writeInt(u32, @intCast(string_table_byte_len), .little) catch unreachable; writer.writeAll(import_descriptor_symbol_name) catch unreachable; writer.writeByte(0) catch unreachable; writer.writeAll(null_import_descriptor_symbol_name) catch unreachable; writer.writeByte(0) catch unreachable; writer.writeAll(null_thunk_symbol_name) catch unreachable; writer.writeByte(0) catch unreachable; var symbol_names_for_import_lib = try allocator.alloc([]const u8, 1); errdefer allocator.free(symbol_names_for_import_lib); const duped_symbol_name = try allocator.dupe(u8, import_descriptor_symbol_name); errdefer allocator.free(duped_symbol_name); symbol_names_for_import_lib[0] = duped_symbol_name; // Confirm byte length was calculated exactly correctly std.debug.assert(writer.end == bytes.len); return .{ .bytes = bytes, .name = module_import_name, .symbol_names_for_import_lib = symbol_names_for_import_lib, }; } fn getNullImportDescriptor( allocator: std.mem.Allocator, machine_type: std.coff.IMAGE.FILE.MACHINE, module_import_name: []const u8, ) error{OutOfMemory}!Members.Member { const number_of_sections = 1; const number_of_symbols = 1; const pointer_to_idata3_data = @sizeOf(std.coff.Header) + (@sizeOf(std.coff.SectionHeader) * number_of_sections); const pointer_to_symbol_table = pointer_to_idata3_data + @sizeOf(std.coff.ImportDirectoryEntry); const string_table_byte_len = 4 + null_import_descriptor_symbol_name.len + 1; const total_byte_len = pointer_to_symbol_table + (std.coff.Symbol.sizeOf() * number_of_symbols) + string_table_byte_len; const bytes = try allocator.alloc(u8, total_byte_len); errdefer allocator.free(bytes); var writer: std.Io.Writer = .fixed(bytes); writer.writeStruct(std.coff.Header{ .machine = machine_type, .number_of_sections = number_of_sections, .time_date_stamp = 0, .pointer_to_symbol_table = @intCast(pointer_to_symbol_table), .number_of_symbols = number_of_symbols, .size_of_optional_header = 0, .flags = .{ .@"32BIT_MACHINE" = !is64Bit(machine_type) }, }, .little) catch unreachable; writer.writeStruct(std.coff.SectionHeader{ .name = ".idata$3".*, .virtual_size = 0, .virtual_address = 0, .size_of_raw_data = @sizeOf(std.coff.ImportDirectoryEntry), .pointer_to_raw_data = pointer_to_idata3_data, .pointer_to_relocations = 0, .pointer_to_linenumbers = 0, .number_of_relocations = 0, .number_of_linenumbers = 0, .flags = .{ .ALIGN = .@"4BYTES", .CNT_INITIALIZED_DATA = true, .MEM_WRITE = true, .MEM_READ = true, }, }, .little) catch unreachable; writer.writeStruct(std.coff.ImportDirectoryEntry{ .forwarder_chain = 0, .import_address_table_rva = 0, .import_lookup_table_rva = 0, .name_rva = 0, .time_date_stamp = 0, }, .little) catch unreachable; writeSymbol(&writer, .{ .name = first_string_table_entry, .value = 0, .section_number = @enumFromInt(1), .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .EXTERNAL, .number_of_aux_symbols = 0, }) catch unreachable; // string table writer.writeInt(u32, string_table_byte_len, .little) catch unreachable; writer.writeAll(null_import_descriptor_symbol_name) catch unreachable; writer.writeByte(0) catch unreachable; var symbol_names_for_import_lib = try allocator.alloc([]const u8, 1); errdefer allocator.free(symbol_names_for_import_lib); const duped_symbol_name = try allocator.dupe(u8, null_import_descriptor_symbol_name); errdefer allocator.free(duped_symbol_name); symbol_names_for_import_lib[0] = duped_symbol_name; // Confirm byte length was calculated exactly correctly std.debug.assert(writer.end == bytes.len); return .{ .bytes = bytes, .name = module_import_name, .symbol_names_for_import_lib = symbol_names_for_import_lib, }; } fn getNullThunk( allocator: std.mem.Allocator, machine_type: std.coff.IMAGE.FILE.MACHINE, module_import_name: []const u8, null_thunk_symbol_name: []const u8, ) error{OutOfMemory}!Members.Member { const number_of_sections = 2; const number_of_symbols = 1; const va_size: u32 = if (is64Bit(machine_type)) 8 else 4; const pointer_to_idata5_data = @sizeOf(std.coff.Header) + (@sizeOf(std.coff.SectionHeader) * number_of_sections); const pointer_to_idata4_data = pointer_to_idata5_data + va_size; const pointer_to_symbol_table = pointer_to_idata4_data + va_size; const string_table_byte_len = 4 + null_thunk_symbol_name.len + 1; const total_byte_len = pointer_to_symbol_table + (std.coff.Symbol.sizeOf() * number_of_symbols) + string_table_byte_len; const bytes = try allocator.alloc(u8, total_byte_len); errdefer allocator.free(bytes); var writer: std.Io.Writer = .fixed(bytes); writer.writeStruct(std.coff.Header{ .machine = machine_type, .number_of_sections = number_of_sections, .time_date_stamp = 0, .pointer_to_symbol_table = @intCast(pointer_to_symbol_table), .number_of_symbols = number_of_symbols, .size_of_optional_header = 0, .flags = .{ .@"32BIT_MACHINE" = !is64Bit(machine_type) }, }, .little) catch unreachable; writer.writeStruct(std.coff.SectionHeader{ .name = ".idata$5".*, .virtual_size = 0, .virtual_address = 0, .size_of_raw_data = va_size, .pointer_to_raw_data = pointer_to_idata5_data, .pointer_to_relocations = 0, .pointer_to_linenumbers = 0, .number_of_relocations = 0, .number_of_linenumbers = 0, .flags = .{ .ALIGN = if (is64Bit(machine_type)) .@"8BYTES" else .@"4BYTES", .CNT_INITIALIZED_DATA = true, .MEM_WRITE = true, .MEM_READ = true, }, }, .little) catch unreachable; writer.writeStruct(std.coff.SectionHeader{ .name = ".idata$4".*, .virtual_size = 0, .virtual_address = 0, .size_of_raw_data = va_size, .pointer_to_raw_data = pointer_to_idata4_data, .pointer_to_relocations = 0, .pointer_to_linenumbers = 0, .number_of_relocations = 0, .number_of_linenumbers = 0, .flags = .{ .ALIGN = if (is64Bit(machine_type)) .@"8BYTES" else .@"4BYTES", .CNT_INITIALIZED_DATA = true, .MEM_WRITE = true, .MEM_READ = true, }, }, .little) catch unreachable; // .idata$5 writer.splatByteAll(0, va_size) catch unreachable; // .idata$4 writer.splatByteAll(0, va_size) catch unreachable; writeSymbol(&writer, .{ .name = first_string_table_entry, .value = 0, .section_number = @enumFromInt(1), .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .EXTERNAL, .number_of_aux_symbols = 0, }) catch unreachable; // string table writer.writeInt(u32, @intCast(string_table_byte_len), .little) catch unreachable; writer.writeAll(null_thunk_symbol_name) catch unreachable; writer.writeByte(0) catch unreachable; var symbol_names_for_import_lib = try allocator.alloc([]const u8, 1); errdefer allocator.free(symbol_names_for_import_lib); const duped_symbol_name = try allocator.dupe(u8, null_thunk_symbol_name); errdefer allocator.free(duped_symbol_name); symbol_names_for_import_lib[0] = duped_symbol_name; // Confirm byte length was calculated exactly correctly std.debug.assert(writer.end == bytes.len); return .{ .bytes = bytes, .name = module_import_name, .symbol_names_for_import_lib = symbol_names_for_import_lib, }; } const WeakExternalOptions = struct { imp_prefix: bool, machine_type: std.coff.IMAGE.FILE.MACHINE, }; fn getWeakExternal( arena: std.mem.Allocator, module_import_name: []const u8, sym: []const u8, weak: []const u8, options: WeakExternalOptions, ) error{OutOfMemory}!Members.Member { const number_of_sections = 1; const number_of_symbols = 4; const number_of_weak_external_defs = 1; const pointer_to_symbol_table = @sizeOf(std.coff.Header) + (@sizeOf(std.coff.SectionHeader) * number_of_sections); const symbol_names = try arena.alloc([]const u8, 2); symbol_names[0] = if (options.imp_prefix) try std.mem.concat(arena, u8, &.{ "__imp_", sym }) else try arena.dupe(u8, sym); symbol_names[1] = if (options.imp_prefix) try std.mem.concat(arena, u8, &.{ "__imp_", weak }) else try arena.dupe(u8, weak); const string_table_byte_len = 4 + symbol_names[0].len + 1 + symbol_names[1].len + 1; const total_byte_len = pointer_to_symbol_table + (std.coff.Symbol.sizeOf() * number_of_symbols) + (std.coff.WeakExternalDefinition.sizeOf() * number_of_weak_external_defs) + string_table_byte_len; const bytes = try arena.alloc(u8, total_byte_len); errdefer arena.free(bytes); var writer: std.Io.Writer = .fixed(bytes); writer.writeStruct(std.coff.Header{ .machine = options.machine_type, .number_of_sections = number_of_sections, .time_date_stamp = 0, .pointer_to_symbol_table = @intCast(pointer_to_symbol_table), .number_of_symbols = number_of_symbols + number_of_weak_external_defs, .size_of_optional_header = 0, .flags = .{}, }, .little) catch unreachable; writer.writeStruct(std.coff.SectionHeader{ .name = ".drectve".*, .virtual_size = 0, .virtual_address = 0, .size_of_raw_data = 0, .pointer_to_raw_data = 0, .pointer_to_relocations = 0, .pointer_to_linenumbers = 0, .number_of_relocations = 0, .number_of_linenumbers = 0, .flags = .{ .LNK_INFO = true, .LNK_REMOVE = true, }, }, .little) catch unreachable; writeSymbol(&writer, .{ .name = "@comp.id".*, .value = 0, .section_number = .ABSOLUTE, .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .STATIC, .number_of_aux_symbols = 0, }) catch unreachable; writeSymbol(&writer, .{ .name = "@feat.00".*, .value = 0, .section_number = .ABSOLUTE, .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .STATIC, .number_of_aux_symbols = 0, }) catch unreachable; var string_table_offset: usize = first_string_table_entry_offset; writeSymbol(&writer, .{ .name = first_string_table_entry, .value = 0, .section_number = @enumFromInt(0), .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .EXTERNAL, .number_of_aux_symbols = 0, }) catch unreachable; string_table_offset += symbol_names[0].len + 1; writeSymbol(&writer, .{ .name = getNameBytesForStringTableOffset(@intCast(string_table_offset)), .value = 0, .section_number = @enumFromInt(0), .type = .{ .base_type = .NULL, .complex_type = .NULL, }, .storage_class = .WEAK_EXTERNAL, .number_of_aux_symbols = 1, }) catch unreachable; writeWeakExternalDefinition(&writer, .{ .tag_index = 2, .flag = .SEARCH_ALIAS, .unused = @splat(0), }) catch unreachable; // string table writer.writeInt(u32, @intCast(string_table_byte_len), .little) catch unreachable; writer.writeAll(symbol_names[0]) catch unreachable; writer.writeByte(0) catch unreachable; writer.writeAll(symbol_names[1]) catch unreachable; writer.writeByte(0) catch unreachable; // Confirm byte length was calculated exactly correctly std.debug.assert(writer.end == bytes.len); return .{ .bytes = bytes, .name = module_import_name, .symbol_names_for_import_lib = symbol_names, }; } const GetShortImportError = error{UnknownImportType} || std.mem.Allocator.Error; fn getShortImport( arena: std.mem.Allocator, module_import_name: []const u8, sym: []const u8, export_name: ?[]const u8, machine_type: std.coff.IMAGE.FILE.MACHINE, ordinal_hint: u16, import_type: std.coff.ImportType, name_type: std.coff.ImportNameType, ) GetShortImportError!Members.Member { var size_of_data = module_import_name.len + 1 + sym.len + 1; if (export_name) |name| size_of_data += name.len + 1; const total_byte_len = @sizeOf(std.coff.ImportHeader) + size_of_data; const bytes = try arena.alloc(u8, total_byte_len); errdefer arena.free(bytes); var writer = std.Io.Writer.fixed(bytes); writer.writeStruct(std.coff.ImportHeader{ .version = 0, .machine = machine_type, .time_date_stamp = 0, .size_of_data = @intCast(size_of_data), .hint = ordinal_hint, .types = .{ .type = import_type, .name_type = name_type, .reserved = 0, }, }, .little) catch unreachable; writer.writeAll(sym) catch unreachable; writer.writeByte(0) catch unreachable; writer.writeAll(module_import_name) catch unreachable; writer.writeByte(0) catch unreachable; if (export_name) |name| { writer.writeAll(name) catch unreachable; writer.writeByte(0) catch unreachable; } var symbol_names_for_import_lib: std.ArrayList([]const u8) = try .initCapacity(arena, 2); switch (import_type) { .CODE, .CONST => { symbol_names_for_import_lib.appendAssumeCapacity(try std.mem.concat(arena, u8, &.{ "__imp_", sym })); symbol_names_for_import_lib.appendAssumeCapacity(try arena.dupe(u8, sym)); }, .DATA => { symbol_names_for_import_lib.appendAssumeCapacity(try std.mem.concat(arena, u8, &.{ "__imp_", sym })); }, else => return error.UnknownImportType, } // Confirm byte length was calculated exactly correctly std.debug.assert(writer.end == bytes.len); return .{ .bytes = bytes, .name = module_import_name, .symbol_names_for_import_lib = try symbol_names_for_import_lib.toOwnedSlice(arena), }; } fn writeSymbol(writer: *std.Io.Writer, symbol: std.coff.Symbol) !void { try writer.writeAll(&symbol.name); try writer.writeInt(u32, symbol.value, .little); try writer.writeInt(u16, @intFromEnum(symbol.section_number), .little); try writer.writeInt(u8, @intFromEnum(symbol.type.base_type), .little); try writer.writeInt(u8, @intFromEnum(symbol.type.complex_type), .little); try writer.writeInt(u8, @intFromEnum(symbol.storage_class), .little); try writer.writeInt(u8, symbol.number_of_aux_symbols, .little); } fn writeWeakExternalDefinition(writer: *std.Io.Writer, weak_external: std.coff.WeakExternalDefinition) !void { try writer.writeInt(u32, weak_external.tag_index, .little); try writer.writeInt(u32, @intFromEnum(weak_external.flag), .little); try writer.writeAll(&weak_external.unused); } fn writeRelocation(writer: *std.Io.Writer, relocation: std.coff.Relocation) !void { try writer.writeInt(u32, relocation.virtual_address, .little); try writer.writeInt(u32, relocation.symbol_table_index, .little); try writer.writeInt(u16, relocation.type, .little); } // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#type-indicators pub fn rvaRelocationTypeIndicator(target: std.coff.IMAGE.FILE.MACHINE) ?u16 { return switch (target) { .AMD64 => @intFromEnum(std.coff.IMAGE.REL.AMD64.ADDR32NB), .I386 => @intFromEnum(std.coff.IMAGE.REL.I386.DIR32NB), .ARMNT => @intFromEnum(std.coff.IMAGE.REL.ARM.ADDR32NB), .ARM64, .ARM64EC, .ARM64X => @intFromEnum(std.coff.IMAGE.REL.ARM64.ADDR32NB), .IA64 => @intFromEnum(std.coff.IMAGE.REL.IA64.DIR32NB), else => null, }; } const StringTable = struct { data: std.ArrayList(u8) = .empty, map: std.HashMapUnmanaged(u32, void, std.hash_map.StringIndexContext, std.hash_map.default_max_load_percentage) = .empty, pub fn deinit(self: *StringTable, allocator: Allocator) void { self.data.deinit(allocator); self.map.deinit(allocator); } pub fn put(self: *StringTable, allocator: Allocator, value: []const u8) !u32 { const result = try self.map.getOrPutContextAdapted( allocator, value, std.hash_map.StringIndexAdapter{ .bytes = &self.data }, .{ .bytes = &self.data }, ); if (result.found_existing) { return result.key_ptr.*; } try self.data.ensureUnusedCapacity(allocator, value.len + 1); const offset: u32 = @intCast(self.data.items.len); self.data.appendSliceAssumeCapacity(value); self.data.appendAssumeCapacity(0); result.key_ptr.* = offset; return offset; } pub fn get(self: StringTable, offset: u32) []const u8 { std.debug.assert(offset < self.data.items.len); return std.mem.sliceTo(@as([*:0]const u8, @ptrCast(self.data.items.ptr + offset)), 0); } pub fn getOffset(self: *StringTable, value: []const u8) ?u32 { return self.map.getKeyAdapted( value, std.hash_map.StringIndexAdapter{ .bytes = &self.data }, ); } };