mmastrac/rust-ctor

ctor not running for statically linked libraries

fredericvauchelles opened this issue · 15 comments

Hi,

I have an issue with the following setup:

  • app crate defines the binary to produce
  • lib crate defines a rust standard library

When using #[ctor] attribute inside the crate lib, it is not called when running the binary built with app.

Using a rust library dependency statically link it so it should also include the ctor function, but it does not seems to be the case.

Am I missing something or is this an unsupported use case?

Could you confirm which version you are using (ie: paste your Cargo.toml here)?

I tried to have a minimal repro and investigated further, and I resulted in a behaviour I don't understand.

The repro is attached to this comment.
These are the main files:

app/src/main.rs

fn main() {
    println!("Main execution");
    #[cfg(feature = "assert")]
    assert_eq!(1, unsafe {
        lib::VALUE.load(std::sync::atomic::Ordering::Acquire)
    });
}

app/Cargo.toml

[package]
name = "app"
version = "0.1.0"
edition = "2018"

[dependencies]
lib = { path = "../lib" }

lib/src/lib.rs

#[macro_use]
extern crate ctor;
#[macro_use]
extern crate libc_print;

use std::sync::atomic::{AtomicUsize, Ordering};

pub static mut VALUE: AtomicUsize = AtomicUsize::new(0);

#[ctor]
fn startup() {
    unsafe {
        VALUE.fetch_add(1, Ordering::AcqRel);
    }
    libc_print!("Startup lib\r\n");
}

#[dtor]
fn tear_down() {
    libc_print!("Tear down lib\r\n");
}

fn unused() {
    let _ = unsafe { VALUE.load(Ordering::Acquire) };
}

lib/Cargo.toml

[package]
name = "lib"
version = "0.1.0"
authors = ["Frédéric Vauchelles <fredpointzero@gmail.com>"]
edition = "2018"

[dependencies]
libc-print = "0.1.8"
ctor = "0.1.10"

This is the strange behaviour I get:

  1. cargo run --package app --bin app will show only Main execution in the console.
  2. Comment the #[cfg(feature = "assert")] feature in the main.rs file
  3. cargo run --package app --bin app will succeed

So this is weird, I can't tell if this is working when it does not access the static variable.

Also, I have my issue when using the crate inventory in a bigger project, but I will try to have a minimal repro in that case.

cargo 1.39.0-nightly (22f7dd049 2019-08-27)
rustc 1.39.0-nightly (dfd43f0fd 2019-09-01) (nightly-x86_64-pc-windows-gnu)
rustup 1.19.0 (2af131cf9 2019-09-08)

and
cargo 1.37.0 (9edd08916 2019-08-02)
rustc 1.37.0 (eae3437df 2019-08-13) (stable-x86_64-pc-windows-msvc)
rustup 1.19.0 (2af131cf9 2019-09-08)

test3-rs.zip

In my project using the inventory crate, I run into the same behaviour.

The ctor and dtor of the library project are not executed until I access a static variable defined in the library from the main application.

Interesting. I wonder if this is an unused symbol getting pruned. Thanks for the repro - I'll poke around.

I confirmed this is definitely an issue. Looks like there's some sort of whole-module pruning going on.

Hi, do you have any updates on this?

Nothing yet. I was able to repro it with your steps, but I feel like this might be an LLVM/rustc bug.

Hi, do you have any updates on this? Is this an LLVM/rustc bug as you suggested?

I can reproduce this on macOS Mojave and Rust 1.39.0 as well. However, it seems that I am able to get the constructor to run at least with the following minimal application:

//! lib.rs

#[ctor::ctor]
fn on_startup() {
    println!("Starting up!");
}

#[ctor::dtor]
unsafe fn on_shutdown() {
    libc::printf("Shutting down!\n\0".as_ptr() as *const i8);
}

pub fn unused() {}
//! main.rs

use foo::unused;

fn main() {
    unused();
    println!("Running");
}

The output produced by this application is:

Starting up!
Running

If I comment out the unused() call in main.rs, the application now produces the following output instead:

Running

I was unable to get the destructor working with the #[dtor] macro, but if you replace the definition of the foo::on_shutdown() function with this instead:

extern "C" fn on_shutdown() {
    unsafe { libc::printf("Shutting down!\n\0".as_ptr() as *const i8) };
}

And then add the following call to libc::atexit() to the foo::on_startup() constructor:

unsafe { libc::atexit(on_shutdown) };

The application now works as expected:

Starting up!
Running
Shutting down!

In short, it seems that a few tweaks to the way your application is written will get the constructor and destructor to run:

  1. Your main.rs must call at least one function or inherent struct method from lib.rs for #[ctor] to register properly. Importing static and const values in the main.rs doesn't seem to help.
  2. #[dtor] doesn't seem to work at all. Register it manually with libc::atexit() in your #[ctor] function.

I'm not sure what is going on, but I am also leaning towards the possibility of an issue with Rust or LLVM.

I thought the whole point of the #[used] attribute (as used here was to ensure a symbol is always present in the final binary?

Just encountered this issue as well.

In a debug build all 233 #[ctor]s are executed, while in a release build only 60 are being executed. Updating to Rust 1.43.1, and then 62 are executed in a release build.

Sadly, @ebkalderon solution did not fix it for me, however I'm on Windows.

It definitely feels like some sort of "whole-module pruning", as @mmastrac said, as in my case either none or all #[ctor]s in a module is executed.

Would someone have some bandwidth to file an upstream bug? This definitely seems like a Rust core issue - #[used] items should not be pruned.

This may be fixed now, however I'll need someone with the issue to attempt to repro.

Marking as fixed as upstream is fixed.