Mutating &'static, memory reclaiming

Migo·2024년 12월 5일

&'static

&'static lifetime means that it lasts for the entire duration of the program - which can be a great choice if you are sure that:

  • the data is read only
  • it will not be changed

For this reason, it makes a little sense to deallocate them. But it doesn't mean that you cannot do it.

Box::leak

First of all, you can dynamically allocate values to heap memory and intentionaly cause a leak, taking the static reference to them.

fn main() {
    let s = String::from("Hello, world!");
    let static_str: &'static str = Box::leak(s.into_boxed_str());

    println!("{}", static_str);
    // Memory for `static_str` cannot be deallocated now.
}

Note, however that the "Hello, world!" you passed to String::from as argument is not the same as string literal that resides in static data area that would be read by compiler as static variable.

Mutating &'static field

Once you leak it and what if you want to change the reference? Well, You can actually do that.

#[test]
fn change_static_reference_field() {
    struct MyStruct {
        field: &'static str,
    }

    let static_str = "Hello, world!".to_string();
    let mut my_struct = MyStruct {
        field: Box::leak(static_str.into_boxed_str()),
    };

    my_struct.field = "Hello, Rust!";
    assert_eq!(my_struct.field, "Hello, Rust!");
}

The thing is, where is static_str? and its value? Has it gone or dropped?

The unfortunate truth is, it's a leak. You keep doing this and it keeps taking over memory space.

Reclaiming leaked memory

But still, you can reclaim that leaked memory with Box::from_raw which is unsafe - feel scared? you should be.

Just as we lost ownership by intentionally leaking, this function works the other way around - it takes a hold of onwership by boxing it.

The following will help you understand how it works.


use std::cell::Cell;
use std::rc::Rc;

#[test]
fn reclaim_leaked_memory() {
    // A shared counter to track drops
    let drop_counter = Rc::new(Cell::new(0));

    struct DroppableString {
        content: String,
        drop_counter: Rc<Cell<usize>>,
    }

    impl Drop for DroppableString {
        fn drop(&mut self) {
            // Increment the drop counter when the instance is dropped
            self.drop_counter.set(self.drop_counter.get() + 1);
        }
    }

    // Create a heap-allocated DroppableString
    let tracked_string = DroppableString {
        content: String::from("This is a tracked string."),
        drop_counter: drop_counter.clone(),
    };

    let static_str = Box::leak(Box::new(tracked_string));

    // Verify the content is available
    assert_eq!(static_str.content, "This is a tracked string.");
    assert_eq!(drop_counter.get(), 0); // Memory has not been reclaimed yet

    // Reclaim the memory manually
    unsafe {
        // Get the pointer to the str
        let ptr: *const DroppableString = static_str;
        // Recreate the Box from the pointer and drop it
        let _reclaimed_box: Box<DroppableString> = Box::from_raw(ptr as *mut DroppableString);
        // Memory is reclaimed when `_reclaimed_box` goes out of scope
    }

    // Ensure the drop counter is incremented
    assert_eq!(drop_counter.get(), 1); // Memory is reclaimed
}

Disclaimer

If you don't know what you are doing, don't follow this practice.
If not handled carefully, it may lead to undefined behavior risk. That may be caused in the following scenarios:

  • You just don't know how to "reclaim" the memory
  • You touched on "genuine" static data such as string literals.

So for example, the following attempt will error out:

#[test]
fn reclaim_leaked_memory_with_leak() {
    // A shared counter to track drops
    let s = "hello world";
    unsafe {
        let s: *const str = s;
        let _ = Box::from_raw(s as *mut str);
    }
}
profile
Dude with existential crisis

0개의 댓글