Skip to content

Commit

Permalink
Support raw-dylib link kind on ELF
Browse files Browse the repository at this point in the history
raw-dylib is a link kind that allows rustc to link against a library
without having any library files present.
This currently only exists on Windows. rustc will take all the symbols
from raw-dylib link blocks and put them in an import library, where they
can then be resolved by the linker.

While import libraries don't exist on ELF, it would still be convenient
to have this same functionality. Not having the libraries present at
build-time can be convenient for several reasons, especially
cross-compilation. With raw-dylib, code linking against a library can be
cross-compiled without needing to have these libraries available on the
build machine. If the libc crate makes use of this, it would allow
cross-compilation without having any libc available on the build
machine. This is not yet possible with this implementation, at least
against libc's like glibc that use symbol versioning.
The raw-dylib kind could be extended with support for symbol versioning
in the future.

This implementation is very experimental and I have not tested it very
well. I have tested it for a toy example and the lz4-sys crate, where it
was able to successfully link a binary despite not having a
corresponding library at build-time.
  • Loading branch information
Noratrieb committed Jan 18, 2025
1 parent 8e59cf9 commit 35d33ec
Show file tree
Hide file tree
Showing 51 changed files with 443 additions and 122 deletions.
281 changes: 246 additions & 35 deletions compiler/rustc_codegen_ssa/src/back/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ use super::linker::{self, Linker};
use super::metadata::{MetadataPosition, create_wrapper_file};
use super::rpath::{self, RPathConfig};
use super::{apple, versioned_llvm_target};
use crate::errors::ErrorCreatingImportLibrary;
use crate::{
CodegenResults, CompiledModule, CrateInfo, NativeLib, common, errors,
looks_like_rust_object_file,
Expand Down Expand Up @@ -378,16 +379,22 @@ fn link_rlib<'a>(
}
}

