rust-lang/rust · #160679
Staticlib rename internal symbols: add COFF support
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;+ }+ }+}