rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
98.35k stars 12.72k forks source link

Correctly handle dllimport on Windows #27438

Open alexcrichton opened 9 years ago

alexcrichton commented 9 years ago

Currently the compiler makes basically no attempt to correctly use dllimport. As a bit of a refresher, the Windows linker requires that if you're importing symbols from a DLL that they're tagged with dllimport. This helps wire things up correctly at runtime and link-time. To help us out, though, the linker will patch up a few cases where dllimport is missing where it would otherwise be required. If a function in another DLL is linked to without dllimport then the linker will inject a local shim which adds a bit of indirection and runtime overhead but allows the crate to link correctly. For importing constants from other DLLs, however, MSVC linker requires that dllimport is annotated correctly. MinGW linkers can sometimes workaround it (see this commit description.

If we're targeting windows, then the compiler currently puts dllimport on all imported constants from external crates, regardless of whether it's actually being imported from another crate. We rely on the linker fixing up all imports of functions. This ends up meaning that some crates don't link correctly, however (see this comment: https://github.com/rust-lang/rust/issues/26591#issuecomment-123513631).

We should fix the compiler's handling of dllimport in a few ways:

I currently have a few thoughts running around in my head for fixing this, but nothing seems plausible enough to push on.

EDIT: Updated as @mati865 requested here.

alexcrichton commented 9 years ago

@Diggsey, @angelsl

I think he's referring to compiler plugins rather than application plugins.

I am indeed! (as I mentioned above as well)


@retep998

In the case of cross-compiling wouldn't crates that are used in plugins already be compiled twice?

Unfortunately I think the problem still comes up with build scripts. Both plugins and build scripts are always compiled for the host architecture, and build scripts are linked statically while plugins are dynamic, so if a build script and a plugin share a dependency it's the same problem.

For normal dependencies, however, yes Cargo will compile it twice.

Shouldn't doing something similar in the non-cross case also be okay in order to correctly apply dllimport?

In theory, yeah, but this is something that'd probably need to be designed from both a Cargo and compiler perspective.


@angelsl

Actually, making Cargo compile the dependency twice in the same run seems like the only proper solution here.

I agree in that this is the only solution I know of here. This is just a fundamental problem with the way Cargo compiles dependencies and how they're linked together. I'm wary of starting to do crazy windows-specific things without reconsidering how it affects other platforms (e.g. making tons of windows-specific or unix-specific bugs), but I don't mean to reject anything.


@arielb1, @Diggsey

So we have x86-pc-msvc-static and x86-pc-msvc-dynamic targets?

I agree that this is more of a compilation-option rather than a target-triple option, I would also prefer to not mess with triples.

angelsl commented 9 years ago

@alexcrichton

Alright, fair enough.

I believe @retep998 meant something similar to treating them as two targets, just not actually messing with the triples.

retep998 commented 9 years ago

Thinking about the current situation, it is already incredibly hairy as soon as you start mixing in dylibs using Cargo because now every single dylib and binary has to make sure they use -Cprefer-dynamic otherwise you'll get duplicate std errors. Not only that but any dependencies which are in common between two dylibs or a dylib and a binary also have to be built as dylibs or they'll be duplicates as well.

Are there any actual real world situations where Cargo is successfully being used with some dependencies being linked in as dylibs? Would it be okay if Cargo made mixing of static and dynamic linking illegal or is there a valid use case that we'd want to preserve?

Also I think it is really silly that plugins are treated as normal dependencies, so Cargo has to assume that all dependencies will be linked in even if they actually get used as plugins instead. This really hamper's Cargo's ability to be smart about how to handle such dependencies. Also unfortunate that we can't easily add special support for plugins to Cargo.toml since it would be insta-stable because Cargo has no notion of stability, which isn't optimal considering plugins are unstable.

alexcrichton commented 9 years ago

@retep998

I'm ok saying mixing dynamic and static linking is fine to forbid (so long as we have a good reason) because I highly doubt it happens in the wild today. It's quite rare to specifically request a dylib as an intermediate artifact, and otherwise you either get it because you're a plugin.

Also, are you familiar with Cargo's support for plugins? Your claims that Cargo assumes plugins are linked and how Cargo can't be smart isn't quite correct. Cargo knows exactly which crates are plugins to know which chains of dependencies should be compiled for the host architecture instead of the target.

retep998 commented 9 years ago

@alexcrichton

Oh, there is a plugin field in the [lib] section of Cargo.toml. Never noticed that before. Well, I guess that means Cargo does know everything it needs to know. In which case all we'd have to do is forbid mixing of rlib/dylib, make sure the plugin and all its dependencies are built and linked as dylibs, and make sure the dependencies of the binaries are built and linked as rlibs (unless someone decides to do everything as dylib instead because they want to have their own application plugins). Then rustc would know whether upstream dependencies will be linked dylibs or rlibs when doing code gen and dllimport can be applied precisely and correctly.

retep998 commented 9 years ago

Cargo does not currently build dependencies of plugins as dylibs. So Cargo would first have to be modified to build all dependencies of plugins as dylibs. This would result in a shared dependency being built twice, once as an rlib for the binary, and again as a dylib for the plugin. Ideally rustc would be able to do it as a single compilation, only internally duplicating the code generation steps when there's a split between whether to use dllimport or not, but that could potentially be complicated so two separate rustc invocations is the easier way for now. Also -Cprefer-dynamic would be passed when compiling each dylib.

Once Cargo has been modified then rustc can be taught to always use dllimport when referring to an upstream dylib and never use dllimport when referring to an upstream rlib. It would also be nice to store metadata on this so that rustc doesn't later try to link against the wrong thing as a sanity check.

KindDragon commented 8 years ago

JFI GCC suggest always use special C++ visibility attributes https://gcc.gnu.org/wiki/Visibility

#if defined _WIN32 || defined __CYGWIN__
  #ifdef BUILDING_DLL
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllexport))
    #else
      #define DLL_PUBLIC __declspec(dllexport) // Note: actually gcc seems to also supports this syntax.
    #endif
  #else
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllimport))
    #else
      #define DLL_PUBLIC __declspec(dllimport) // Note: actually gcc seems to also supports this syntax.
    #endif
  #endif
  #define DLL_LOCAL