for output_path in create_dll_import_libs(
sess,
archive_builder_builder,
codegen_results.crate_info.used_libraries.iter(),
tmpdir.as_ref(),
true,
) {
ab.add_archive(&output_path, Box::new(|_| false)).unwrap_or_else(|error| {
sess.dcx().emit_fatal(errors::AddNativeLibrary { library_path: output_path, error });
});
// On Windows, we add the raw-dylib import libraries to the rlibs already.
// But on ELF, this is not possible, as a shared object cannot be a member of a static library.
// Instead, we add all raw-dylibs to the final link on ELF.
if sess.target.is_like_windows {
for output_path in create_raw_dylib_dll_import_libs(
sess,
archive_builder_builder,
codegen_results.crate_info.used_libraries.iter(),
tmpdir.as_ref(),
true,
) {
ab.add_archive(&output_path, Box::new(|_| false)).unwrap_or_else(|error| {
sess.dcx()
.emit_fatal(errors::AddNativeLibrary { library_path: output_path, error });
});
}
}

if let Some(trailing_metadata) = trailing_metadata {
Expand Down Expand Up @@ -428,6 +435,12 @@ fn link_rlib<'a>(
ab
}

#[derive(Debug, PartialEq, Eq, Hash, Clone)]
struct RawDylibName {
filename: String,
library_name: Symbol,
}

/// Extract all symbols defined in raw-dylib libraries, collated by library name.
///
/// If we have multiple extern blocks that specify symbols defined in the same raw-dylib library,
Expand All @@ -437,14 +450,19 @@ fn link_rlib<'a>(
fn collate_raw_dylibs<'a>(
sess: &Session,
used_libraries: impl IntoIterator<Item = &'a NativeLib>,
) -> Vec<(String, Vec<DllImport>)> {
) -> Vec<(RawDylibName, Vec<DllImport>)> {
// Use index maps to preserve original order of imports and libraries.
let mut dylib_table = FxIndexMap::<String, FxIndexMap<Symbol, &DllImport>>::default();
let mut dylib_table = FxIndexMap::<RawDylibName, FxIndexMap<Symbol, &DllImport>>::default();

for lib in used_libraries {
if lib.kind == NativeLibKind::RawDylib {
let ext = if lib.verbatim { "" } else { ".dll" };
let name = format!("{}{}", lib.name, ext);
let ext = if sess.target.is_like_windows {
if lib.verbatim { "" } else { ".dll" }
} else {
".so"
};
let filename = format!("{}{}", lib.name, ext);
let name = RawDylibName { filename, library_name: lib.name };
let imports = dylib_table.entry(name.clone()).or_default();
for import in &lib.dll_imports {
if let Some(old_import) = imports.insert(import.name, import) {
Expand All @@ -454,7 +472,7 @@ fn collate_raw_dylibs<'a>(
sess.dcx().emit_err(errors::MultipleExternalFuncDecl {
span: import.span,
function: import.name,
library_name: &name,
library_name: &name.filename,
});
}
}
Expand All @@ -470,7 +488,7 @@ fn collate_raw_dylibs<'a>(
.collect()
}

fn create_dll_import_libs<'a>(
fn create_raw_dylib_dll_import_libs<'a>(
sess: &Session,
archive_builder_builder: &dyn ArchiveBuilderBuilder,
used_libraries: impl IntoIterator<Item = &'a NativeLib>,
Expand All @@ -481,7 +499,7 @@ fn create_dll_import_libs<'a>(
.into_iter()
.map(|(raw_dylib_name, raw_dylib_imports)| {
let name_suffix = if is_direct_dependency { "_imports" } else { "_imports_indirect" };
let output_path = tmpdir.join(format!("{raw_dylib_name}{name_suffix}.lib"));
let output_path = tmpdir.join(format!("{}{name_suffix}.lib", raw_dylib_name.filename));

let mingw_gnu_toolchain = common::is_mingw_gnu_toolchain(&sess.target);

Expand Down Expand Up @@ -520,7 +538,7 @@ fn create_dll_import_libs<'a>(

archive_builder_builder.create_dll_import_lib(
sess,
&raw_dylib_name,
&raw_dylib_name.filename,
items,
&output_path,
);
Expand All @@ -530,6 +548,38 @@ fn create_dll_import_libs<'a>(
.collect()
}

fn create_raw_dylib_elf_stub_shared_objects<'a>(
sess: &Session,
used_libraries: impl IntoIterator<Item = &'a NativeLib>,
raw_dylib_so_dir: &Path,
) -> Vec<Symbol> {
collate_raw_dylibs(sess, used_libraries)
.into_iter()
.map(|(raw_dylib_name, raw_dylib_imports)| {
let filename = format!("lib{}", raw_dylib_name.filename);

let shared_object = create_elf_raw_dylib_stub(&raw_dylib_imports, sess);

let so_path = raw_dylib_so_dir.join(&filename);
let file = match fs::File::create_new(&so_path) {
Ok(file) => file,
Err(error) => sess.dcx().emit_fatal(ErrorCreatingImportLibrary {
lib_name: &filename,
error: error.to_string(),
}),
};
if let Err(error) = BufWriter::new(file).write_all(&shared_object) {
sess.dcx().emit_fatal(ErrorCreatingImportLibrary {
lib_name: &filename,
error: error.to_string(),
});
};

raw_dylib_name.library_name
})
.collect()
}

/// Create a static archive.
///
/// This is essentially the same thing as an rlib, but it also involves adding all of the upstream
Expand Down Expand Up @@ -2319,15 +2369,32 @@ fn linker_with_args(
link_output_kind,
);

let raw_dylib_dir = tmpdir.join("raw-dylibs");
if let Err(error) = fs::create_dir(&raw_dylib_dir) {
sess.dcx().emit_fatal(errors::CreateTempDir { error })
}
// Only used on ELF for raw-dylibs.
cmd.include_path(&raw_dylib_dir);

// Link with the import library generated for any raw-dylib functions.
for output_path in create_dll_import_libs(
sess,
archive_builder_builder,
codegen_results.crate_info.used_libraries.iter(),
tmpdir,
true,
) {
cmd.add_object(&output_path);
if sess.target.is_like_windows {
for output_path in create_raw_dylib_dll_import_libs(
sess,
archive_builder_builder,
codegen_results.crate_info.used_libraries.iter(),
tmpdir,
true,
) {
cmd.add_object(&output_path);
}
} else {
for library_name in create_raw_dylib_elf_stub_shared_objects(
sess,
codegen_results.crate_info.used_libraries.iter(),
&raw_dylib_dir,
) {
cmd.link_dylib_by_name(library_name.as_str(), false, false);
}
}
// As with add_upstream_native_libraries, we need to add the upstream raw-dylib symbols in case
// they are used within inlined functions or instantiated generic functions. We do this *after*
Expand All @@ -2346,19 +2413,34 @@ fn linker_with_args(
.native_libraries
.iter()
.filter_map(|(&cnum, libraries)| {
(dependency_linkage[cnum] != Linkage::Static).then_some(libraries)
if sess.target.is_like_windows {
(dependency_linkage[cnum] != Linkage::Static).then_some(libraries)
} else {
Some(libraries)
}
})
.flatten()
.collect::<Vec<_>>();
native_libraries_from_nonstatics.sort_unstable_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
for output_path in create_dll_import_libs(
sess,
archive_builder_builder,
native_libraries_from_nonstatics,
tmpdir,
false,
) {
cmd.add_object(&output_path);

if sess.target.is_like_windows {
for output_path in create_raw_dylib_dll_import_libs(
sess,
archive_builder_builder,
native_libraries_from_nonstatics,
tmpdir,
false,
) {
cmd.add_object(&output_path);
}
} else {
for library_name in create_raw_dylib_elf_stub_shared_objects(
sess,
native_libraries_from_nonstatics,
&raw_dylib_dir,
) {
cmd.link_dylib_by_name(library_name.as_str(), false, false);
}
}

// Library linking above uses some global state for things like `-Bstatic`/`-Bdynamic` to make
Expand Down Expand Up @@ -3356,3 +3438,132 @@ fn add_lld_args(
}
}
}

/// Create an ELF .so stub file for raw-dylib.
/// It exports all the provided symbols, but is otherwise empty.
fn create_elf_raw_dylib_stub(symbols: &[DllImport], sess: &Session) -> Vec<u8> {
use object::write::elf as write;
use object::{Architecture, elf};

let mut stub_buf = Vec::new();

// When using the low-level object::write::elf, the order of the reservations
// needs to match the order of the writing.

let mut stub = write::Writer::new(object::Endianness::Little, true, &mut stub_buf);

// These initial reservations don't reserve any space yet.
stub.reserve_null_dynamic_symbol_index();

let dynstrs = symbols
.iter()
.map(|sym| {
stub.reserve_dynamic_symbol_index();
(sym, stub.add_dynamic_string(sym.name.as_str().as_bytes()))
})
.collect::<Vec<_>>();

stub.reserve_shstrtab_section_index();
let text_section_name = stub.add_section_name(".text".as_bytes());
let text_section = stub.reserve_section_index();
stub.reserve_dynstr_section_index();
stub.reserve_dynsym_section_index();

// These reservations determine the actual layout order of the object file.
stub.reserve_file_header();
stub.reserve_shstrtab();
stub.reserve_section_headers();
stub.reserve_dynstr();
stub.reserve_dynsym();

// File header
let (arch, sub_arch) = sess.target.object_architecture(&sess.unstable_target_features);
let e_machine = match (arch, sub_arch) {
(Architecture::Aarch64, None) => elf::EM_AARCH64,
(Architecture::Aarch64_Ilp32, None) => elf::EM_AARCH64,
(Architecture::Arm, None) => elf::EM_ARM,
(Architecture::Avr, None) => elf::EM_AVR,
(Architecture::Bpf, None) => elf::EM_BPF,
(Architecture::Csky, None) => elf::EM_CSKY,
(Architecture::E2K32, None) => elf::EM_MCST_ELBRUS,
(Architecture::E2K64, None) => elf::EM_MCST_ELBRUS,
(Architecture::I386, None) => elf::EM_386,
(Architecture::X86_64, None) => elf::EM_X86_64,
(Architecture::X86_64_X32, None) => elf::EM_X86_64,
(Architecture::Hexagon, None) => elf::EM_HEXAGON,
(Architecture::LoongArch64, None) => elf::EM_LOONGARCH,
(Architecture::M68k, None) => elf::EM_68K,
(Architecture::Mips, None) => elf::EM_MIPS,
(Architecture::Mips64, None) => elf::EM_MIPS,
(Architecture::Mips64_N32, None) => elf::EM_MIPS,
(Architecture::Msp430, None) => elf::EM_MSP430,
(Architecture::PowerPc, None) => elf::EM_PPC,
(Architecture::PowerPc64, None) => elf::EM_PPC64,
(Architecture::Riscv32, None) => elf::EM_RISCV,
(Architecture::Riscv64, None) => elf::EM_RISCV,
(Architecture::S390x, None) => elf::EM_S390,
(Architecture::Sbf, None) => elf::EM_SBF,
(Architecture::Sharc, None) => elf::EM_SHARC,
(Architecture::Sparc, None) => elf::EM_SPARC,
(Architecture::Sparc32Plus, None) => elf::EM_SPARC32PLUS,
(Architecture::Sparc64, None) => elf::EM_SPARCV9,
(Architecture::Xtensa, None) => elf::EM_XTENSA,
_ => {
sess.dcx().fatal(format!(
"raw-dylib is not supported for the architecture {}",
sess.target.arch
));
}
};

stub.write_file_header(&write::FileHeader {
os_abi: super::metadata::elf_os_abi(sess),
abi_version: 0,
e_type: object::elf::ET_DYN,
e_machine,
e_entry: 0,
e_flags: super::metadata::elf_e_flags(arch, sess),
})
.unwrap();

// .shstrtab
stub.write_shstrtab();

// Section headers
stub.write_null_section_header();
stub.write_shstrtab_section_header();
// Create a dummy .text section for our dummy symbols.
stub.write_section_header(&write::SectionHeader {
name: Some(text_section_name),
sh_type: elf::SHT_PROGBITS,
sh_flags: 0,
sh_addr: 0,
sh_offset: 0,
sh_size: 0,
sh_link: 0,
sh_info: 0,
sh_addralign: 1,
sh_entsize: 0,
});
stub.write_dynstr_section_header(0);
stub.write_dynsym_section_header(0, 1);

// .dynstr
stub.write_dynstr();

// .dynsym
stub.write_null_dynamic_symbol();
for (_, name) in dynstrs {
stub.write_dynamic_symbol(&write::Sym {
name: Some(name),
st_info: (elf::STB_GLOBAL << 4) | elf::STT_NOTYPE,
st_other: elf::STV_DEFAULT,
section: Some(text_section),
st_shndx: 0, // ignored by object in favor of the `section` field
st_value: 0,
st_size: 0,
});
}

stub_buf
}
Loading

0 comments on commit 35d33ec

Please sign in to comment.