Commit e393543e63

Ryan Liptak <squeek502@hotmail.com>
2025-10-01 07:06:36
Support generating import libraries from mingw .def files without LLVM
For the supported COFF machine types of X64 (x86_64), I386 (x86), ARMNT (thumb), and ARM64 (aarch64), this new Zig implementation results in byte-for-byte identical .lib files when compared to the previous LLVM-backed implementation.
1 parent 900315a
Changed files (6)
src/codegen/llvm/bindings.zig
@@ -338,14 +338,6 @@ extern fn ZigLLVMWriteArchive(
 pub const ParseCommandLineOptions = ZigLLVMParseCommandLineOptions;
 extern fn ZigLLVMParseCommandLineOptions(argc: usize, argv: [*]const [*:0]const u8) void;
 
-pub const WriteImportLibrary = ZigLLVMWriteImportLibrary;
-extern fn ZigLLVMWriteImportLibrary(
-    def_path: [*:0]const u8,
-    coff_machine: c_uint,
-    output_lib_path: [*:0]const u8,
-    kill_at: bool,
-) bool;
-
 pub const GetHostCPUName = LLVMGetHostCPUName;
 extern fn LLVMGetHostCPUName() ?[*:0]u8;
 
src/libs/mingw/def.zig
@@ -0,0 +1,1079 @@
+const std = @import("std");
+
+pub const ModuleDefinitionType = enum {
+    mingw,
+};
+
+pub const ModuleDefinition = struct {
+    exports: std.ArrayList(Export) = .empty,
+    name: ?[]const u8 = null,
+    base_address: usize = 0,
+    arena: std.heap.ArenaAllocator,
+    type: ModuleDefinitionType,
+
+    pub const Export = struct {
+        /// This may lack mangling, such as underscore prefixing and stdcall suffixing.
+        /// In a .def file, this is `foo` in `foo` or `bar` in `foo = bar`.
+        name: []const u8,
+        /// Note: This is currently only set by `fixupForImportLibraryGeneration`
+        mangled_symbol_name: ?[]const u8,
+        /// The external, exported name.
+        /// In a .def file, this is `foo` in `foo = bar`.
+        ext_name: ?[]const u8,
+        /// In a .def file, this is `bar` in `foo == bar`.
+        import_name: ?[]const u8,
+        /// In a .def file, this is `bar` in `foo EXPORTAS bar`.
+        export_as: ?[]const u8,
+        no_name: bool,
+        ordinal: u16,
+        type: std.coff.ImportType,
+        private: bool,
+    };
+
+    /// Modifies `exports` such that import library generation will
+    /// behave as expected. Based on LLVM's dlltool driver.
+    pub fn fixupForImportLibraryGeneration(self: *ModuleDefinition, machine_type: std.coff.IMAGE.FILE.MACHINE) void {
+        const kill_at = true;
+        for (self.exports.items) |*e| {
+            // If ExtName is set (if the "ExtName = Name" syntax was used), overwrite
+            // Name with ExtName and clear ExtName. When only creating an import
+            // library and not linking, the internal name is irrelevant. This avoids
+            // cases where writeImportLibrary tries to transplant decoration from
+            // symbol decoration onto ExtName.
+            if (e.ext_name) |ext_name| {
+                e.name = ext_name;
+                e.ext_name = null;
+            }
+
+            if (kill_at) {
+                if (e.import_name != null or std.mem.startsWith(u8, e.name, "?"))
+                    continue;
+
+                if (machine_type == .I386) {
+                    // By making sure E.SymbolName != E.Name for decorated symbols,
+                    // writeImportLibrary writes these symbols with the type
+                    // IMPORT_NAME_UNDECORATE.
+                    e.mangled_symbol_name = e.name;
+                }
+                // Trim off the trailing decoration. Symbols will always have a
+                // starting prefix here (either _ for cdecl/stdcall, @ for fastcall
+                // or ? for C++ functions). Vectorcall functions won't have any
+                // fixed prefix, but the function base name will still be at least
+                // one char.
+                const name_len_without_at_suffix = std.mem.indexOfScalarPos(u8, e.name, 1, '@') orelse e.name.len;
+                e.name = e.name[0..name_len_without_at_suffix];
+            }
+        }
+    }
+
+    pub fn deinit(self: *const ModuleDefinition) void {
+        self.arena.deinit();
+    }
+};
+
+pub const Diagnostics = struct {
+    err: Error,
+    token: Token,
+    extra: Extra = .{ .none = {} },
+
+    pub const Extra = union {
+        none: void,
+        expected: Token.Tag,
+    };
+
+    pub const Error = enum {
+        invalid_byte,
+        unfinished_quoted_identifier,
+        /// `expected` is populated
+        expected_token,
+        expected_integer,
+        unknown_statement,
+        unimplemented,
+    };
+
+    fn formatToken(ctx: TokenFormatContext, writer: *std.Io.Writer) std.Io.Writer.Error!void {
+        switch (ctx.token.tag) {
+            .eof, .invalid => return writer.writeAll(ctx.token.tag.nameForErrorDisplay()),
+            else => return writer.writeAll(ctx.token.slice(ctx.source)),
+        }
+    }
+
+    const TokenFormatContext = struct {
+        token: Token,
+        source: []const u8,
+    };
+
+    fn fmtToken(self: Diagnostics, source: []const u8) std.fmt.Alt(TokenFormatContext, formatToken) {
+        return .{ .data = .{
+            .token = self.token,
+            .source = source,
+        } };
+    }
+
+    pub fn writeMsg(self: Diagnostics, writer: *std.Io.Writer, source: []const u8) !void {
+        switch (self.err) {
+            .invalid_byte => {
+                return writer.print("invalid byte '{f}'", .{std.ascii.hexEscape(self.token.slice(source), .upper)});
+            },
+            .unfinished_quoted_identifier => {
+                return writer.print("unfinished quoted identifier at '{f}', expected closing '\"'", .{self.fmtToken(source)});
+            },
+            .expected_token => {
+                return writer.print("expected '{s}', got '{f}'", .{ self.extra.expected.nameForErrorDisplay(), self.fmtToken(source) });
+            },
+            .expected_integer => {
+                return writer.print("expected integer, got '{f}'", .{self.fmtToken(source)});
+            },
+            .unimplemented => {
+                return writer.print("support for '{f}' has not yet been implemented", .{self.fmtToken(source)});
+            },
+            .unknown_statement => {
+                return writer.print("unknown/invalid statement syntax beginning with '{f}'", .{self.fmtToken(source)});
+            },
+        }
+    }
+};
+
+pub fn parse(
+    allocator: std.mem.Allocator,
+    source: [:0]const u8,
+    machine_type: std.coff.IMAGE.FILE.MACHINE,
+    module_definition_type: ModuleDefinitionType,
+    diagnostics: *Diagnostics,
+) !ModuleDefinition {
+    var tokenizer = Tokenizer.init(source);
+    var parser = Parser.init(&tokenizer, machine_type, module_definition_type, diagnostics);
+
+    return parser.parse(allocator);
+}
+
+const Token = struct {
+    tag: Tag,
+    start: usize,
+    end: usize,
+
+    pub const keywords = std.StaticStringMap(Tag).initComptime(.{
+        .{ "BASE", .keyword_base },
+        .{ "CONSTANT", .keyword_constant },
+        .{ "DATA", .keyword_data },
+        .{ "EXPORTS", .keyword_exports },
+        .{ "EXPORTAS", .keyword_exportas },
+        .{ "HEAPSIZE", .keyword_heapsize },
+        .{ "LIBRARY", .keyword_library },
+        .{ "NAME", .keyword_name },
+        .{ "NONAME", .keyword_noname },
+        .{ "PRIVATE", .keyword_private },
+        .{ "STACKSIZE", .keyword_stacksize },
+        .{ "VERSION", .keyword_version },
+    });
+
+    pub const Tag = enum {
+        invalid,
+        eof,
+        identifier,
+        comma,
+        equal,
+        equal_equal,
+        keyword_base,
+        keyword_constant,
+        keyword_data,
+        keyword_exports,
+        keyword_exportas,
+        keyword_heapsize,
+        keyword_library,
+        keyword_name,
+        keyword_noname,
+        keyword_private,
+        keyword_stacksize,
+        keyword_version,
+
+        pub fn nameForErrorDisplay(self: Tag) []const u8 {
+            return switch (self) {
+                .invalid => "<invalid>",
+                .eof => "<eof>",
+                .identifier => "<identifier>",
+                .comma => ",",
+                .equal => "=",
+                .equal_equal => "==",
+                .keyword_base => "BASE",
+                .keyword_constant => "CONSTANT",
+                .keyword_data => "DATA",
+                .keyword_exports => "EXPORTS",
+                .keyword_exportas => "EXPORTAS",
+                .keyword_heapsize => "HEAPSIZE",
+                .keyword_library => "LIBRARY",
+                .keyword_name => "NAME",
+                .keyword_noname => "NONAME",
+                .keyword_private => "PRIVATE",
+                .keyword_stacksize => "STACKSIZE",
+                .keyword_version => "VERSION",
+            };
+        }
+    };
+
+    /// Returns a useful slice of the token, e.g. for quoted identifiers, this
+    /// will return a slice without the quotes included.
+    pub fn slice(self: Token, source: []const u8) []const u8 {
+        return source[self.start..self.end];
+    }
+};
+
+const Tokenizer = struct {
+    source: [:0]const u8,
+    index: usize,
+    error_context_token: ?Token = null,
+
+    pub fn init(source: [:0]const u8) Tokenizer {
+        return .{
+            .source = source,
+            .index = 0,
+        };
+    }
+
+    const State = enum {
+        start,
+        identifier_or_keyword,
+        quoted_identifier,
+        comment,
+        equal,
+        eof_or_invalid,
+    };
+
+    pub const Error = error{
+        InvalidByte,
+        UnfinishedQuotedIdentifier,
+    };
+
+    pub fn next(self: *Tokenizer) Error!Token {
+        var result: Token = .{
+            .tag = undefined,
+            .start = self.index,
+            .end = undefined,
+        };
+        state: switch (State.start) {
+            .start => switch (self.source[self.index]) {
+                0 => continue :state .eof_or_invalid,
+                '\r', '\n', ' ', '\t', '\x0B' => {
+                    self.index += 1;
+                    result.start = self.index;
+                    continue :state .start;
+                },
+                ';' => continue :state .comment,
+                '=' => continue :state .equal,
+                ',' => {
+                    result.tag = .comma;
+                    self.index += 1;
+                },
+                '"' => continue :state .quoted_identifier,
+                else => continue :state .identifier_or_keyword,
+            },
+            .comment => {
+                self.index += 1;
+                switch (self.source[self.index]) {
+                    0 => continue :state .eof_or_invalid,
+                    '\n' => {
+                        self.index += 1;
+                        result.start = self.index;
+                        continue :state .start;
+                    },
+                    else => continue :state .comment,
+                }
+            },
+            .equal => {
+                self.index += 1;
+                switch (self.source[self.index]) {
+                    '=' => {
+                        result.tag = .equal_equal;
+                        self.index += 1;
+                    },
+                    else => result.tag = .equal,
+                }
+            },
+            .quoted_identifier => {
+                self.index += 1;
+                switch (self.source[self.index]) {
+                    0 => {
+                        self.error_context_token = .{
+                            .tag = .eof,
+                            .start = self.index,
+                            .end = self.index,
+                        };
+                        return error.UnfinishedQuotedIdentifier;
+                    },
+                    '"' => {
+                        result.tag = .identifier;
+                        self.index += 1;
+
+                        // Return the token unquoted
+                        return .{
+                            .tag = result.tag,
+                            .start = result.start + 1,
+                            .end = self.index - 1,
+                        };
+                    },
+                    else => continue :state .quoted_identifier,
+                }
+            },
+            .identifier_or_keyword => {
+                self.index += 1;
+                switch (self.source[self.index]) {
+                    0, '=', ',', ';', '\r', '\n', ' ', '\t', '\x0B' => {
+                        const keyword = Token.keywords.get(self.source[result.start..self.index]);
+                        result.tag = keyword orelse .identifier;
+                    },
+                    else => continue :state .identifier_or_keyword,
+                }
+            },
+            .eof_or_invalid => {
+                if (self.index == self.source.len) {
+                    return .{
+                        .tag = .eof,
+                        .start = self.index,
+                        .end = self.index,
+                    };
+                }
+                self.error_context_token = .{
+                    .tag = .invalid,
+                    .start = self.index,
+                    .end = self.index + 1,
+                };
+                return error.InvalidByte;
+            },
+        }
+
+        result.end = self.index;
+        return result;
+    }
+};
+
+test Tokenizer {
+    try testTokenizer(
+        \\foo
+        \\; hello
+        \\BASE
+        \\"bar"
+        \\
+    , &.{
+        .identifier,
+        .keyword_base,
+        .identifier,
+    });
+}
+
+fn testTokenizer(source: [:0]const u8, expected: []const Token.Tag) !void {
+    var tokenizer = Tokenizer.init(source);
+    for (expected) |expected_tag| {
+        const token = try tokenizer.next();
+        try std.testing.expectEqual(expected_tag, token.tag);
+    }
+    const last_token = try tokenizer.next();
+    try std.testing.expectEqual(.eof, last_token.tag);
+}
+
+pub const Parser = struct {
+    tokenizer: *Tokenizer,
+    diagnostics: *Diagnostics,
+    lookahead_tokenizer: Tokenizer,
+    machine_type: std.coff.IMAGE.FILE.MACHINE,
+    module_definition_type: ModuleDefinitionType,
+
+    pub fn init(
+        tokenizer: *Tokenizer,
+        machine_type: std.coff.IMAGE.FILE.MACHINE,
+        module_definition_type: ModuleDefinitionType,
+        diagnostics: *Diagnostics,
+    ) Parser {
+        return .{
+            .tokenizer = tokenizer,
+            .machine_type = machine_type,
+            .module_definition_type = module_definition_type,
+            .diagnostics = diagnostics,
+            .lookahead_tokenizer = undefined,
+        };
+    }
+
+    pub const Error = error{ParseError} || std.mem.Allocator.Error;
+
+    pub fn parse(self: *Parser, allocator: std.mem.Allocator) Error!ModuleDefinition {
+        var module: ModuleDefinition = .{
+            .arena = .init(allocator),
+            .type = self.module_definition_type,
+        };
+        const arena = module.arena.allocator();
+        errdefer module.deinit();
+        while (true) {
+            const tok = try self.nextToken();
+            switch (tok.tag) {
+                .eof => break,
+                .keyword_library, .keyword_name => {
+                    const is_library = tok.tag == .keyword_library;
+
+                    const name = try self.lookaheadToken();
+                    if (name.tag != .identifier) continue;
+                    self.commitLookahead();
+
+                    const base_tok = try self.lookaheadToken();
+                    if (base_tok.tag == .keyword_base) {
+                        self.commitLookahead();
+
+                        _ = try self.expectToken(.equal);
+
+                        module.base_address = try self.expectInteger(usize);
+                    }
+
+                    // Append .dll/.exe if there's no extension
+                    const name_slice = name.slice(self.tokenizer.source);
+                    module.name = if (std.fs.path.extension(name_slice).len == 0)
+                        try std.mem.concat(arena, u8, &.{ name_slice, if (is_library) ".dll" else ".exe" })
+                    else
+                        try arena.dupe(u8, name_slice);
+                },
+                .keyword_exports => {
+                    while (true) {
+                        var name_tok = try self.lookaheadToken();
+                        if (name_tok.tag != .identifier) break;
+                        self.commitLookahead();
+
+                        const ext_name_tok = ext_name: {
+                            const equal = try self.lookaheadToken();
+                            if (equal.tag != .equal) break :ext_name null;
+                            self.commitLookahead();
+
+                            // The syntax is `<ext_name> = <name>`, so we need to
+                            // swap the current name token over to ext_name and use
+                            // this token as the name.
+                            const ext_name_tok = name_tok;
+                            name_tok = try self.expectToken(.identifier);
+                            break :ext_name ext_name_tok;
+                        };
+
+                        var name_needs_underscore = false;
+                        var ext_name_needs_underscore = false;
+                        if (self.machine_type == .I386) {
+                            const is_decorated = isDecorated(name_tok.slice(self.tokenizer.source), self.module_definition_type);
+                            const is_forward_target = ext_name_tok != null and std.mem.indexOfScalar(u8, name_tok.slice(self.tokenizer.source), '.') != null;
+                            name_needs_underscore = !is_decorated and !is_forward_target;
+
+                            if (ext_name_tok) |ext_name| {
+                                ext_name_needs_underscore = !isDecorated(ext_name.slice(self.tokenizer.source), self.module_definition_type);
+                            }
+                        }
+
+                        var import_name_tok: ?Token = null;
+                        var export_as_tok: ?Token = null;
+                        var ordinal: ?u16 = null;
+                        var import_type: std.coff.ImportType = .CODE;
+                        var private: bool = false;
+                        var no_name: bool = false;
+                        while (true) {
+                            const arg_tok = try self.lookaheadToken();
+                            switch (arg_tok.tag) {
+                                .identifier => {
+                                    const slice = arg_tok.slice(self.tokenizer.source);
+                                    if (slice[0] != '@') break;
+
+                                    // foo @ 10
+                                    if (slice.len == 1) {
+                                        self.commitLookahead();
+                                        ordinal = try self.expectInteger(u16);
+                                        continue;
+                                    }
+                                    // foo @10
+                                    ordinal = std.fmt.parseUnsigned(u16, slice[1..], 0) catch {
+                                        // e.g. foo @bar, the @bar is presumed to be the start of a separate
+                                        // export (and there could be a newline between them)
+                                        break;
+                                    };
+                                    // finally safe to commit to consuming the token
+                                    self.commitLookahead();
+
+                                    const noname_tok = try self.lookaheadToken();
+                                    if (noname_tok.tag == .keyword_noname) {
+                                        self.commitLookahead();
+                                        no_name = true;
+                                    }
+                                },
+                                .equal_equal => {
+                                    self.commitLookahead();
+                                    import_name_tok = try self.expectToken(.identifier);
+                                },
+                                .keyword_data => {
+                                    self.commitLookahead();
+                                    import_type = .DATA;
+                                },
+                                .keyword_constant => {
+                                    self.commitLookahead();
+                                    import_type = .CONST;
+                                },
+                                .keyword_private => {
+                                    self.commitLookahead();
+                                    private = true;
+                                },
+                                .keyword_exportas => {
+                                    self.commitLookahead();
+                                    export_as_tok = try self.expectToken(.identifier);
+                                },
+                                else => break,
+                            }
+                        }
+
+                        const name = if (name_needs_underscore)
+                            try std.mem.concat(arena, u8, &.{ "_", name_tok.slice(self.tokenizer.source) })
+                        else
+                            try arena.dupe(u8, name_tok.slice(self.tokenizer.source));
+
+                        const ext_name: ?[]const u8 = if (ext_name_tok) |ext_name| if (name_needs_underscore)
+                            try std.mem.concat(arena, u8, &.{ "_", ext_name.slice(self.tokenizer.source) })
+                        else
+                            try arena.dupe(u8, ext_name.slice(self.tokenizer.source)) else null;
+
+                        try module.exports.append(arena, .{
+                            .name = name,
+                            .mangled_symbol_name = null,
+                            .ext_name = ext_name,
+                            .import_name = if (import_name_tok) |imp_name| try arena.dupe(u8, imp_name.slice(self.tokenizer.source)) else null,
+                            .export_as = if (export_as_tok) |export_as| try arena.dupe(u8, export_as.slice(self.tokenizer.source)) else null,
+                            .no_name = no_name,
+                            .ordinal = ordinal orelse 0,
+                            .type = import_type,
+                            .private = private,
+                        });
+                    }
+                },
+                .keyword_heapsize,
+                .keyword_stacksize,
+                .keyword_version,
+                => return self.unimplemented(tok),
+                else => {
+                    self.diagnostics.* = .{
+                        .err = .unknown_statement,
+                        .token = tok,
+                    };
+                    return error.ParseError;
+                },
+            }
+        }
+        return module;
+    }
+
+    fn isDecorated(symbol: []const u8, module_definition_type: ModuleDefinitionType) bool {
+        // In def files, the symbols can either be listed decorated or undecorated.
+        //
+        // - For cdecl symbols, only the undecorated form is allowed.
+        // - For fastcall and vectorcall symbols, both fully decorated or
+        //   undecorated forms can be present.
+        // - For stdcall symbols in non-MinGW environments, the decorated form is
+        //   fully decorated with leading underscore and trailing stack argument
+        //   size - like "_Func@0".
+        // - In MinGW def files, a decorated stdcall symbol does not include the
+        //   leading underscore though, like "Func@0".
+
+        // This function controls whether a leading underscore should be added to
+        // the given symbol name or not. For MinGW, treat a stdcall symbol name such
+        // as "Func@0" as undecorated, i.e. a leading underscore must be added.
+        // For non-MinGW, look for '@' in the whole string and consider "_Func@0"
+        // as decorated, i.e. don't add any more leading underscores.
+        // We can't check for a leading underscore here, since function names
+        // themselves can start with an underscore, while a second one still needs
+        // to be added.
+        if (std.mem.startsWith(u8, symbol, "@")) return true;
+        if (std.mem.indexOf(u8, symbol, "@@") != null) return true;
+        if (std.mem.startsWith(u8, symbol, "?")) return true;
+        if (module_definition_type != .mingw and std.mem.indexOfScalar(u8, symbol, '@') != null) return true;
+        return false;
+    }
+
+    fn expectInteger(self: *Parser, T: type) Error!T {
+        const tok = try self.nextToken();
+        blk: {
+            if (tok.tag != .identifier) break :blk;
+            return std.fmt.parseUnsigned(T, tok.slice(self.tokenizer.source), 0) catch break :blk;
+        }
+        self.diagnostics.* = .{
+            .err = .expected_integer,
+            .token = tok,
+        };
+        return error.ParseError;
+    }
+
+    fn unimplemented(self: *Parser, tok: Token) Error {
+        self.diagnostics.* = .{
+            .err = .unimplemented,
+            .token = tok,
+        };
+        return error.ParseError;
+    }
+
+    fn expectToken(self: *Parser, tag: Token.Tag) Error!Token {
+        const tok = try self.nextToken();
+        if (tok.tag != tag) {
+            self.diagnostics.* = .{
+                .err = .expected_token,
+                .token = tok,
+                .extra = .{ .expected = tag },
+            };
+            return error.ParseError;
+        }
+        return tok;
+    }
+
+    fn nextToken(self: *Parser) Error!Token {
+        return self.nextFromTokenizer(self.tokenizer);
+    }
+
+    fn lookaheadToken(self: *Parser) Error!Token {
+        self.lookahead_tokenizer = self.tokenizer.*;
+        return self.nextFromTokenizer(&self.lookahead_tokenizer);
+    }
+
+    fn commitLookahead(self: *Parser) void {
+        self.tokenizer.* = self.lookahead_tokenizer;
+    }
+
+    fn nextFromTokenizer(
+        self: *Parser,
+        tokenizer: *Tokenizer,
+    ) Error!Token {
+        return tokenizer.next() catch |err| {
+            self.diagnostics.* = .{
+                .err = switch (err) {
+                    error.InvalidByte => .invalid_byte,
+                    error.UnfinishedQuotedIdentifier => .unfinished_quoted_identifier,
+                },
+                .token = tokenizer.error_context_token.?,
+            };
+            return error.ParseError;
+        };
+    }
+};
+
+test parse {
+    const source =
+        \\LIBRARY "foo"
+        \\; hello
+        \\EXPORTS
+        \\foo @ 10
+        \\bar @104
+        \\baz@4
+        \\foo == bar
+        \\alias = function
+        \\
+        \\data DATA
+        \\constant CONSTANT
+        \\
+    ;
+
+    try testParse(.AMD64, source, "foo.dll", &[_]ModuleDefinition.Export{
+        .{
+            .name = "foo",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 10,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "bar",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 104,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "baz@4",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "foo",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = "bar",
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "function",
+            .mangled_symbol_name = null,
+            .ext_name = "alias",
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "data",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .DATA,
+            .private = false,
+        },
+        .{
+            .name = "constant",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CONST,
+            .private = false,
+        },
+    });
+
+    try testParse(.I386, source, "foo.dll", &[_]ModuleDefinition.Export{
+        .{
+            .name = "_foo",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 10,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "_bar",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 104,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "_baz@4",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "_foo",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = "bar",
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "_function",
+            .mangled_symbol_name = null,
+            .ext_name = "_alias",
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "_data",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .DATA,
+            .private = false,
+        },
+        .{
+            .name = "_constant",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CONST,
+            .private = false,
+        },
+    });
+
+    try testParse(.ARMNT, source, "foo.dll", &[_]ModuleDefinition.Export{
+        .{
+            .name = "foo",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 10,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "bar",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 104,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "baz@4",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "foo",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = "bar",
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "function",
+            .mangled_symbol_name = null,
+            .ext_name = "alias",
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "data",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .DATA,
+            .private = false,
+        },
+        .{
+            .name = "constant",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CONST,
+            .private = false,
+        },
+    });
+
+    try testParse(.ARM64, source, "foo.dll", &[_]ModuleDefinition.Export{
+        .{
+            .name = "foo",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 10,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "bar",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 104,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "baz@4",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "foo",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = "bar",
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "function",
+            .mangled_symbol_name = null,
+            .ext_name = "alias",
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "data",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .DATA,
+            .private = false,
+        },
+        .{
+            .name = "constant",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CONST,
+            .private = false,
+        },
+    });
+}
+
+test "ntdll" {
+    const source =
+        \\;
+        \\; Definition file of ntdll.dll
+        \\; Automatic generated by gendef
+        \\; written by Kai Tietz 2008
+        \\;
+        \\LIBRARY "ntdll.dll"
+        \\EXPORTS
+        \\RtlDispatchAPC@12
+        \\RtlActivateActivationContextUnsafeFast@0
+    ;
+
+    try testParse(.AMD64, source, "ntdll.dll", &[_]ModuleDefinition.Export{
+        .{
+            .name = "RtlDispatchAPC@12",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+        .{
+            .name = "RtlActivateActivationContextUnsafeFast@0",
+            .mangled_symbol_name = null,
+            .ext_name = null,
+            .import_name = null,
+            .export_as = null,
+            .no_name = false,
+            .ordinal = 0,
+            .type = .CODE,
+            .private = false,
+        },
+    });
+}
+
+fn testParse(machine_type: std.coff.IMAGE.FILE.MACHINE, source: [:0]const u8, expected_module_name: []const u8, expected_exports: []const ModuleDefinition.Export) !void {
+    var diagnostics: Diagnostics = undefined;
+    const module = parse(std.testing.allocator, source, machine_type, .mingw, &diagnostics) catch |err| switch (err) {
+        error.OutOfMemory => |e| return e,
+        error.ParseError => {
+            const stderr = std.debug.lockStderrWriter(&.{});
+            defer std.debug.unlockStderrWriter();
+            try diagnostics.writeMsg(stderr, source);
+            try stderr.writeByte('\n');
+            return err;
+        },
+    };
+    defer module.deinit();
+
+    try std.testing.expectEqualStrings(expected_module_name, module.name orelse "");
+    try std.testing.expectEqual(expected_exports.len, module.exports.items.len);
+    for (expected_exports, module.exports.items) |expected, actual| {
+        try std.testing.expectEqualStrings(expected.name, actual.name);
+        try std.testing.expectEqualStrings(expected.export_as orelse "", actual.export_as orelse "");
+        try std.testing.expectEqualStrings(expected.ext_name orelse "", actual.ext_name orelse "");
+        try std.testing.expectEqualStrings(expected.import_name orelse "", actual.import_name orelse "");
+        try std.testing.expectEqualStrings(expected.mangled_symbol_name orelse "", actual.mangled_symbol_name orelse "");
+        try std.testing.expectEqual(expected.ordinal, actual.ordinal);
+        try std.testing.expectEqual(expected.no_name, actual.no_name);
+        try std.testing.expectEqual(expected.private, actual.private);
+        try std.testing.expectEqual(expected.type, actual.type);
+    }
+}
+
+test "parse errors" {
+    for (&[_]std.coff.IMAGE.FILE.MACHINE{ .AMD64, .I386, .ARMNT, .ARM64 }) |machine_type| {
+        try testParseErrorMsg("invalid byte '\\x00'", machine_type, "LIBRARY \x00");
+        try testParseErrorMsg("unfinished quoted identifier at '<eof>', expected closing '\"'", machine_type, "LIBRARY \"foo");
+        try testParseErrorMsg("expected '=', got 'foo'", machine_type, "LIBRARY foo BASE foo");
+        try testParseErrorMsg("expected integer, got 'foo'", machine_type, "EXPORTS foo @ foo");
+        try testParseErrorMsg("support for 'HEAPSIZE' has not yet been implemented", machine_type, "HEAPSIZE");
+        try testParseErrorMsg("unknown/invalid statement syntax beginning with 'LIB'", machine_type, "LIB");
+    }
+}
+
+fn testParseErrorMsg(expected_msg: []const u8, machine_type: std.coff.IMAGE.FILE.MACHINE, source: [:0]const u8) !void {
+    var diagnostics: Diagnostics = undefined;
+    _ = parse(std.testing.allocator, source, machine_type, .mingw, &diagnostics) catch |err| switch (err) {
+        error.OutOfMemory => |e| return e,
+        error.ParseError => {
+            var buf: [256]u8 = undefined;
+            var writer: std.Io.Writer = .fixed(&buf);
+            try diagnostics.writeMsg(&writer, source);
+            try std.testing.expectEqualStrings(expected_msg, writer.buffered());
+            return;
+        },
+    };
+    return error.UnexpectedSuccess;
+}
src/libs/mingw/implib.zig
@@ -0,0 +1,1088 @@
+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 = "!<arch>\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 },
+        );
+    }
+};
src/libs/mingw.zig
@@ -10,6 +10,13 @@ const Compilation = @import("../Compilation.zig");
 const build_options = @import("build_options");
 const Cache = std.Build.Cache;
 const dev = @import("../dev.zig");
