Skip to content
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

Better support for linking against alternate libraries #6519

Open
indygreg opened this issue Jan 3, 2019 · 6 comments
Open

Better support for linking against alternate libraries #6519

indygreg opened this issue Jan 3, 2019 · 6 comments
Labels
A-documenting-cargo-itself Area: Cargo's documentation A-linkage Area: linker issues, dylib, cdylib, shared libraries, so C-feature-request Category: proposal for a feature. Before PR, ping rust-lang/cargo if this is not `Feature accepted` S-triage Status: This issue is waiting on initial triage.

Comments

@indygreg
Copy link

indygreg commented Jan 3, 2019

I have a wonky use case that requires doing dirty things with linking. I'm not exactly sure what I want Cargo to do yet - only what I want it to enable me to do. This issue could be interpreted as asking for help. But I think exposing the problem to Cargo's maintainers would be useful, as it may help influence future features.

My problem statement can be summarized as: I want to force a dependency crate using #[link] to obtain symbols from the current crate instead of the library specified by #[link]. But that may not capture the full detail of the problem.

My use case is with the https://github.com/indygreg/PyOxidizer project. I have a crate that depends on the python3-sys crate (https://github.com/dgrunwald/rust-cpython). In addition to its build.rs emitting tons of lines (which I can override easily enough), the python3-sys crate heavily uses #[cfg_attr(windows, link(name="pythonXY"))] extern "C" { ... }. Its build.rs emits a cargo:rustc-link-lib=pythonXY:python{}{} line remapping pythonXY to what it finds when running. This is all pretty standard for *-sys crates.

Here's where things get wonky.

I have a crate (pyembed) in PyOxidizer whose build.rs effectively builds a static library containing a custom Python distribution using the cc crate. The cc crate and pyembed's build.rs emit cargo:rustc-link-lib=static= and cargo:rustc-link-search=native= lines so this static library is linked. This is also fairly straightforward I think.

What makes this use case special is that the static library I'm building provides the symbols that the dependency python3-sys crate wants - and has declared as available in the pythonXY library. This kinda/sorta creates a circular dependency. But because symbols aren't resolved until link time, I don't run into problems building the dependency python3-sys crate - even though its external symbols and #[link] library aren't available/built yet.

I have a few solutions to this problem available to me today. But they feel less than optimal.

If my build.rs produces a static library whose name matches the #[link] in the dependent crate exactly (in my case pythonXY), that library satisfies the pythonXY.lib requirement that Cargo automatically adds to the link.exe invocation. The linker can find the .lib file and all is good. Furthermore, I don't think it matters if the symbols are actually in that .lib or not, as the linker doesn't care what .lib provides the symbols. All that matters is that the symbols can be found. So, my build.rs could produce a pythonXY.lib actually containing the Python symbols. Or it could produce an empty pythonXY.lib to satisfy the #[link] requirement in the dependency.

Producing .lib files to satisfy #[link] requirements feels like the best option available to me today.

Something else I tried was to use the cargo:rustc-link-library=ALIAS:TARGET syntax to remap pythonXY to something else.

Originally, the build.rs produced static library containing Python's symbols was named pyembedded. I tried adding a cargo:rustc-link-library=pythonXY:pyembedded line to tell Cargo to link pyembedded.lib instead of pythonXY.lib. From a linker perspective, this should just work. pyembedded.lib might be present multiple times to link.exe's invocation, but that's OK. However, Cargo rejected this with error: renaming of the library `pythonXY` was specified, however this crate contains no #[link(...)] attributes referencing this library.. I suppose I could add a #[link] attribute to pyembed. But it feels weird telling it to link against pythonXY since that is exactly the thing I'm trying to prevent!

I also tried adding a cargo:rustc-link-library=pythonXY:pyembed (pyembed is the name of the crate). My thinking was I would just refer the crate to itself and things might just work (in theory I think they would from a linker perspective). But Cargo also rejected this due to pythonXY not being declared. This makes sense.