#else
  #if __GNUC__ >= 4
    #define DLL_PUBLIC __attribute__ ((visibility ("default")))
    #define DLL_LOCAL  __attribute__ ((visibility ("hidden")))
  #else
    #define DLL_PUBLIC
    #define DLL_LOCAL
  #endif
#endif
upsuper commented 8 years ago

@nikomatsakis

static variables are unusual

FWIW, this is probably not true. Stylo extensively uses static variables to reference string atoms from Gecko side (see this script), so it could potentially be affected. Also, I see tons of warnings like

geckoservo.lib(style.0.o) : warning LNK4217: locally defined symbol ?rubyTextContainer@nsCSSAnonBoxes@@2PEAVnsICSSAnonBoxPseudo@@EA (public: static class nsICSSAnonBoxPseudo * nsCSSAnonBoxes::rubyTextContainer) imported in function _ZN5style19gecko_selector_impl13PseudoElement9from_atom17h219208fbc1c0455dE

when linking Gecko with Servo on Windows.

retep998 commented 8 years ago

@upsuper Those warnings mean that static is being referenced with dllimport despite the static being statically linked in and not actually imported from a DLL. Ideally Rust would know whether to apply dllimport and you wouldn't see those warnings.

Ericson2314 commented 8 years ago

https://github.com/rust-lang/rfcs/pull/1717 should fix this? (I didn't see it fixed)

Aatch commented 8 years ago

@Ericson2314 did you mean #31717? Since the issue you mentioned is about changing the precedence of as...

Ericson2314 commented 8 years ago

Sorry, meant to link an RFC. Fixed now.

retep998 commented 8 years ago

@Ericson2314 It will fix the situation for FFI. However it doesn't apply to inter-crate linking as Rust still doesn't know whether a dependency will be linked as an rlib or a dylib until link time, when it needs to know it at code generation time.

retep998 commented 7 years ago

As an extreme solution to the problem of dllimport/dllexport between crates, we can ditch the dylib crate type completely. Thus rust crates will always be statically linked together so we can simply never apply dllimport or dllexport.

FFI currently has this working mostly thanks to kind=dylib applying dllimport and kind=static-nobundle not applying dllimport. We just need kind=static-nobundle to be stabilized (or to replace kind=static wholesale).

mati865 commented 4 years ago

Windows-gnu targets now also use dllimport/dllexport for various reasons: https://github.com/rust-lang/rust/pull/72049 Can we update this issue?

nikomatsakis commented 4 years ago

