Rust: Using LTO while building as static library causes conflicts

Created on 4 Sep 2017  ·  10Comments  ·  Source: rust-lang/rust

If you build your Rust project as a static library to link it into a C codebase, you usually want to crank up the optimizations, as also indicated by the librsvg blogpost (towards the end):

https://people.gnome.org/~federico/blog/librsvg-build-infrastructure.html

This is all fine, until you want to link in another library built with Rust, which is very likely if you want to incrementally introduce Rust into your codebase. At that point the linker will start complaining about collisions caused by symbols defined by the standard library:

./liblib2.a(lib2-91c10be1e28e3b68.0.o): In function `std::heap::__default_lib_allocator::__rdl_alloc':
/checkout/src/libstd/heap.rs:31: multiple definition of `__rdl_alloc'
./liblib1.a(lib1-25b44f601e0ef586.0.o):/checkout/src/libstd/heap.rs:31: first defined here
./liblib2.a(lib2-91c10be1e28e3b68.0.o): In function `__rdl_alloc_excess':
lib2.cgu-0.rs:(.text.__rdl_alloc_excess+0x0): multiple definition of `__rdl_alloc_excess'
./liblib1.a(lib1-25b44f601e0ef586.0.o):lib1.cgu-0.rs:(.text.__rdl_alloc_excess+0x0): first defined here
./liblib2.a(lib2-91c10be1e28e3b68.0.o): In function `__rdl_alloc_zeroed':
lib2.cgu-0.rs:(.text.__rdl_alloc_zeroed+0x0): multiple definition of `__rdl_alloc_zeroed'
./liblib1.a(lib1-25b44f601e0ef586.0.o):lib1.cgu-0.rs:(.text.__rdl_alloc_zeroed+0x0): first defined here
./liblib2.a(lib2-91c10be1e28e3b68.0.o): In function `__rdl_dealloc':
lib2.cgu-0.rs:(.text.__rdl_dealloc+0x0): multiple definition of `__rdl_dealloc'
./liblib1.a(lib1-25b44f601e0ef586.0.o):lib1.cgu-0.rs:(.text.__rdl_dealloc+0x0): first defined here
./liblib2.a(lib2-91c10be1e28e3b68.0.o): In function `__rdl_grow_in_place':
lib2.cgu-0.rs:(.text.__rdl_grow_in_place+0x0): multiple definition of `__rdl_grow_in_place'
./liblib1.a(lib1-25b44f601e0ef586.0.o):lib1.cgu-0.rs:(.text.__rdl_grow_in_place+0x0): first defined here
./liblib2.a(lib2-91c10be1e28e3b68.0.o): In function `__rdl_oom':
lib2.cgu-0.rs:(.text.__rdl_oom+0x0): multiple definition of `__rdl_oom'
./liblib1.a(lib1-25b44f601e0ef586.0.o):lib1.cgu-0.rs:(.text.__rdl_oom+0x0): first defined here
./liblib2.a(lib2-91c10be1e28e3b68.0.o): In function `__rdl_realloc':
lib2.cgu-0.rs:(.text.__rdl_realloc+0x0): multiple definition of `__rdl_realloc'
./liblib1.a(lib1-25b44f601e0ef586.0.o):lib1.cgu-0.rs:(.text.__rdl_realloc+0x0): first defined here
./liblib2.a(lib2-91c10be1e28e3b68.0.o): In function `__rdl_realloc_excess':
lib2.cgu-0.rs:(.text.__rdl_realloc_excess+0x0): multiple definition of `__rdl_realloc_excess'
./liblib1.a(lib1-25b44f601e0ef586.0.o):lib1.cgu-0.rs:(.text.__rdl_realloc_excess+0x0): first defined here
./liblib2.a(lib2-91c10be1e28e3b68.0.o): In function `__rdl_shrink_in_place':
lib2.cgu-0.rs:(.text.__rdl_shrink_in_place+0x0): multiple definition of `__rdl_shrink_in_place'
./liblib1.a(lib1-25b44f601e0ef586.0.o):lib1.cgu-0.rs:(.text.__rdl_shrink_in_place+0x0): first defined here
./liblib2.a(lib2-91c10be1e28e3b68.0.o): In function `__rdl_usable_size':
lib2.cgu-0.rs:(.text.__rdl_usable_size+0x0): multiple definition of `__rdl_usable_size'
./liblib1.a(lib1-25b44f601e0ef586.0.o):lib1.cgu-0.rs:(.text.__rdl_usable_size+0x0): first defined here
./liblib2.a(lib2-91c10be1e28e3b68.0.o): In function `rust_eh_personality':
lib2.cgu-0.rs:(.text.rust_eh_personality+0x0): multiple definition of `rust_eh_personality'
./liblib1.a(lib1-25b44f601e0ef586.0.o):lib1.cgu-0.rs:(.text.rust_eh_personality+0x0): first defined here

This only happens if you set lto = true.

A-linkage C-bug T-compiler

Most helpful comment

@alexcrichton @TimNN any idea what affects the compilation of the rust_eh_personality symbol specifically? There is this: https://github.com/rust-lang/rust/blob/master/src/librustc_codegen_ssa/back/symbol_export.rs#L118

I'm trying to figure out if there is a way to get this symbol definition built as part of a separate object file, like what happens when it gets built as part of panic_unwind.o (+ the name mangling). I am pretty sure this would get rid of the duplicated symbol issue once and for all.

It may seem like a hack, but it would actually work quite well. The recommendation would be to build with the same version of Rust to avoid potential issues with symbols that have slight differences, but this is acceptable in my mind. It is very similar to building static libraries against a compatible MSVC static runtime on Windows, except we're talking about the Rust core libraries here.