+const def = @import("mingw/def.zig");
+const implib = @import("mingw/implib.zig");
+
+test {
+    _ = def;
+    _ = implib;
+}
 
 pub const CrtFile = enum {
     crt2_o,
@@ -290,11 +297,6 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void {
     var o_dir = try comp.dirs.global_cache.handle.makeOpenPath(o_sub_path, .{});
     defer o_dir.close();
 
-    const final_def_basename = try std.fmt.allocPrint(arena, "{s}.def", .{lib_name});
-    const def_final_path = try comp.dirs.global_cache.join(arena, &[_][]const u8{
-        "o", &digest, final_def_basename,
-    });
-
     const aro = @import("aro");
     var diagnostics: aro.Diagnostics = .{
         .output = .{ .to_list = .{ .arena = .init(gpa) } },
@@ -312,7 +314,6 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void {
         defer std.debug.unlockStderrWriter();
         nosuspend stderr.print("def file: {s}\n", .{def_file_path}) catch break :print;
         nosuspend stderr.print("include dir: {s}\n", .{include_dir}) catch break :print;
-        nosuspend stderr.print("output path: {s}\n", .{def_final_path}) catch break :print;
     }
 
     try aro_comp.include_dirs.append(gpa, include_dir);
@@ -339,32 +340,46 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void {
         }
     }
 
-    {
-        // new scope to ensure definition file is written before passing the path to WriteImportLibrary
-        const def_final_file = try o_dir.createFile(final_def_basename, .{ .truncate = true });
-        defer def_final_file.close();
-        var buffer: [1024]u8 = undefined;
-        var file_writer = def_final_file.writer(&buffer);
-        try pp.prettyPrintTokens(&file_writer.interface, .result_only);
-        try file_writer.interface.flush();
-    }
+    const members = members: {
+        var aw: std.Io.Writer.Allocating = .init(gpa);
+        errdefer aw.deinit();
+        try pp.prettyPrintTokens(&aw.writer, .result_only);
+
+        const input = try aw.toOwnedSliceSentinel(0);
+        defer gpa.free(input);
+
+        const machine_type = target.toCoffMachine();
+        var def_diagnostics: def.Diagnostics = undefined;
+        var module_def = def.parse(gpa, input, machine_type, .mingw, &def_diagnostics) catch |err| switch (err) {
+            error.OutOfMemory => |e| return e,
+            error.ParseError => {
+                var buffer: [64]u8 = undefined;
+                const w = std.debug.lockStderrWriter(&buffer);
+                defer std.debug.unlockStderrWriter();
+                try w.writeAll("error: ");
+                try def_diagnostics.writeMsg(w, input);
+                try w.writeByte('\n');
+                return error.WritingImportLibFailed;
+            },
+        };
+        defer module_def.deinit();
+
+        module_def.fixupForImportLibraryGeneration(machine_type);
+
+        break :members try implib.getMembers(gpa, module_def, machine_type);
+    };
+    defer members.deinit();
 
     const lib_final_path = try std.fs.path.join(gpa, &.{ "o", &digest, final_lib_basename });
     errdefer gpa.free(lib_final_path);
 
-    if (!build_options.have_llvm) return error.ZigCompilerNotBuiltWithLLVMExtensions;
-    const llvm_bindings = @import("../codegen/llvm/bindings.zig");
-    const def_final_path_z = try arena.dupeZ(u8, def_final_path);
-    const lib_final_path_z = try comp.dirs.global_cache.joinZ(arena, &.{lib_final_path});
-    if (llvm_bindings.WriteImportLibrary(
-        def_final_path_z.ptr,
-        @intFromEnum(target.toCoffMachine()),
-        lib_final_path_z.ptr,
-        true,
-    )) {
-        // TODO surface a proper error here
-        log.err("unable to turn {s}.def into {s}.lib", .{ lib_name, lib_name });
-        return error.WritingImportLibFailed;
+    {
+        const lib_final_file = try o_dir.createFile(final_lib_basename, .{ .truncate = true });
+        defer lib_final_file.close();
+        var buffer: [1024]u8 = undefined;
+        var file_writer = lib_final_file.writer(&buffer);
+        try implib.writeCoffArchive(gpa, &file_writer.interface, members);
+        try file_writer.interface.flush();
     }
 
     man.writeManifest() catch |err| {
src/zig_llvm.cpp
@@ -39,9 +39,6 @@
 #include <llvm/Passes/StandardInstrumentations.h>
 #include <llvm/Object/Archive.h>
 #include <llvm/Object/ArchiveWriter.h>
-#include <llvm/Object/COFF.h>
-#include <llvm/Object/COFFImportFile.h>
-#include <llvm/Object/COFFModuleDefinition.h>
 #include <llvm/PassRegistry.h>
 #include <llvm/Support/CommandLine.h>
 #include <llvm/Support/FileSystem.h>
@@ -475,62 +472,6 @@ void ZigLLVMParseCommandLineOptions(size_t argc, const char *const *argv) {
     cl::ParseCommandLineOptions(argc, argv);
 }
 
-bool ZigLLVMWriteImportLibrary(const char *def_path, unsigned int coff_machine,
-    const char *output_lib_path, bool kill_at)
-{
-    COFF::MachineTypes machine = static_cast<COFF::MachineTypes>(coff_machine);
-
-    auto bufOrErr = MemoryBuffer::getFile(def_path);
-    if (!bufOrErr) {
-        return false;
-    }
-
-    MemoryBuffer& buf = *bufOrErr.get();
-    Expected<object::COFFModuleDefinition> def =
-        object::parseCOFFModuleDefinition(buf, machine, /* MingwDef */ true);
-
-    if (!def) {
-        return true;
-    }
-
-    // The exports-juggling code below is ripped from LLVM's DlltoolDriver.cpp
-
-    // If ExtName is set (if the "ExtName = Name" syntax was used), overwrite
-    // Name with ExtName and clear ExtName. When only creating an import
-    // library and not linking, the internal name is irrelevant. This avoids
-    // cases where writeImportLibrary tries to transplant decoration from
-    // symbol decoration onto ExtName.
-    for (object::COFFShortExport& E : def->Exports) {
-        if (!E.ExtName.empty()) {
-            E.Name = E.ExtName;
-            E.ExtName.clear();
-        }
-    }
-
-    if (kill_at) {
-        for (object::COFFShortExport& E : def->Exports) {
-            if (!E.ImportName.empty() || (!E.Name.empty() && E.Name[0] == '?'))
-                continue;
-            if (machine == COFF::IMAGE_FILE_MACHINE_I386) {
-                // By making sure E.SymbolName != E.Name for decorated symbols,
-                // writeImportLibrary writes these symbols with the type
-                // IMPORT_NAME_UNDECORATE.
-                E.SymbolName = E.Name;
-            }
-            // Trim off the trailing decoration. Symbols will always have a
-            // starting prefix here (either _ for cdecl/stdcall, @ for fastcall
-            // or ? for C++ functions). Vectorcall functions won't have any
-            // fixed prefix, but the function base name will still be at least
-            // one char.
-            E.Name = E.Name.substr(0, E.Name.find('@', 1));
-        }
-    }
-
-    return static_cast<bool>(
-        object::writeImportLibrary(def->OutputFile, output_lib_path,
-                                   def->Exports, machine, /* MinGW */ true));
-}
-
 bool ZigLLVMWriteArchive(const char *archive_name, const char **file_names, size_t file_name_count,
     ZigLLVMArchiveKind archive_kind)
 {
src/zig_llvm.h
@@ -124,7 +124,4 @@ ZIG_EXTERN_C bool ZigLLDLinkWasm(int argc, const char **argv, bool can_exit_earl
 ZIG_EXTERN_C bool ZigLLVMWriteArchive(const char *archive_name, const char **file_names, size_t file_name_count,
     ZigLLVMArchiveKind archive_kind);
 
-ZIG_EXTERN_C bool ZigLLVMWriteImportLibrary(const char *def_path, unsigned int coff_machine,
-    const char *output_lib_path, bool kill_at);
-
 #endif