When a DLL built with Rust is unloaded, it seems that Rust does not automatically call drop on static/global variables defined in the DLL

When a DLL built with Rust is unloaded (e.g., ), it seems that Rust does not automatically call drop on static/global variables defined in the DLL. Is this intentional by design?

In C++, global or static variables in a DLL will have their destructors called when the DLL is unloaded. I noticed this difference and would like to understand why Rust behaves differently.

use std::ffi::c_void;
use windows::core::{s, BOOL};
use windows::Win32::Foundation::{HINSTANCE, TRUE};
use windows::Win32::System::SystemServices::{DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH};

struct Test
{
    pub num : i32
}
impl Drop for Test {
    fn drop(&mut self) {
        unsafe { println!("{}","drop success"); }
    }
}

static test: Test = Test{num:0};

#[unsafe(no_mangle)]
pub extern "system" fn DllMain(hModule : HINSTANCE,  fdwReason : u32, _ :*const c_void) -> BOOL
{
    match fdwReason
    {
        DLL_PROCESS_ATTACH => { println!("load success"); }
        DLL_PROCESS_DETACH => { println!("detach success"); }
        _ => {}
    }
    TRUE
}
// test exe
fn main() {
    unsafe {
       let lib = LoadLibraryA(s!("test.dll")).unwrap();
       FreeLibrary(lib);
    }
}
// output
load success
detach success

Yes, this is intentional, or at least, revisiting it would require design work.

  • From a static item, you can obtain a &'static reference.
  • 'static is the lifetime which never observably ends (it is valid until the entire process and its address space is destroyed).
  • Therefore, a &'static reference may be kept around “forever” without any lifetime parameterization or other tracking of its existence.
  • Therefore, dropping static items would be unsound.

Being able to drop static items during unloading would mean that all uses of the whole crate have a lifetime parameter (since all of the code in the crate could take a reference to one of its static items, and the reference would then be valid for that parameter lifetime). Supporting this practically would require ML-style module-level generics or some other new construct, and lots of code using T: 'static bounds would need to be changed to T: 'the_involved_crates.

14 Likes

I suggest that there should at least be a way to make cdylib automatically drop static variables upon unloading. While I can manually manage the lifetime of my own code, I have no control over third-party libraries. In such cases, this could lead to resource or memory leaks.

Rust also doesn’t support static constructors, which means it never bothered to define a drop order to use for the globals in your library. I think you’ll have better luck registering some explicit on-unload listener than waiting for the design and implementation of Drop for globals.

8 Likes

Isn't 'static already a problem with unloading dylibs without dropping? the memory will get deallocated when the library is unloaded, right?

Unloading a dylib is an unsafe operation, through libc::dlclose or similar. Its statics will be lost sort of like a (safe) mem::forget, and it's your responsibility to make sure nothing else is referencing the library that would become dangling.

Resource and memory leaks are often bugs; but not critical. On the other hand, there are quite a few potential and actual bugs relating to finalization of shared libraries that are memory safety bugs. The finalization order on different platforms is to various degrees ill-specified, conflicting between different standardizations, or hard to model.

E.g. just consider this combination alone: Challenges of shared library environments, Part 2 – Nat!'s Journal & the gcc decision leading to this. A bug fix that made the de-facto implementation sound but breaks the standardization requirements, making the combination illogical. (Albeit seemingly compounded by C just not having good standard containers that would make an insertion-order SortedMap easy to use).

Then also the exact mechanism for loading itself is specified by the target platform, realistically their libc, and until one reviews them all and finds a coherent interface matching all of their lifetime behavior, the Rust interface would require some form of unsafe anyway. Can Rust make itself beholden to libdy anyways? Not to forget the inherent conflict in adhering to the standard-required behavior vs. actual behavior (as above). No one is helped stabilizing a feature broken in practice in the standard library. The safety requirements are also impractically hard to document when they vary by platform and not every dylib behavior is as explicit as Itanium. The mechanism that would come to mind is a form of extension traits that enables atexit / finalization on some platforms, with a completely open design question of interactions with static symbols or at least static-like usage.

On that note as prior art, in Safe C++ (the Sean Baxter experiment) they do have statics with destructors. They resolve this by not giving you a &'static to the symbols despite the symbols being static. (Which seemingly makes it hard to write static values which use other static symbols in their initialization or drop; but alas). This also has the more sneaky effect of resolving the leak-problem for those symbols since your code never gets access to any owning value either. At least this latter part could be hard to emulate outside a language-side solution

And I think that in general it can't define one, because statics can hold &'statics to other statics, so there might not be a topological order between them.

2 Likes

Default behaviour is fine. It is intentional.

If you need to run destructors when unloading dlls, you can do that by using dtor crate.

Unrelated, but good to know.: On MacOS, unloading a DLL sometimes does nothing. [1]

Hence, even if rustc would generate a lot of smart code, it will not help to free memory.

==> This is a hard problem


  1. ↩︎

Musl libc on Linux makes dlclose a no-op, unlike glibc. So that is even more limiting.

1 Like

This is not ideal. The troublesome part is that many third-party crates may internally use global variables, which makes it impossible to release resources properly.

Also note that macOS will also ignore unload requests if the object ever creates its own thread-local storage. AFAIK, any non-no_std build of rustc-compiled code will end up having thread-local storage in it somewhere effectively making macOS act just like musl in this respect.

1 Like