@mati865 do you mean to update the top comment? if you want to supply some replacement text, I can insert it (presumin you don't have the required perms)

mati865 commented 4 years ago

@nikomatsakis yeah, my proposed changes:

Thanks in advance.

eerii commented 1 month ago

I have been struggling with dllimport for the past week, and the current limitations, this is just meant as an overview so that other people that face this issue may understand the current status. Please correct me if I understood anything wrong.

Using the #[link] attribute is fine in most cases. However, when using something like system-deps to get and link the system libraries on the build script using pkg-config files, in the best case it is redundant, but in the worse it gets in the way. This is because if the library has a different name on the system, or if you want to link to a different library (in our case, we want to use gstreamer-full to replace all of the individual glib, gobject, gstreamer-* and so on), the link attribute still links the one with the previous name as well.

It is possible to rename the libraries from the build script, but it is hard to do so from dependent crates, as described in rust-lang/cargo#6519. The most viable way is using links and overriding build scripts. However, its support is very restricted at the moment, forcing to use specific targets and not allowing cfg options (rust-lang/cargo#11042).

While it may not be feasible to automatically detect the symbols in which to apply dllimport when not using a #[link] attribute, there are some suggestions that might make it easier to work with:

To summarize, the main issue right now is that linking to global variables doesn't work on Windows without using #[link], and this can cause trouble when linking against alternate libraries.

bjorn3 commented 1 month ago

Would it be possible for the build script to set an env var with the name of the library and then use #[link(name = env!("LIBNAME"))]? You need #[link] outside of Windows too of you are building a dylib which links against a C staticlib for rustc to correctly export all imported symbols of the staticlib from the dylib of they may be used outside of the dylib.

eerii commented 1 month ago

Would it be possible for the build script to set an env var with the name of the library and then use #[link(name = env!("LIBNAME"))]?

Is it possible to use env! on an attribute? I thought it was not supported yet (rust-lang/rust#52393). Having support for that would be another possible workaround for this.

You need #[link] outside of Windows too of you are building a dylib which links against a C staticlib for rustc to correctly export all imported symbols of the staticlib from the dylib of they may be used outside of the dylib.

No, in our case #[link] is only needed in Windows for correctly calling dllimport on the symbols when the conditions are right. The rest of the linking happens withing the build script, using system-deps which ultimately calls rustc-link-lib.

ChrisDenton commented 1 month ago

I believe the specific issue of setting the import library name separately could be adequately solved by allowing a link kind without a name (your second option):

// Without a `name`, only `kind` is allowed here
// This would be the equivalent of using `__declspec(dllimport)` in Windows C/C++
#[link(kind = "dylib")]
extern "C" {
    ...
}

// kind=static is redundant here but allowed for consistency
// other kinds are not allowed
#[link(kind = "static")]
extern "C" {
    ...
}

From what I can see, putting a name on an import block does not in any way connect that name to the functions or statics used in the extern block (unless using raw-dylib). That means a (not great) workaround to implement the above in stable Rust is:

// `empty` is just a lib file with no contents that we add to the search path.
// Even more hackily, we could use a well known lib name like "kernel32" which almost certainly exists.
// Either way this allows the real lib name to be supplied separately.
#[link(name = "empty", kind = "dylib")] // The kind here is redundant 
extern "C" {
    ...
}
eerii commented 1 month ago

I believe the specific issue of setting the import library name separately could be adequately solved by allowing a link kind without a name

The issue that I can see with this would be how to override the linking type on the build script. If you have a name you can rename the library and change the type later with rustc-link-lib, but in this case, how would you change the linking type later?

From what I can see, putting a name on an import block does not in any way connect that name to the functions or statics used in the extern block (unless using raw-dylib). That means a (not great) workaround to implement the above in stable Rust is:

That's not the worst workaround, but it relies on having certain libraries installed which may not be the case for all users.

ChrisDenton commented 1 month ago

That's not the worst workaround, but it relies on having certain libraries installed which may not be the case for all users.

You can supply an empty.lib file (or libempty.a for mingw) just using a build script without the user needing to have anything installed. Edit: though admittedly, it is a hack.

The issue that I can see with this would be how to override the linking type on the build script. If you have a name you can rename the library and change the type later with rustc-link-lib, but in this case, how would you change the linking type later?

True, if you need to switch between dylib and static you'd need to use #[cfg_attr] to set the #[link] line with a custom cfg (rather than a Cargo feature).

ChrisDenton commented 1 month ago

Oh actually, if you always override the name then it doesn't have to actually exist. I tried name = "<DOESNOTEXIST>" and it worked with an override.

eerii commented 1 month ago

That's not a bad workaround actually, it beats linking directly to the __imp_* symbol and manually dereferencing it conditionally. However, the issue with renaming the library is the same as before, it is harder to do from upstream crates, since the instruction has to come from the build script of the crate. You can't pass env variables conditionally, and links overrides are also not very configurable. But using an empty library generated in build.rs like you first suggested may be a reasonable workaround in the meantime. In our project, we ended up being able to remove the one problematic variable and replace it with a function call, but this is not ideal and the problem still persists in other instances.