Add COFF support to -Zstaticlib-rename-internal-symbols
diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs
index c283e46..221998a 100644
--- a/compiler/rustc_codegen_ssa/src/back/link.rs
+++ b/compiler/rustc_codegen_ssa/src/back/link.rs
@@ -834,24 +834,33 @@ fn link_staticlib(
     let hide = sess.opts.unstable_opts.staticlib_hide_internal_symbols;
     let rename = sess.opts.unstable_opts.staticlib_rename_internal_symbols;
 
+    let hide_supported =
+        matches!(sess.target.binary_format, BinaryFormat::Elf | BinaryFormat::MachO);
+    // Rename only rewrites symbol names, so it also works on COFF; hide
+    // needs a visibility concept COFF lacks.
+    let rename_supported = matches!(
+        sess.target.binary_format,
+        BinaryFormat::Elf | BinaryFormat::MachO | BinaryFormat::Coff
+    );
+
     let exported_symbols = if hide || rename {
-        if !matches!(sess.target.binary_format, BinaryFormat::Elf | BinaryFormat::MachO) {
-            if hide {
-                sess.dcx().emit_warn(diagnostics::StaticlibHideInternalSymbolsUnsupported {
-                    binary_format: sess.target.archive_format.to_string(),
-                });
-            }
-            if rename {
-                sess.dcx().emit_warn(diagnostics::StaticlibRenameInternalSymbolsUnsupported {
-                    binary_format: sess.target.archive_format.to_string(),
-                });
-            }
-            None
-        } else {
+        if hide && !hide_supported {
+            sess.dcx().emit_warn(diagnostics::StaticlibHideInternalSymbolsUnsupported {
+                binary_format: sess.target.archive_format.to_string(),
+            });
+        }
+        if rename && !rename_supported {
+            sess.dcx().emit_warn(diagnostics::StaticlibRenameInternalSymbolsUnsupported {
+                binary_format: sess.target.archive_format.to_string(),
+            });
+        }
+        if (hide && hide_supported) || (rename && rename_supported) {
             crate_info
                 .exported_symbols
                 .get(&CrateType::StaticLib)
                 .map(|symbols| symbols.iter().map(|symbol| symbol.name.clone()).collect())
+        } else {
+            None
         }
     } else {
         None
@@ -859,8 +868,11 @@ fn link_staticlib(
 
     let symbols = exported_symbols.map(|exported| ArchiveSymbols {
         exported,
-        rename_suffix: rename.then(|| crate_info.symbol_rename_suffix.clone()),
-        hide,
+        rename_suffix: (rename && rename_supported)
+            .then(|| crate_info.symbol_rename_suffix.clone()),
+        // A warning was already emitted above if hiding was requested for an
+        // unsupported format; don't also ask the backend to hide there.
+        hide: hide && hide_supported,
     });
 
     ab.build(out_filename, symbols);
diff --git a/compiler/rustc_codegen_ssa/src/back/symbol_edit.rs b/compiler/rustc_codegen_ssa/src/back/symbol_edit.rs
index ef80082..490c634 100644
--- a/compiler/rustc_codegen_ssa/src/back/symbol_edit.rs
+++ b/compiler/rustc_codegen_ssa/src/back/symbol_edit.rs
@@ -7,9 +7,10 @@
 use std::borrow::Cow;
 use std::mem;
 
+use object::read::coff::{CoffHeader, ImageSymbol as _};
 use object::read::elf::{SectionHeader as _, Sym as _};
 use object::read::macho::Nlist;
-use object::{Endianness, elf, macho};
+use object::{Endianness, elf, macho, pe};
 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
 
 struct Patch {
@@ -57,6 +58,20 @@ pub(super) fn apply_edits<'a>(
             rename,
             mem::offset_of!(macho::Nlist32<Endianness>, n_type),
         ),
+        Some(object::File::Coff(f)) => coff_edit_impl(
+            data,
+            f.coff_header(),
+            hide,
+            rename,
+            coff_strip_underscore(f.coff_header()),
+        ),
+        Some(object::File::CoffBig(f)) => coff_edit_impl(
+            data,
+            f.coff_header(),
+            hide,
+            rename,
+            coff_strip_underscore(f.coff_header()),
+        ),
         _ => None,
     };
     match result {
@@ -84,10 +99,30 @@ pub(super) fn collect_internal_names(
         object::File::MachO32(_) => {
             macho_collect_impl::<macho::MachHeader32<Endianness>>(data, exported, out)
         }
+        object::File::Coff(f) => coff_collect_impl(
+            data,
+            f.coff_header(),
+            exported,
+            out,
+            coff_strip_underscore(f.coff_header()),
+        ),
+        object::File::CoffBig(f) => coff_collect_impl(
+            data,
+            f.coff_header(),
+            exported,
+            out,
+            coff_strip_underscore(f.coff_header()),
+        ),
         _ => {}
     }
 }
 
+/// Whether this machine's COFF ABI decorates external symbols with a leading `_`
+/// (i686 only).
+fn coff_strip_underscore(header: &impl CoffHeader) -> bool {
+    header.machine() == pe::IMAGE_FILE_MACHINE_I386
+}
+
 fn elf_collect_impl<Elf: object::read::elf::FileHeader<Endian = Endianness>>(
     data: &[u8],
     exported: &FxHashSet<String>,
@@ -442,6 +477,125 @@ fn macho_rebuild_strtab(
 }
 
 // ---------------------------------------------------------------------------
+// COFF: single-pass collection + apply
+// ---------------------------------------------------------------------------
+
+fn coff_collect_impl<'data, Coff: CoffHeader>(
+    data: &'data [u8],
+    header: &'data Coff,
+    exported: &FxHashSet<String>,
+    out: &mut FxHashSet<String>,
+    strip_underscore: bool,
+) {
+    let Ok(symbols) = header.symbols(data) else { return };
+    let strings = symbols.strings();
+
+    for (_index, sym) in symbols.iter() {
+        let sclass = sym.storage_class();
+        if sclass != pe::IMAGE_SYM_CLASS_EXTERNAL && sclass != pe::IMAGE_SYM_CLASS_WEAK_EXTERNAL {
+            continue;
+        }
+        if sym.section_number() <= 0 {
+            continue;
+        }
+        let Ok(name_bytes) = sym.name(strings) else { continue };
+        let Ok(mut name) = str::from_utf8(name_bytes).map(String::from) else { continue };
+        if strip_underscore {
+            name = name.strip_prefix('_').unwrap_or(&name).to_string();
+        }
+        if !exported.contains(&name) {
+            out.insert(name);
+        }
+    }
+}
+
+fn coff_edit_impl<'data, Coff: CoffHeader>(
+    data: &'data [u8],
+    header: &'data Coff,
+    hide: bool,
+    rename: Option<&(FxHashSet<String>, &str)>,
+    strip_underscore: bool,
+) -> Option<Vec<u8>> {
+    // COFF has no visibility concept, so hiding is unsupported; the caller
+    // has already warned and does not request it.
+    assert!(!hide);
+
+    let pointer_to_symbol_table = header.pointer_to_symbol_table() as usize;
+    let number_of_symbols = header.number_of_symbols() as usize;
+    // ImageSymbol is 18 bytes; ImageSymbolEx (bigobj) is 20.
+    let sym_size = mem::size_of::<Coff::ImageSymbolBytes>();
+
+    let symbol_bytes_offset = pointer_to_symbol_table;
+    let strtab_base = pointer_to_symbol_table + number_of_symbols * sym_size;
+    if strtab_base > data.len() {
+        return None;
+    }
+
+    if data.len() < strtab_base + 4 {
+        return None;
+    }
+    let old_len =
+        u32::from_le_bytes(data[strtab_base..strtab_base + 4].try_into().unwrap()) as usize;
+    if strtab_base + old_len > data.len() {
+        return None;
+    }
+
+    let Ok(symbols) = header.symbols(data) else { return None };
+    let strings = symbols.strings();
+
+    let mut renames = Vec::new();
+    for (index, sym) in symbols.iter() {
+        let sclass = sym.storage_class();
+        if sclass != pe::IMAGE_SYM_CLASS_EXTERNAL && sclass != pe::IMAGE_SYM_CLASS_WEAK_EXTERNAL {
+            continue;
+        }
+        let Ok(name_bytes) = sym.name(strings) else { continue };
+        let Ok(name) = str::from_utf8(name_bytes) else { continue };
+        let check_name =
+            if strip_underscore { name.strip_prefix('_').unwrap_or(name) } else { name };
+        if rename.is_some_and(|(rename_set, _)| rename_set.contains(check_name)) {
+            renames.push(RenameEntry {
+                name_field_offset: symbol_bytes_offset + index.0 * sym_size,
+                name: name.to_string(),
+            });
+        }
+    }
+    if renames.is_empty() {
+        return None;
+    }
+    let suffix = rename.unwrap().1;
+
+    let mut new_strtab = Vec::new();
+    let mut map: FxHashMap<String, u32> = FxHashMap::default();
+    let mut sorted_names: Vec<&str> = renames.iter().map(|r| r.name.as_str()).collect();
+    sorted_names.sort();
+    sorted_names.dedup();
+    for name in &sorted_names {
+        let rel_offset = (old_len + new_strtab.len()) as u32;
+        new_strtab.extend_from_slice(name.as_bytes());
+        new_strtab.extend_from_slice(suffix.as_bytes());
+        new_strtab.push(0);
+        map.insert(name.to_string(), rel_offset);
+    }
+
+    let mut result = data.to_vec();
+    let new_len = (old_len + new_strtab.len()) as u32;
+    result[strtab_base..strtab_base + 4].copy_from_slice(&new_len.to_le_bytes());
+    result.extend_from_slice(&new_strtab);
+
+    // Long names are stored as all-zero name[0..4] plus the string table
+    // offset in name[4..8].
+    for r in &renames {
+        let rel = map[&r.name];
+        let field = &mut result[r.name_field_offset..r.name_field_offset + 8];
+        field[..4].fill(0);
+        field[4..].copy_from_slice(&rel.to_le_bytes());
+    }
+
+    Some(result)
+}
+
+// ---------------------------------------------------------------------------
 // Shared helpers
 // ---------------------------------------------------------------------------
 
diff --git a/compiler/rustc_codegen_ssa/src/diagnostics.rs b/compiler/rustc_codegen_ssa/src/diagnostics.rs
index 8aa2da9..0e03a59 100644
--- a/compiler/rustc_codegen_ssa/src/diagnostics.rs
+++ b/compiler/rustc_codegen_ssa/src/diagnostics.rs
@@ -695,7 +695,7 @@ pub(crate) struct StaticlibHideInternalSymbolsUnsupported {
 
 #[derive(Diagnostic)]
 #[diag(
-    "-Zstaticlib-rename-internal-symbols only supports ELF and Mach-O targets, but the target uses `{$binary_format}`"
+    "-Zstaticlib-rename-internal-symbols only supports ELF, Mach-O, and COFF targets, but the target uses `{$binary_format}`"
 )]
 pub(crate) struct StaticlibRenameInternalSymbolsUnsupported {
     pub binary_format: String,
diff --git a/tests/run-make/staticlib-rename-internal-symbols-coff/rmake.rs b/tests/run-make/staticlib-rename-internal-symbols-coff/rmake.rs
new file mode 100644
index 0000000..38cf724
--- /dev/null
+++ b/tests/run-make/staticlib-rename-internal-symbols-coff/rmake.rs
@@ -0,0 +1,200 @@
+//@ only-windows
+//@ ignore-cross-compile
+
+use std::collections::HashSet;
+
+use run_make_support::object::read::archive::ArchiveFile;
+use run_make_support::object::read::coff::ImageSymbol as _;
+use run_make_support::object::{File, pe};
+use run_make_support::path_helpers::source_root;
+use run_make_support::{cc, extra_c_flags, rfs, run, rustc, static_lib_name};
+
+const EXPORTED: &[&str] = &["my_add", "my_hash_lookup", "call_internal", "my_safe_div"];
+
+fn main() {
+    let hide_sibling = source_root().join("tests/run-make/staticlib-hide-internal-symbols");
+    let rename_sibling = source_root().join("tests/run-make/staticlib-rename-internal-symbols");
+    rfs::copy(hide_sibling.join("lib.rs"), "lib.rs");
+    rfs::copy(hide_sibling.join("main.c"), "main.c");
+    rfs::copy(rename_sibling.join("liba.rs"), "liba.rs");
+    rfs::copy(rename_sibling.join("libb.rs"), "libb.rs");
+    rfs::copy(rename_sibling.join("dual_main.c"), "dual_main.c");
+
+    test_basic_functionality();
+    test_rs_suffix_present();
+    test_dual_staticlib_linking();
+    test_hide_and_rename();
+}
+
+fn test_basic_functionality() {
+    let lib_name = static_lib_name("lib");
+
+    rustc()
+        .input("lib.rs")
+        .crate_type("staticlib")
+        .arg("-Zstaticlib-rename-internal-symbols")
+        .opt()
+        .run();
+
+    cc().input("main.c").input(&lib_name).out_exe("main").args(extra_c_flags()).run();
+    run("main");
+
+    rfs::remove_file(&lib_name);
+}
+
+fn test_rs_suffix_present() {
+    let lib_name = static_lib_name("lib");
+
+    rustc()
+        .input("lib.rs")
+        .crate_type("staticlib")
+        .arg("-Zstaticlib-rename-internal-symbols")
+        .opt()
+        .run();
+
+    let data = rfs::read(&lib_name);
+    check_rename_symbols(&data);
+
+    rfs::remove_file(&lib_name);
+}
+
+fn test_dual_staticlib_linking() {
+    let liba_name = static_lib_name("liba");
+    let libb_name = static_lib_name("libb");
+
+    rustc()
+        .input("liba.rs")
+        .crate_type("staticlib")
+        .arg("-Zstaticlib-rename-internal-symbols")
+        .opt()
+        .run();
+
+    rustc()
+        .input("libb.rs")
+        .crate_type("staticlib")
+        .arg("-Zstaticlib-rename-internal-symbols")
+        .opt()
+        .run();
+
+    cc().input("dual_main.c")
+        .input(&liba_name)
+        .input(&libb_name)
+        .out_exe("dual_main")
+        .args(extra_c_flags())
+        .run();
+    run("dual_main");
+}
+
+/// On COFF, hiding is unsupported and must only produce a warning, while
+/// renaming still applies.
+fn test_hide_and_rename() {
+    let lib_name = static_lib_name("lib");
+
+    rustc()
+        .input("lib.rs")
+        .crate_type("staticlib")
+        .arg("-Zstaticlib-hide-internal-symbols")
+        .arg("-Zstaticlib-rename-internal-symbols")
+        .opt()
+        .run_unchecked()
+        .assert_stderr_contains(
+            "-Zstaticlib-hide-internal-symbols only supports ELF and Mach-O targets",
+        )
+        .assert_exit_code(0);
+
+    let data = rfs::read(&lib_name);
+    check_rename_symbols(&data);
+
+    cc().input("main.c").input(&lib_name).out_exe("main").args(extra_c_flags()).run();
+    run("main");
+
+    rfs::remove_file(&lib_name);
+}
+
+fn check_rename_symbols(archive_data: &[u8]) {
+    let archive = ArchiveFile::parse(archive_data).unwrap();
+    let mut found_exported = HashSet::new();
+    let mut found_rs_suffix = false;
+
+    for member in archive.members() {
+        let member = member.unwrap();
+        if !member.name().ends_with(b".rcgu.o") {
+            continue;
+        }
+        // COFF header/symbol types have alignment 1, so odd-offset members
+        // parse directly from the borrowed slice.
+        let data = member.data(archive_data).unwrap();
+        match File::parse(data) {
+            Ok(File::Coff(f)) => {
+                check_coff_symbols(f.coff_header(), data, &mut found_exported, &mut found_rs_suffix)
+            }
+            Ok(File::CoffBig(f)) => {
+                check_coff_symbols(f.coff_header(), data, &mut found_exported, &mut found_rs_suffix)
+            }
+            Ok(_) => panic!("unexpected object file format in archive member"),
+            Err(e) => panic!("failed to parse archive member: {e}"),
+        }
+    }
+
+    assert!(found_rs_suffix, "expected to find at least one renamed symbol with .rs suffix");
+    for expected in EXPORTED {
+        assert!(
+            found_exported.contains(*expected),
+            "expected to find exported symbol `{expected}` in archive"
+        );
+    }
+}
+
+fn check_coff_symbols<Coff: run_make_support::object::read::coff::CoffHeader>(
+    header: &Coff,
+    data: &[u8],
+    found_exported: &mut HashSet<String>,
+    found_rs_suffix: &mut bool,
+) {
+    // ImageSymbol is 18 bytes; ImageSymbolEx (bigobj) is 20.
+    let sym_size = std::mem::size_of::<Coff::ImageSymbolBytes>();
+    // i686 decorates symbol names with a leading underscore.
+    let strip_underscore = header.machine() == pe::IMAGE_FILE_MACHINE_I386;
+    let Ok(symbols) = header.symbols(data) else { return };
+    let strings = symbols.strings();
+    let symtab_base = header.pointer_to_symbol_table() as usize;
+
+    for (index, symbol) in symbols.iter() {
+        let storage_class = symbol.storage_class();
+        if storage_class != pe::IMAGE_SYM_CLASS_EXTERNAL
+            && storage_class != pe::IMAGE_SYM_CLASS_WEAK_EXTERNAL
+        {
+            continue;
+        }
+        if symbol.section_number() <= 0 {
+            continue;
+        }
+        // String-table references keep all four leading name bytes zero.
+        let name_field = symtab_base + index.0 * sym_size;
+        if data[name_field] == 0 {
+            assert!(
+                data[name_field + 1..name_field + 4] == [0, 0, 0],
+                "long-name symbol reference at offset {name_field} has non-zero padding bytes"
+            );
+        }
+        let Ok(name_bytes) = symbol.name(strings) else { continue };
+        let Ok(mut name) = str::from_utf8(name_bytes).map(String::from) else { continue };
+        if strip_underscore {
+            name = name.strip_prefix('_').unwrap_or(&name).to_string();
+        }
+
+        if EXPORTED.contains(&name.as_str()) {
+            assert!(
+                !name.contains(".rs"),
+                "exported symbol `{name}` should not contain .rs suffix"
+            );
+            found_exported.insert(name);
+        } else {
+            assert!(
+                name.contains(".rs"),
+                "internal symbol `{name}` should contain .rs suffix after rename"
+            );
+            *found_rs_suffix = true;
+        }
+    }
+}