Search code examples
rustlinkerld

Using an ld version script in a cdylib Rust crate


I am attempting to use a version script while building a cdylib Rust crate, however I am running into issues due to the anonymous version script created by the Rust compiler. I followed this forum post on how to add a version script, but they never mentioned this issue.

Execution

I'm using cargo-make to build my project. In my Makefile.toml I have this task:

[tasks.build]
toolchain = "nightly"  # Running with nightly-x86_64-unknown-linux-gnu
command = "cargo"
args = ["rustc", "--release", "-p", "my_crate", "--", "-C", "link-args=-Wl,--version-script=versions.map"]

Upon running cargo make build, that task executes this build command.

rustup run nightly cargo rustc --release -p my_crate -- -C link-args=-Wl,--version-script=versions.map

Error

However, it keeps producing this error. From what I can tell, my version script (shown below) conflicts with an anonymous version script that gets generated by Rust (/tmp/rustcyXUHTy/list in the error). Unfortunately, the version script Rust generates is deleted immediately after creation so I don't actually know what it looks like. I attempted to follow this answer to view the other version script, but it was deleted too quickly and I was unable to see the output.

error: linking with `cc` failed: exit status: 1
  |
  = note: "cc" "-Wl,--version-script=/tmp/rustcyXUHTy/list" ... "-Wl,--version-script=versions.map"
  = note: /usr/bin/ld: anonymous version tag cannot be combined with other version tags
          collect2: error: ld returned 1 exit status

Rust

// I'm not completely sure which tags should be used and so far they have had no effect on the error
// #[no_mangle]
// #[export_name = "foo"]
pub unsafe extern "system" fn foo() {}

// The crate also contains other functions which are not covered by my version script
// I tried removing all of the other #[no_mangle] functions, but it had no effect
#[no_mangle]
pub unsafe extern "system" fn bar() {}

Version Script

I'm not very experienced writing version scripts so this is the simple test script I came up with. The final product will use a similar version script from an existing C project.

Project_1.0 {
    global:
        foo;
};

Solution

  • Solution provided by itamarst on the Rust forums.

    Explanation

    As shown in the question, ld does not support multiple version scripts. However, lld does so we can use that instead. (Can be installed with sudo apt install lld on ubuntu). To use lld instead of ld, pass -Clink-arg=-fuse-ld=lld to rustc.

    However, this is not enough on its own. The version script Rust generates will take precedence and the version node will not be applied as specified in our version script. To get around this, functions can be given a temporary name and a new symbol can be linked to it via linker args (--defsym). In the version script the new symbol can be freely used and the original function name can be marked as local to prevent a duplicate symbols from being uploaded.

    Rust Code

    // Name function foo_impl and rename it on the command line
    #[no_mangle]
    pub unsafe extern "system" fn foo_impl() {}
    

    Version Script

    Project_1.0 {
        global:
            foo;
        local:
            foo_inner;
    };
    

    Building

    In a cdylib all of the rust/linker arguments can be configured in the build.rs.

    // Tell Rust to use lld instead of ld
    println!("cargo:rustc-cdylib-link-arg=-fuse-ld=lld");
    
    // Set version script path
    println!("cargo:rustc-cdylib-link-arg=-Wl,--version-script=mapfile");
    
    // Rename symbols to get around the anonymous version script
    for symbol in &["foo"] {
        println!("cargo:rustc-cdylib-link-arg=-Wl,--defsym={}={}_impl", symbol, symbol);
    }
    

    Alternatively, all of these arguments can be passed on the command line.

    cargo rustc -- -Clink-arg=-fuse-ld=lld -Clink-args=-Wl,--defsym=foo=foo_impl,--version-script=mapfile