RFC: Support External Linkers - #3993
Conversation
Original Author: Patrick Walton. Co-authored-by: Patrick Walton <pcwalton@fb.com>
…scope sections. Supported Platforms note, expanded examples, reflow
| the Rust compiler, driven by a variety of build systems, in a way that doesn't result in | ||
| symbol conflicts when diamond dependencies are involved. | ||
|
|
||
| ### Standard library bundles |
There was a problem hiding this comment.
The way I was imagining this wouldn't special case the standard library. Rather we would introduce a new rstaticlib crate type to fill the following matrix:
| - | Rust+C ABI | C ABI only |
|---|---|---|
| statically linked | rstaticlib | staticlib |
| dynamically linked | (rust) dylib | cdylib |
So like the rust dylib crate type it would be usable as regular rust dependency. Just like the staticlib crate type it would be statically linked. And like all non-rlib crate types it can contain multiple crates and contains things like the allocator shim as appropriate.
This avoids special casing the standard library and would make it possible to use #[global_allocator] and work for no_std projects too. And in addition it would avoid the need for -Crlib-version as no ribs would need to be directly linked. And EIIs would trivially work. If necessary the compiler can insert any object files to wire up EII defaults when building the rstaticlib like would happen for (c)dylibs and staticlibs.
The way you did use this is to compile some crate as rstaticlib and have this crate depend on the standard library and have it or a dependency define #[global_allocator] if you need one. And then every other can be compiled as either rstaticlib or rlib (if you want to bundle it together with other rlibs to form another rstaticlib) with a dependency on this first rstaticlib. And then you link all rstaticlibs together as regular C static libraries.
There was a problem hiding this comment.
Am I right in thinking you're describing something similar to the staticlib-nobundle alternative described here:
https://github.com/rust-lang/rfcs/pull/3993/changes#diff-68e8cfbb8f0b236256ec7cb366e93d1a56e4296801ff0f97fea3cc7729217b9dR472-R480
But with this archive file also including the .rmeta data (and possibly other things) which an rlib contains, so that rustc would consume it a lot like an rlib when building a rust application?
In this world I think you're saying the supported workflow would look something like this:
- Build at least one crate (although probably close to 1, for performance reasons since these would be large?) as an
rstaticlib. - Build all your other rust dependencies as an
rlib - Link your application by passing in the rlibs and rstaticlibs together.
Assuming I got the above right:
I agree this would avoid special casing the standard library. In practice, if the number of rstaticlib crates being built needs to be kept low, I imagine build systems choose to build exactly one of these at the bottom of the dependency tree?
Separately I'm not currently sure how we could support being able to link multiple rstaticlibs while having them export the standard library symbols as needed for linking against rlibs.
There was a problem hiding this comment.
For step 2 you would either build all other rust dependencies as rstaticlib or if you have an EII, you build the crate that declare the EII and the one that define it as rlib and then build an rstaticlib that bundles them together (or build the one defining the EII directly as rstaticlib, bundling the crate that declares the EII).
Separately I'm not currently sure how we could support being able to link multiple rstaticlibs while having them export the standard library symbols as needed for linking against rlibs.
If one rstaticlib depends on another rstaticlib, it will assume that all crates included in that other rstaticlib don't need to be bundled into the local rstaticlib. This is the same behavior as for rust dylibs where any crates included through an upstream dylib don't get linked in locally but rather the version in the upstream dylib is used and rustc errors if two dylibs that are used as dependency together both bundle the same crate.
| echo $? | ||
| ``` | ||
|
|
||
| RFC Note: This works today by removing the `-C emit-std-bundle=yes` and `-C rlib-version=v0` flags. |
There was a problem hiding this comment.
You've already said this above, but it helps to explain what doesn't work again here:
| RFC Note: This works today by removing the `-C emit-std-bundle=yes` and `-C rlib-version=v0` flags. | |
| RFC Note: This works today by removing the `-C emit-std-bundle=yes` and `-C rlib-version=v0` flags, | |
| but it would fail if there was a diamond dependency. |
There was a problem hiding this comment.
This technique actually works today even with diamond dependencies (only the standard library builds as a staticlib, the crate builds as an rlib). This note was meant to highlight that the only implementation changes required by this RFC are compiler flags/versions for a backwards compatibility strategy.
I can expand this example to include a diamond dependency to illustrate this point?
There was a problem hiding this comment.
There's already a diamond dependency example in the RFC, so I think it's enough to just say it works here too. (And that's an important piece of info!)
| scheme for symbols. | ||
|
|
||
| *Note (non-normative):* Symbols relating to global allocation and panic handling | ||
| must not be defined in the .rlib unless the crate itself defines those symbols. |
There was a problem hiding this comment.
Does this limitation apply to all lang items?
| system linker. | ||
| * Exactly one of the core or std standard library bundles is supplied to the system | ||
| linker. This standard library bundle must have been built in a *compatible manner* | ||
| with all rlibs to be linked. |
There was a problem hiding this comment.
I think there's a subtle requirement here around lang items, which is roughly:
| with all rlibs to be linked. | |
| with all rlibs to be linked. | |
| * All used [*language items*](https://rustc-dev-guide.rust-lang.org/lang-items.html) are supplied by exactly one rlib. Ordinarily, the standard library supplies these language items. But in `no_std` builds, some may be supplied by other rlibs. |
There was a problem hiding this comment.
I think this is a sensible wording, and this probably also sets the scene for tweaking this wording for a similar requirement for EIIs(same requirement?). I'll add this if no one objects for a couple of days
| - **The CI-tested configuration.** Which compiler flags, panic strategies, | ||
| and platforms are in-scope for covering under tests. | ||
| - **Testing scope.** What should the tests check for. I.e. link test | ||
| of a diamond graph, symbol-table checks on emitted `rlib`s?, etc. |
There was a problem hiding this comment.
It would be useful to test a recursive cross-language dependency:
- a Rust crate that requires symbol A from C++ and provides symbol B
- a C++ library that requires symbol B and provides A
This is currently possible but tricky to get right with some system linkers.
There was a problem hiding this comment.
I agree it would be good to test at least one level of language weaving. At least the use case I have in mind would likely do this. Do you specifically want two libraries which depend on each other? In my experience that type of scenario is fairly rare/problematic (maybe I'm out of date there?).
I think I would pick to have a cross-language dependency tree more like:
A (C++ Executable)
|
v
B (Rust Library)
| \
v \
C (C++ Library) \
| \
+---------------> D (Rust Library)
There was a problem hiding this comment.
I think both are good tests, and they increase test coverage in slightly different ways.
I agree recursion is rare, and we might not support it initially. But either way it would be good to know its status, and make sure it doesn't regress. Recursion might also be an opportunity to improve error messages or documentation more generally.
I've seen new interop users run into it in small projects when just starting out, too.
Co-authored-by: teor <teor@riseup.net>
| embed this information within the archive. | ||
| 4. Swift can build modules into an object file, or an ar archive, or a `bundle`. | ||
| This RFC is only aiming to produce `ar` archives (with the file extension `.rlib`) | ||
|
|
There was a problem hiding this comment.
It's not the same kind of prior art as Swift, but an example of how it's already used and how these issues are solved might be useful
| ### Bazel-Based Rust Bootstrap | |
| The Rust compiler can be [bootstrapped using Bazel](https://github.com/dtolnay/buck2-rustc-bootstrap) | |
| instead of `x.py`. This process relies on unsupported rlib linking, but it also deals with issues such as: | |
| - [allocator shims](https://github.com/dtolnay/buck2-rustc-bootstrap/blob/71c6455e36b8b051efa8e5d0690c9ea680096211/allocator/allocator.rs#L29) | |
| - [duplicate symbols](https://github.com/dtolnay/buck2-rustc-bootstrap/blob/71c6455e36b8b051efa8e5d0690c9ea680096211/fixups/compiler_builtins/fixups.toml#L9) | |
| - panic handling [unwind](https://github.com/dtolnay/buck2-rustc-bootstrap/blob/71c6455e36b8b051efa8e5d0690c9ea680096211/fixups/std/fixups.toml#L19) [abort](https://github.com/dtolnay/buck2-rustc-bootstrap/blob/71c6455e36b8b051efa8e5d0690c9ea680096211/fixups/panic_abort/fixups.toml#L1) |
| In order to successfully produce a binary containing both Rust code and native code, a | ||
| way to link to the Rust standard library is needed. This RFC specifies a simple mechanism | ||
| for doing so: simply compile an empty crate (an empty lib.rs file is fine) as a staticlib | ||
| with a flag `-C emit-std-bundle=yes`. Any desired crate-level metadata and/or compiler |
There was a problem hiding this comment.
What members does the bundle contain? One reading for it is includes everything from the sysroot, including private vendored dependencies of the standard libs, like hashbrown.
Does it include members that are only needed in some contexts, like libtest or libproc_macro?
If the rust toolchain includes the rustc-dev component (so a bunch of rustc_* packages in the sysroot), does the stdlib bundle also include them?
How does it pick between (so resolve potential symbol conflicts) libpanic_unwind and libpanic_abort?
How do we deal with no_std variants with-- and without-- alloc?
For context, in the Bazel Rust rules, when we adapt Rust standard libraries .rlibs to be consumed by the native linking infrastructure (currently unsupported by upstream), we have to deal with a bunch of cases like this to produce the standard libraries linker inputs dependency graph. So I'm imagining we may need some fine grained mechanism of controlling what exactly's in and out of the bundle.
There was a problem hiding this comment.
How do we deal with no_std variants with-- and without-- alloc?
This is covered in the lines below:
Any desired crate-level metadata and/or compiler flags can be supplied in the process of compiling this standard library bundle, for example
#![no_std]to omit the standard library (and only link the core library), or-C target-featureto enable specific CPU features.
I've also made a suggestion in the reference section, which might cover some of these choices:
All used language items are supplied by exactly one rlib. Ordinarily, the standard library supplies these language items. But in
no_stdbuilds, some may be supplied by other rlibs.
And there's this text as well:
The exact symbols that are exposed in a standard library bundle is unspecified by this RFC. In general, they are expected to change with every Rust release and may change depending on the manner in which the standard library bundle was compiled.
But I don't think that's enough, because it's important that different tools agree on how to make these choices.
So for each of the common member selection choices, it would be good to document in the RFC how it is configured. This will also help us identify any gaps.
For less common choices we could leave them for the implementation stage.
There was a problem hiding this comment.
I like the RFC in general! One thing I think is important to note (don't know where it fits) is that #[used] won't work with this scheme (or, to my knowledge, any external linker scheme that only concerns itself with archives).
The expected semantics of #[used] is that once the crate is included in the crate graph, the symbol should reach the linker. This is most prominently utilized in crates such as inventory and ctor to do before-main initialization for symbols, which the rest of the crate can then (mostly) depend on.
For example:
use std::sync::atomic::{AtomicUsize, Ordering};
static FOO: AtomicUsize = AtomicUsize::new(0);
mod internal {
use super::*;
// Initialize `FOO` at the start of the program.
// Roughly equivalent to what `#[ctor::ctor]` does.
#[used]
#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__DATA,__mod_init_func,mod_init_funcs"))]
#[cfg_attr(target_os = "windows", unsafe(link_section = ".CRT$XCU"))]
#[cfg_attr(not(any(target_vendor = "apple", target_os = "windows")), unsafe(link_section = ".init_array"))]
static INITIALIZER: extern "C" fn() = {
extern "C" fn init() {
FOO.store(42, Ordering::Relaxed);
}
init
};
}
// Once the user runs `bar`, `FOO` _should_ be initialized.
#[unsafe(no_mangle)]
pub extern "C" fn bar() {
assert_eq!(FOO.load(Ordering::Relaxed), 42);
}rustc could then produce a mycrate.rlib with these members:
lib.rmeta.foo.owhich definesFOO.initializer.owhich definesINITIALIZER, referencesFOOandstd.bar.owhich definesbar, referencesFOOandstd.
Assume there's a piece of C++ code elsewhere that references and runs bar. Now, if a build system naively links against this archive, the linker will look for bar, find it via the archive index and parse bar.o, look for FOO, find it via the archive index and parse foo.o, but will completely ignore initializer.o (unless --whole-archive is used), which means that FOO will not be initialized, and bar will panic!
The way rustc currently works around this is by adding a symbols.o file to the final link that references all #[used] symbols, see rust-lang/rust#133832 and rust-lang/rust#95604 for details.
I can see a few ways of resolving this:
- Require build systems to use
--whole-archive(might be bad for link-time performance, we would have to profile to see if it'd be acceptable. Likely acceptable in the initial implementation). - Some kind of side-channel to the build system, so that it knows to pass
-u INITIALIZERor similar to the linker. - Add a fake reference to
INITIALIZERto every object file in the archive, such that if any symbol is used from the archive,#[used]symbols are visible to the linker as well. This would help, though some use-cases such asdivanwould still be broken.
It may also be acceptable that certain language features are unavailable, but I feel that it should be discussed in the RFC, and should probably be linted against in dependencies if the user is compiling in this mode.
| ### Machine Readable --print native-static-libs | ||
| The libstdrust.a standard library bundle still requires a few other native libraries | ||
| to link correctly when invoking `ld` directly. These are nearly all also required by | ||
| most C++ code, but currently the best way to get this is to pass `--print native-static-libs` | ||
| while building the standard library bundle, and parse this output: | ||
|
|
||
| ``` | ||
| note: link against the following native artifacts when linking against this static library. The order and any duplication can be significant on some platforms. | ||
|
|
||
| note: native-static-libs: -lSystem -lc -lm | ||
| ``` |
There was a problem hiding this comment.
I think having a good story for this is important for the ecosystem, without it, it becomes effectively a breaking change for libraries to add new #[link(...)] attributes. For example, a library may want to, in a minor version, link to the Security framework on macOS, but if nothing else in the final binary links against that, the user would get a confusing link error after updating their dependencies.
Specifying the linking in a build script with println!("cargo::rustc-link-lib=framework=Security") will likely work better with external build systems, so I fear that #[link(...)] may become a second-class citizen (which will cause us to regress on rust-lang/cargo#14948).
I won't demand that we solve this issue now, but I do think it would be useful for this RFC to discuss possible solutions to this, to give a better picture of how everything might fit together.
One partial solution might be to improve rust-lang/rust#121293.
Whatever we plan to do here, it should work together with link-arg kind (rust-lang/rust#99427), which IIRC also has options to specify whether a linker argument is early or late.
There was a problem hiding this comment.
There are already users of -Zlink-native-libraries=no like Android.
Specifying the linking in a build script with println!("cargo::rustc-link-lib=framework=Security") will likely work better with external build systems, so I fear that #[link(...)] may become a second-class citizen (which will cause us to regress on rust-lang/cargo#14948).
On the other hand you need #[link] on each extern "C" {} block that imports from a static library to correctly support building rust dylibs. Otherwise depending on what functions rustc decides to cross-crate inline or not, you might get linker errors when using the rust dylib as dependency due to functions of the static library that cross-crate inlined functions may call not getting exported from the dylib.
This RFC partially stabilises a version 0 of the
rlibformat, and defines a 'standard library bundle' mechanism with the aim to make this a supported workflow:Getting there requires a couple of practical steps, which would come if this RFC is accepted - or could happen in parallel if desirable:
rustcdocuments and supports a "build crate asrlib, link with a non-rustclinker" model as a first-class workflow, with clearly stated constraints that keep long-term support feasible.x86_64-unknown-linux-gnu.I decided to pick this up after some conversations with folks where I work, and others within the Rust Community. My understanding is that this technique is already used today by a few large companies who are also active in the Rust community. Standardising this feels like a positive step forward, which makes it easier for more projects to adopt Rust in existing C++ codebases.
It's my first RFC & I probably didn't get everything right but hopefully I'm close enough. Happy to discuss here/on Zuilip/zoom/etc as needed to keep this moving.
Credit note: Although I am submitting this RFC, and stand by every word in it, a large majority of its content comes directly from @pcwalton's pre-RFC here: https://internals.rust-lang.org/t/pre-rfc-stabilize-a-version-of-the-rlib-format/17558 which was attached to rust-lang/rust#73632
Rendered