rust-lang/rust · #160679

Staticlib rename internal symbols: add COFF support

cezarbbb · merged Sep 21, 20264 files · 383 + / 17
compiler/rustc_codegen_ssa/src/back/link.rs27 + / 15
@@ -834,33 +834,45 @@ 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     };      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);
compiler/rustc_codegen_ssa/src/back/symbol_edit.rs155 + / 1
@@ -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>,@@ -441,6 +476,125 @@ fn macho_rebuild_strtab(     Some(result) } +// ---------------------------------------------------------------------------+// 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 // ---------------------------------------------------------------------------
compiler/rustc_codegen_ssa/src/diagnostics.rs1 + / 1
@@ -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,
tests/run-make/staticlib-rename-internal-symbols-coff/rmake.rsadded200 + / 0
@@ -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;+        }+    }+}