This type of issue needs to be fixed for cases where shared libraries are not a suitable option, like iOS. It is only going to become more common as more C libraries get rewritten to use Rust under the hood, causing conflicts when two of such libraries get linked together.

All 10 comments

I don't think linking two rust static libraries into the same binary is actively supported today, see also @alexcrichton's https://github.com/rust-lang/rust/issues/44283#issuecomment-328181342 in another issue.

Still marking as a bug for the moment since it works without LTO.

@CryZe can you provide a script or repository to reproduce this? In theory linking together two staticlibs with LTO should work, although there may be bugs preventing it from doing so. In the long run you probably don't want to do this, though, as each Rust library you link in will bring in a new copy of the standard library, which ideally everyone would share.

Triage: Hey @CryZe , are you still seeing this? Alex asked for a reproduction a while ago, if we can't reproduce, then we can't fix this :)

Alright I made a repository here: https://github.com/CryZe/multi-lib-lto-rust-bug-repro
Just run make and it should fail:

gcc main.c -Llib1/target/release/ -Llib2/target/release/ -llib1 -llib2 -lpthread -ldl
lib2/target/release//liblib2.a(lib2-929bb870274e5eac.lib2.8t8q9ckg-cgu.0.rcgu.o): In function `rust_eh_personality':
/rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b//src/libpanic_abort/lib.rs:106: multiple definition of `rust_eh_personality'
lib1/target/release//liblib1.a(lib1-d10e44ca50aa3d7b.lib1.eqtft63h-cgu.0.rcgu.o):/rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b//src/libpanic_abort/lib.rs:106: first defined here
collect2: error: ld returned 1 exit status
Makefile:2: recipe for target 'a.out' failed
make: *** [a.out] Error 1

I am also investigating this issue. I did a bunch of tests, and it looks like lto = true is the only way to avoid having the __rdl_alloc symbols defined without name mangling. lto = false or lto = "off" both get them defined and cause the conflict. While my primary concern is with iOS, I did notice that the multi-lib-lto-rust-bug-repro doesn't link on macOS because of a symbol conflict on _rust_eh_personality. This symbol specifically doesn't seem to be affected by the lto option :/

@CryZe did you get any further on this? We are desperate for a workaround to make it work on iOS, where using shared libraries is not an option :(

I came up with a "solution" that is wrong on so many levels, but at least makes the @CryZe reproduction sample link:

PKG_NAME=lib1
cd $PKG_NAME
cargo clean
cargo build --release
cd target/release
ar x lib${PKG_NAME}.a
LANG= sed -i ‘’ “s/rust_eh_personality/rust_eh_personaliti/g” $(ls ${PKG_NAME}-*.rcgu.o)
ar cr lib${PKG_NAME}.a *.o

This effectively builds the first library, extracts the object files, does a search & replace on "rust_eh_personality" to rename it to "rust_eh_personaliti", and rebuilds the static library from the object files. By forcing the symbol change on one of the two libraries, the symbol conflict is avoided.

Now this is obviously a very ugly hack, and I don't know the impact of forcing a symbol name change on rust_eh_personality. Technically, does it need to keep this name, or could it be name mangled like the rest?

After digging through some older tickets, I found https://github.com/rust-lang/rust/issues/43415 and it mentioned a different result when building in debug vs release. I modified the reproduction sample to build in debug, and there is no linker issue. It does significantly change the layout of internal object files and where the symbols are (extracting the .a with 'ar' to see where symbols are).

One thing I noticed is that the linker doesn't complain about duplicated symbols if they are identical, and inside an object file of the same name (and possibly identical in totality I'm guessing). When building in debug, rust_eh_personality is defined in the same object file for both lib1 and lib2:

panic_unwind-bf192f19d9586a96.panic_unwind.67c9yyht-cgu.0.rcgu.o

When built in release, rust_eh_personality is defined in lib1-93605d7bc2291864.lib1.1hx46jnd-cgu.0.rcgu.o and lib2-3edec0329c3e4919.lib2.64tkgp8c-cgu.0.rcgu.o respectively. We decompiled the symbols to see if they were identical, and they are in our case (empty function stubs) but the linker sees them as different. I'm pretty sure that if rust_eh_personality was compiled inside an object file of the same name like what happens when you build in debug, the linker problem would go away.

This would still mean that in order to get things to build and link correctly, one should use exactly the same version of Rust to avoid issues, but it would still be a much better option that being unable to link multiple static libraries together.

Nevermind about building as debug as a way to get it working, as it fails the same way as in release mode when using panic="abort":

[profile.dev]
panic = "abort"
lto = true

@alexcrichton @TimNN any idea what affects the compilation of the rust_eh_personality symbol specifically? There is this: https://github.com/rust-lang/rust/blob/master/src/librustc_codegen_ssa/back/symbol_export.rs#L118

I'm trying to figure out if there is a way to get this symbol definition built as part of a separate object file, like what happens when it gets built as part of panic_unwind.o (+ the name mangling). I am pretty sure this would get rid of the duplicated symbol issue once and for all.

It may seem like a hack, but it would actually work quite well. The recommendation would be to build with the same version of Rust to avoid potential issues with symbols that have slight differences, but this is acceptable in my mind. It is very similar to building static libraries against a compatible MSVC static runtime on Windows, except we're talking about the Rust core libraries here.

This type of issue needs to be fixed for cases where shared libraries are not a suitable option, like iOS. It is only going to become more common as more C libraries get rewritten to use Rust under the hood, causing conflicts when two of such libraries get linked together.

Was this page helpful?
0 / 5 - 0 ratings