#[link] attribute on extern "Rust" block

I'm trying to figure out how to add load time dll dependency to a rust binary. I managed to get it working with the "C" ABI as expected by just linking against the import library and defining the symbols in an extern block like this.

#[link(name = "plugin.dll", kind="dylib")]
unsafe extern "C" {
	pub fn print_test();
}

I wanted to try doing the same with the extern "Rust" abi (the dll is from a rust crate). But when i tried to use the "Rust" ABI , the link attribute is ignored which causes a missing symbol error from the linker.

#[link(name = "plugin.dll", kind="dylib")]
unsafe extern "Rust" {
	pub fn print_test();
}

Warning:

warning: attribute should be applied to an `extern` block with non-Rust ABI
  --> base\src\main.rs:17:1
   |
17 | #[link(name = "plugin.dll", kind="dylib")]

Error:

error LNK2019: unresolved external symbol print_test referenced in function _ZN4base4main17h83576916a4b36923E

Why can't I link against an import library with extern "Rust". Is there any other way around this that still preserves plugin.dll as a load time dependency (i.e. not using LoadLibrary and GetProcAddress)?

Note

I know extern "Rust" is unstable and i shouldn't rely on it for anything important. I'm just doing this to learn.
I've tried using extern "Rust" with runtime loading before and it worked ok enough for me. I just wanted to try the same with a load time dll because it requires less boilerplate from the main crate.

If you have a rust dylib, you should handle it as a regular crate dependency and not use extern blocks at all. Rust mangles symbols by default and looks up the right name in the crate metadata, which is only loaded if you use the dylib as a regular dependency.

Just to be clear, it is not just unstable across rustc versions, but may also change whenever you make any change to the dylib itself. For example because a function you call is codegened when compiling the calling crate rather than the dylib because it is cross-crate inlineable (due to #[inline] or because rustc thinks it is small enough to automatically mark it as inlineable) or it is a generic function.

2 Likes

I believe the reason that you're seeing a linker error is that extern "C" doesn't do name mangling, but extern "Rust" does do name mangling by default. Your code should work if you put #[no_mangle] on both sides (or #[link_name] and #[export_name]).

But as said before, the proper way to do load-time Rust dependencies (independent of them being static or dynamic) is with cargo dependencies.

The main reason i wanted to try this was because my compile times were becoming unusably long (30 seconds minimum if only the final crate changed). If i had a dll as a normal dependency, it would have to recompile the final binary if any dependency changed (for the reasons you mentioned), which is not the case if the dll is not a normal dependency.

I know that means i have to use #[unsafe[no_mangle]) and can't use generics or allocations. I just wanted to be able to directly use normal rust types like slices and enums across the dll boundary. But it seems like rust just doesn't allow you to do that.

Additionally, it looks like "dylib" crates export ALL used symbols from the standard library when they are built (unlike "cdylibs" which only export pub functions). So even if you try manually linking the import library with rustc, you get duplicate symbol errors, so there really does seem to be no way to do what i want :/.

Which, of course, requires support for generics and allocations, at least, in some cases.

No, Rust is not designed for that. It can be fixed, of course, swift did that, after all… but I doubt project that would become a few man-years of work was something that you were thinking about.

There are some possible approaches to do this:

Note that I haven't tried either option, and both approaches comes with caveats.

It might be better in the long term to see if you can refactor for better build times, there are numerous resources on this, but some good starting points are:

If like you said your final crate is the slowest, look into why that is. Perhaps some things could be broken out of it, into several crates that are independent of each other. Perhaps you use generics from earlier levels in the dependency graph, and all the work of instantiating those ends up in the final crate (consider dyn instead of impl, consider instantiating in other crates).