Skip to content

[WIP] parameterize -C prefer-dynamic #88101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_llvm/src/back/lto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ fn prepare_lto(
// with either fat or thin LTO
let mut upstream_modules = Vec::new();
if cgcx.lto != Lto::ThinLocal {
if cgcx.opts.cg.prefer_dynamic {
if cgcx.opts.cg.prefer_dynamic.is_non_empty() {
diag_handler
.struct_err("cannot prefer dynamic linking when performing LTO")
.note(
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_llvm/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ impl CodegenCx<'ll, 'tcx> {
debug_assert!(
!(self.tcx.sess.opts.cg.linker_plugin_lto.enabled()
&& self.tcx.sess.target.is_like_windows
&& self.tcx.sess.opts.cg.prefer_dynamic)
&& self.tcx.sess.opts.cg.prefer_dynamic.is_non_empty())
);

if needs_dll_storage_attr {
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_codegen_ssa/src/back/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1950,7 +1950,7 @@ fn msvc_imps_needed(tcx: TyCtxt<'_>) -> bool {
assert!(
!(tcx.sess.opts.cg.linker_plugin_lto.enabled()
&& tcx.sess.target.is_like_windows
&& tcx.sess.opts.cg.prefer_dynamic)
&& tcx.sess.opts.cg.prefer_dynamic.is_non_empty())
);

tcx.sess.target.is_like_windows &&
Expand Down
71 changes: 53 additions & 18 deletions compiler/rustc_metadata/src/dependency_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,23 +85,43 @@ fn calculate_type(tcx: TyCtxt<'_>, ty: CrateType) -> DependencyList {
return Vec::new();
}

let prefer_dynamic = &sess.opts.cg.prefer_dynamic;

// traverses all crates to see if any are in the prefer-dynamic set.
let prefer_dynamic_for_any_crate = || {
if prefer_dynamic.is_empty() {
// skip traversal if we know it cannot ever bear fruit.
false
} else {
tcx.crates(()).iter().any(|cnum| prefer_dynamic.contains_crate(tcx.crate_name(*cnum)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should skip non-linkable crates like proc-macros and their dependencies I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I'll look into that.

}
};

let preferred_linkage = match ty {
// Generating a dylib without `-C prefer-dynamic` means that we're going
// to try to eagerly statically link all dependencies. This is normally
// done for end-product dylibs, not intermediate products.
// If `-C prefer-dynamic` is not set for any crate, then means that we're
// going to try to eagerly statically link all dependencies into the dylib.
// This is normally done for end-product dylibs, not intermediate products.
//
// Treat cdylibs similarly. If `-C prefer-dynamic` is set, the caller may
// be code-size conscious, but without it, it makes sense to statically
// link a cdylib.
CrateType::Dylib | CrateType::Cdylib if !sess.opts.cg.prefer_dynamic => Linkage::Static,
CrateType::Dylib | CrateType::Cdylib => Linkage::Dynamic,
// Treat cdylibs similarly. If `-C prefer-dynamic` is set for any given
// crate, the caller may be code-size conscious, but without it, it
// makes sense to statically link a cdylib.
CrateType::Dylib | CrateType::Cdylib => {
if prefer_dynamic_for_any_crate() {
Linkage::Dynamic
} else {
Linkage::Static
}
}

// If the global prefer_dynamic switch is turned off, or the final
// If `prefer_dynamic` is not set for any crate, or the final
// executable will be statically linked, prefer static crate linkage.
CrateType::Executable if !sess.opts.cg.prefer_dynamic || sess.crt_static(Some(ty)) => {
Linkage::Static
CrateType::Executable => {
if !prefer_dynamic_for_any_crate() || sess.crt_static(Some(ty)) {
Linkage::Static
} else {
Linkage::Dynamic
}
}
CrateType::Executable => Linkage::Dynamic,

// proc-macro crates are mostly cdylibs, but we also need metadata.
CrateType::ProcMacro => Linkage::Static,
Expand Down Expand Up @@ -148,6 +168,11 @@ fn calculate_type(tcx: TyCtxt<'_>, ty: CrateType) -> DependencyList {
));
}
return Vec::new();
} else {
tracing::info!(
"calculate_type {} attempt_static failed; falling through to dynamic linkage",
tcx.crate_name(LOCAL_CRATE)
);
}
}

Expand All @@ -163,7 +188,14 @@ fn calculate_type(tcx: TyCtxt<'_>, ty: CrateType) -> DependencyList {
}
let name = tcx.crate_name(cnum);
let src: Lrc<CrateSource> = tcx.used_crate_source(cnum);
if src.dylib.is_some() {
// We should prefer the dylib, when available, if either:
// 1. the user has requested that dylib (or all dylibs) via `-C prefer-dynamic`, or
// 2. for backwards compatibility: if the prefer-dynamic subset is unset, then we *still*
// favor dylibs here. This way, if static linking fails above, we might still hope to
// succeed at linking here.
let prefer_dylib =
|name| -> bool { prefer_dynamic.is_unset() || prefer_dynamic.contains_crate(name) };
if src.dylib.is_some() && prefer_dylib(name) {
tracing::info!("calculate_type {} adding dylib: {}", tcx.crate_name(LOCAL_CRATE), name);
add_library(tcx, cnum, RequireDynamic, &mut formats, &mut rev_deps, LOCAL_CRATE);
let deps = tcx.dylib_dependency_formats(cnum);
Expand Down Expand Up @@ -196,13 +228,16 @@ fn calculate_type(tcx: TyCtxt<'_>, ty: CrateType) -> DependencyList {
// If the crate hasn't been included yet and it's not actually required
// (e.g., it's an allocator) then we skip it here as well.
for &cnum in tcx.crates(()).iter() {
let name = tcx.crate_name(cnum);
let src: Lrc<CrateSource> = tcx.used_crate_source(cnum);
if src.dylib.is_none()
&& !formats.contains_key(&cnum)
&& tcx.dep_kind(cnum) == CrateDepKind::Explicit
{
let static_available = src.rlib.is_some() || src.rmeta.is_some();
let prefer_static =
src.dylib.is_none() || (static_available && !prefer_dynamic.contains_crate(name));
let missing = !formats.contains_key(&cnum);
let actually_required = tcx.dep_kind(cnum) == CrateDepKind::Explicit;
if prefer_static && missing && actually_required {
assert!(src.rlib.is_some() || src.rmeta.is_some());
tracing::info!("adding staticlib: {}", tcx.crate_name(cnum));
tracing::info!("adding staticlib: {}", name);
add_library(tcx, cnum, RequireStatic, &mut formats, &mut rev_deps, LOCAL_CRATE);
ret[cnum.as_usize() - 1] = Linkage::Static;
}
Expand Down
67 changes: 65 additions & 2 deletions compiler/rustc_session/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,68 @@ pub enum CFGuard {
Checks,
}

/// Handle `-C prefer-dynamic` flag: either its all, or its some explicit
/// subset (potentially the empty set).
#[derive(Clone, PartialEq, Hash, Debug)]
pub enum PreferDynamicSet {
/// Specified by the absence of `-C prefer-dynamic`, or via an explicit `-C prefer-dynamic=...`
/// with value `n`, `no`, or `off`.
///
/// Under this (default) behavior, the compiler first optimistically attempts to statically
/// link everything, but if that fails, then attempts to dynamically link *everything* with a
/// dylib path available.
Unset,

/// Specified via `-C prefer-dynamic` with no value. For backwards-compatibility, also
/// specified via `-C prefer-dynamic=...` with value `y`, `yes`, or `on`.
All,

/// Specified via `-C prefer-dynamic=crate,...`.
Set(BTreeSet<String>),
}

impl PreferDynamicSet {
pub fn unset() -> Self {
PreferDynamicSet::Unset
}

pub fn every_crate() -> Self {
PreferDynamicSet::All
}

pub fn subset(crates: impl Iterator<Item = impl ToString>) -> Self {
PreferDynamicSet::Set(crates.map(|x| x.to_string()).collect())
}

pub fn is_unset(&self) -> bool {
if let PreferDynamicSet::Unset = *self { true } else { false }
}

pub fn is_empty(&self) -> bool {
match self {
PreferDynamicSet::Unset => true,
PreferDynamicSet::All => false,
PreferDynamicSet::Set(s) => s.len() == 0,
}
}

pub fn is_non_empty(&self) -> bool {
match self {
PreferDynamicSet::Unset => false,
PreferDynamicSet::All => true,
PreferDynamicSet::Set(s) => s.len() > 0,
}
}

pub fn contains_crate(&self, crate_name: Symbol) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using just the crate name is not a good idea. It doesn't allow distinguishing between two different versions of the same library. Ideally it would accept the StableCrateId, but that is not accessible outside of rustc without depending on nightly (-Zls?) or implementation details. Maybe also allow accepting a dylib path or combination of crate name, if it has bin as crate type in addition to dylib and the list of -Cmetadata arguments. The StableCrateId is reproducible using just this information. I don't think we can't change what it depends on as that would change which crates can be linked into the same crate graph and which can't: https://github.com/rust-lang/rust/blob/master/compiler/rustc_span/src/def_id.rs#L148-L175

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would some encoding of crate name and an optionally-supplied version be sufficient to satisfy your criteria here? I'm thinking in the common cases, a single crate name is exactly what an end user will want to write down.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that would work I guess. Cargo could specify everything and a user just the crate name.

match self {
PreferDynamicSet::Unset => false,
PreferDynamicSet::All => true,
PreferDynamicSet::Set(s) => s.iter().any(|s| s == &*crate_name.as_str()),
}
}
}

#[derive(Clone, Copy, Debug, PartialEq, Hash)]
pub enum OptLevel {
No, // -O0
Expand Down Expand Up @@ -2420,8 +2482,8 @@ crate mod dep_tracking {
use super::LdImpl;
use super::{
CFGuard, CrateType, DebugInfo, ErrorOutputType, InstrumentCoverage, LinkerPluginLto,
LtoCli, OptLevel, OutputType, OutputTypes, Passes, SourceFileHashAlgorithm,
SwitchWithOptPath, SymbolManglingVersion, TrimmedDefPaths,
LtoCli, OptLevel, OutputType, OutputTypes, Passes, PreferDynamicSet,
SourceFileHashAlgorithm, SwitchWithOptPath, SymbolManglingVersion, TrimmedDefPaths,
};
use crate::lint;
use crate::options::WasiExecModel;
Expand Down Expand Up @@ -2510,6 +2572,7 @@ crate mod dep_tracking {
TrimmedDefPaths,
Option<LdImpl>,
OutputType,
PreferDynamicSet,
RealFileName,
);

Expand Down
36 changes: 35 additions & 1 deletion compiler/rustc_session/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ mod desc {
"one of supported relocation models (`rustc --print relocation-models`)";
pub const parse_code_model: &str = "one of supported code models (`rustc --print code-models`)";
pub const parse_tls_model: &str = "one of supported TLS models (`rustc --print tls-models`)";
pub const parse_prefer_dynamic: &str =
"one of `y`, `yes`, `on`, `n`, `no`, `off`, or a comma separated list of crates";
pub const parse_target_feature: &str = parse_string;
pub const parse_wasi_exec_model: &str = "either `command` or `reactor`";
pub const parse_split_debuginfo: &str =
Expand Down Expand Up @@ -835,6 +837,38 @@ mod parse {
true
}

crate fn parse_prefer_dynamic(slot: &mut PreferDynamicSet, v: Option<&str>) -> bool {
let s = match v {
// Note: n/no/off do *not* map to an empty-set of crates.
//
// This is to continue supporting rustc's historical behavior where it attempts to
// link everything statically, but failing that, then greedily link as many crates
// dynamically as it can.
//
// If these options mapped to an empty set, then that would denote that no dynamic
// linkage should be given preference over static, which would not correspond to
// historical meaning of `-C prefer-dynamic=no`.
//
// (One requests an empty set by writing `-C prefer-dynamic=`, with an empty string
// as the value.)
Some("n") | Some("no") | Some("off") => PreferDynamicSet::unset(),

// `-C prefer-dynamic` gives all crates preferred dynamic linkage.
// `-C prefer-dynamic=...` with `y`/`yes`/`on` is a synonym, for backwards
// compatibility.
Some("y") | Some("yes") | Some("on") | None => PreferDynamicSet::every_crate(),

// `-C prefer-dynamic=crate1,crate2,...` gives *just* crate1, crate2, ... preferred
// dynamic linkage.
Some(s) => {
let v = s.split(',').map(|s| s.to_string()).collect();
PreferDynamicSet::Set(v)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it should be a different argument as the new option is not a preference, but mandatory?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. My intention was that it would still be a preference, in the sense that if you do -C prefer-dynamic=crate1, but only a static library for crate1 is available, then linkage will still proceed using the static library.

I probably should add a test illustrating that.

Copy link
Member Author

@pnkfelix pnkfelix Aug 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On reflection, I do think I must have messed up something in my logic that ends up making something a requirement rather than a preference. Specifically, I'm working on a run-make test analogous to https://github.com/rust-lang/rust/blob/master/src/test/run-make-fulldeps/dylib-chain/Makefile
but where I choose variations of whether dylibs on the chain have both rlib and dylib, or just rlib, or just dylib.

The problem I'm seeing is that in some scenarios using -C prefer-dynamic=crate,..., a crate that is solely provided as a dylib is not getting linked in unless I include it in the above list. That wasn't my intention here: My intention was that the preference is meant to give guidance when the compiler must make a choice, not act as an assertion about what it expects to be present.

Update: I forgot that of course the choice isn't necessarily about which file to link in; you can still statically link a .dylib file. So even if I generate just a .dylib for some crate in the chain, the crates linking to it still have a choice to make, and this flag is going to affect that choice.

(Or at least, that's my current interpretation of the behavior I'm observing. Still digging a bit deeper.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update 2: Hmm maybe I was wrong about the above: In general .so files don't have enough info to be statically linked. But then I'm really confused about some of the behavior I'm seeing. Still looking.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I have now made a run-make test that I think illustrates the current behavior of the system, including cases where rlib's end up depending on dylibs.

I don't think pre-existing behavior is necessarily intuitive, but my main goal right now is to address missing use cases, not to try to figure out what the ideal behavior is for all cases and potentially inject breaking changes to support that idealized behavior.

};
*slot = s;
return true;
}

crate fn parse_src_file_hash(
slot: &mut Option<SourceFileHashAlgorithm>,
v: Option<&str>,
Expand Down Expand Up @@ -962,7 +996,7 @@ options! {
"panic strategy to compile crate with"),
passes: Vec<String> = (Vec::new(), parse_list, [TRACKED],
"a list of extra LLVM passes to run (space separated)"),
prefer_dynamic: bool = (false, parse_bool, [TRACKED],
prefer_dynamic: PreferDynamicSet = (PreferDynamicSet::unset(), parse_prefer_dynamic, [TRACKED],
"prefer dynamic linking to static linking (default: no)"),
profile_generate: SwitchWithOptPath = (SwitchWithOptPath::Disabled,
parse_switch_with_opt_path, [TRACKED],
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_session/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1379,7 +1379,7 @@ fn validate_commandline_args_with_session_available(sess: &Session) {
// when compiling for LLD ThinLTO. This way we can validly just not generate
// the `dllimport` attributes and `__imp_` symbols in that case.
if sess.opts.cg.linker_plugin_lto.enabled()
&& sess.opts.cg.prefer_dynamic
&& sess.opts.cg.prefer_dynamic.is_non_empty()
&& sess.target.is_like_windows
{
sess.err(
Expand Down
7 changes: 7 additions & 0 deletions src/doc/rustc/src/codegen-options/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,13 @@ linkage. This flag takes one of the following values:

* `y`, `yes`, `on`, or no value: use dynamic linking.
* `n`, `no`, or `off`: use static linking (the default).
* A comma-separated list of crate names `crate1,crate2,...`: prefer dynamic
linking, but solely for the indicated crates. For example, to statically link
everything except `std`, use `-C prefer-dynamic=std`.

Note that the explicit list of crate names variant of this flag is gated behind
`-Zprefer-dynamic-subset`; as a special case, one can enable the handling of
the single crate `std` as shown in the example above via `-Zprefer-dynamic-std`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those options don't exist at least as of this commit.

Edit: these are introduced in the next commit.


## profile-generate

Expand Down