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 b057efd
Show file tree
Hide file tree
Showing 49 changed files with 299 additions and 46 deletions.
242 changes: 207 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_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);

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_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_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,93 @@ 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]) -> Vec<u8> {
use object::elf;
use object::write::elf as write;

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
stub.write_file_header(&write::FileHeader {
os_abi: object::elf::ELFOSABI_SYSV,
abi_version: 0,
e_type: object::elf::ET_DYN,
e_machine: object::elf::EM_X86_64,
e_entry: 0,
e_flags: 0,
})
.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
}
10 changes: 1 addition & 9 deletions compiler/rustc_codegen_ssa/src/back/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,7 @@ pub(crate) fn create_object_file(sess: &Session) -> Option<write::Object<'static
// Unsupported architecture.
_ => return None,
};
let binary_format = if sess.target.is_like_osx {
BinaryFormat::MachO
} else if sess.target.is_like_windows {
BinaryFormat::Coff
} else if sess.target.is_like_aix {
BinaryFormat::Xcoff
} else {
BinaryFormat::Elf
};
let binary_format = sess.target.binary_format();

let mut file = write::Object::new(binary_format, architecture, endianness);
file.set_sub_architecture(sub_architecture);
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_feature/src/unstable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,8 @@ declare_features! (
(unstable, precise_capturing_in_traits, "1.83.0", Some(130044)),
/// Allows macro attributes on expressions, statements and non-inline modules.
(unstable, proc_macro_hygiene, "1.30.0", Some(54727)),
/// Allows the use of raw-dylibs on ELF platforms
(incomplete, raw_dylib_elf, "CURRENT_RUSTC_VERSION", Some(135694)),
/// Makes `&` and `&mut` patterns eat only one layer of references in Rust 2024.
(incomplete, ref_pat_eat_one_layer_2024, "1.79.0", Some(123076)),
/// Makes `&` and `&mut` patterns eat only one layer of references in Rust 2024—structural variant
Expand Down
3 changes: 3 additions & 0 deletions compiler/rustc_metadata/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ metadata_prev_alloc_error_handler =
metadata_prev_global_alloc =
previous global allocator defined here
metadata_raw_dylib_elf_unstable =
link kind `raw-dylib` is unstable on ELF platforms
metadata_raw_dylib_no_nul =
link name must not contain NUL characters if link kind is `raw-dylib`
Expand Down
Loading

0 comments on commit b057efd

Please sign in to comment.