I also thought maybe I can remap pythonXY to an empty string to make the link requirement go away. I attempted to add a cargo:rustc-link-lib=pythonXY: line. But this was rejected with error: an empty renaming target was specified for library `pythonXY and error: empty library name given via `-l` .

It's worth noting that the rustc-link-lib=ALIAS:NAME renaming syntax doesn't appear to be documented at https://doc.rust-lang.org/cargo/reference/build-scripts.html. This scares me a little, as I'm not sure if the feature is stable. Is this feature documented anywhere?

While I think I have a workaround in the form of producing .lib files matching the #[link] requirements of dependent crates, it feels a bit hacky. In my use case, I explicitly do not want the dependent crates to attempt linking against defined #[link] entries, since I'm providing those symbols as part of my crate.

From my perspective, I think the simplest feature that would do the trick is the ability to remove / cancel out the #[link] attributes of dependent crates. The cargo:rustc-link-library:NAME: syntax /might/ be a clever way to implement this.

Of course, removing a #[link] requirement is a subset of a larger, more general feature, which is to have full control over linking behavior, overriding or replacing behavior automatically derived from dependent crates. This seems like a complex, hard-to-design-and-implement feature and I'm not requesting it. (But it would be nice.)

I hope the Cargo maintainers find this report useful. Since I think there is a sufficient existing workaround and my use case is a bit special, I wouldn't consider this a high priority to address.

@indygreg indygreg added the C-feature-request Category: proposal for a feature. Before PR, ping rust-lang/cargo if this is not `Feature accepted` label Jan 3, 2019
@indygreg
Copy link
Author

indygreg commented Jan 3, 2019

Another wrinkle to this is that in the case of python3-sys, it assumes pythonXY is a shared library. As such, Rust is adding the __imp_ prefix to symbol names. But my crate is producing a static library containing those symbols. So I'm getting unresolved symbol errors when attempting to link because python3-sys is looking for __imp_ prefixed symbols and my crate isn't.

I'm, uh, not sure if I can override #[link] attributes with a cargo:rustc-link-lib that forces that library to be static, even if #[link] isn't declaring kind = "static".

I'm really trying to avoid having to hack up the python3-sys crate to accommodate my wonky linking situation. I want *-sys crates to declare symbols and not let their opinions on how linking usually works to prevent me from linking things in new and novel ways.

@indygreg
Copy link
Author

indygreg commented Jan 3, 2019

I've started to go down the rabbit holes at rust-lang/rust#27438 and rust-lang/rust#37403 and suspect this issue has been discussed there. Although I have yet to excavate the holes deeply enough to see if someone has mentioned exactly my use case. I'm starting to think I should have filed this issue under rust-lang/lang instead of cargo...

@indygreg
Copy link
Author

indygreg commented Jan 4, 2019

OK. So it seems Rust wants to add dllimport to every extern by default. This means that you need to export symbols with __declspec(dllexport) to get the symbol names to line up. But, even if we do this, you can't just have everything statically link together because the COFF object files or .lib archive containing COFF object files are linked with the rlib using dllimport and there is a symbol mismatch!

If you use king="static", this prevents the symbol munging and should theoretically work. However, Rust insists on finding the static library to include it in the rlib archive. But since the library doesn't exist yet, that fails.

I found rust-lang/rust#37403 and the kind="static-nobundle" annotation. That seems to bypass the dllimport and symbol name munging as well as the requirement that the library be archived in the rlib. This is exactly what I want!

Unfortunately kind="static-nobundle" isn't stable. But at least I'm unblocked.

@alexcrichton
Copy link
Member

Thanks for the report here, and the detail! I'll admit though that after reading this over I'm a bit lost still, but I'm glad to hear that static-nobundle solves your issues! That's been unstable for quite some time now, but I wonder if that's the best way to solve this? (stabilizing that upstream)

@indygreg
Copy link
Author

indygreg commented Jan 9, 2019

Yeah, the issue is kind of a rambling train wreck. Sorry about that. I'm not exactly sure what I'm trying to ask for either!

But with a few days detached from me filing this, I think the following would definitely help:

  1. Making static-nobundle stable so you can force lazy symbol resolution without requiring a library be bundled with the rlib.

  2. Supporting a way to remove a a link/library dependency from a dependent crate. My comment suggested (and I found an old RFC somewhere also mentioning it) that the rustc-link-lib:LIBNAME: syntax could be used to accomplish this.

  3. Better documentation on how link/library dependencies/annotations are handled. e.g. what's evaluated at crate build time, when can other crates in the build hierarchy override things, when linkage types map to symbol munging (e.g. __declspec(dllimport), when during builds a library is searched for. It took me a while to realize that the linkage type implied __declspec(dllimport) on Windows as well as the rules for locating the library and /packaging/ it in the rlib. If I knew exactly what the semantics of each linkage type implied, I could have saved a lot of time!

  4. More flexibility (or maybe better documentation if the features exist) to modify the link/library settings of dependent crates. Not all crate authors may want to support a mode that e.g. forces static-nobundle behavior. I think it is unrealistic for special use-cases like mine with PyOxidizer to require all dependent crates to opt in to supporting these edge cases. Crate authors will drag their feet. I want the ability to easily override aspects of dependent crates without e.g. having to modify their build scripts or other code. https://doc.rust-lang.org/cargo/reference/build-scripts.html#overriding-build-scripts documents what is possible today. But I don't think it is adequate because a) it only applies if manifest contains a links key b) it bypasses the build.rs altogether. I think I want something that a) is always applicable b) still runs the build script c) allows modifying the build script output. I'm thinking along the lines of features like remove build script output lines if they match a pattern and supplement the build script output lines with these lines.

@alexcrichton
Copy link
Member

That all makes sense to me! I definitely agree that we basically always need more documentation about linking and how things work in Rust, we've mostly just relied on how things tend to "mostly work" today and the edge cases seem to be few and far between! (but as you've seen are pretty awful once you hit them).

Want to poke at the static-nobundle tracking issue and see if someone would like to FCP it for stabilization? It may also be good to open issues on rust-lang/rust to document the various behaviors here?

@ehuss ehuss added A-linkage Area: linker issues, dylib, cdylib, shared libraries, so A-documenting-cargo-itself Area: Cargo's documentation labels Jan 31, 2019
@epage epage added the S-triage Status: This issue is waiting on initial triage. label Oct 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-documenting-cargo-itself Area: Cargo's documentation A-linkage Area: linker issues, dylib, cdylib, shared libraries, so C-feature-request Category: proposal for a feature. Before PR, ping rust-lang/cargo if this is not `Feature accepted` S-triage Status: This issue is waiting on initial triage.
Projects
None yet
Development

No branches or pull requests

4 participants