rust_libloading icon indicating copy to clipboard operation
rust_libloading copied to clipboard

TLS with RefCell with heap-allocated object produces unexpected DSL re-loading behavior

Open collinoc opened this issue 2 months ago • 0 comments

I'm running into an issue with static thread local storage that contains a heap-allocated object when loading a library.

The main program loop is as follows:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut buffer = String::new();

    let mut libs = Vec::new();
    let mut plugins = Vec::new();

    loop {
        plugins.clear();
        libs.clear();

        println!("Press enter to reload...");
        std::io::stdin().read_line(&mut buffer)?;

        unsafe {
            // This typically would loop over a directory, attempting to load all *.so files
            // that match a certain signature, but this is just an example
            let lib = libloading::Library::new("../target/release/libplugins.so")?;
            let plugin: libloading::Symbol<fn(&mut Vec<fn()>)> = lib.get(b"init_func")?;

            plugin(&mut plugins);
            libs.push(lib);
        }

        for plugin in &plugins {
            plugin();
        }
    }
}

It loads libraries that can register themselves into a Vec of plugins (or hooks) via an init_func that takes a single argument of &mut Vec<fn()>, and then loops over the loaded plugins. The lib lifetime should be valid for the lifetime of the registered plugin.

Within the plugin (crate-type = "dylib"):

use std::cell::RefCell;

#[no_mangle]
pub fn init_func(plugins: &mut Vec<fn()>) {
    plugins.push(do_something);
    println!("Loaded plugin A");
}

pub fn do_something() {
    thread_local! {
        static CELL: RefCell<u8> = RefCell::new(1);
    }

    CELL.with_borrow(|cell| {
        println!("Cell: {cell}");
    });
}

It registers a function that prints Loaded plugin A and then value of the static CELL.

If you change the "Loaded plugin A" to "Loaded plugin B", recompile, and press enter in the main program to reload, it prints Loaded plugin B, just as expected.

However if you change the static CELL's type from RefCell<u8> to RefCell<Box<u8>> so the plugin reads as such:

use std::cell::RefCell;

#[no_mangle]
pub fn init_func(plugins: &mut Vec<fn()>) {
    plugins.push(do_something);
    println!("Loaded plugin A");
}

pub fn do_something() {
    thread_local! {
        static CELL: RefCell<Box<u8>> = RefCell::new(Box::new(1));
    }

    CELL.with_borrow(|cell| {
        println!("Cell: {cell}");
    });
}

Then upon changing the program to print "Loaded plugin B", recompiling, and reloading by pressing enter, it will still print Loaded plugin A.

This seems to only happen for heap-allocated objects within the static RefCell.

Placing a &'static str literal inside of the RefCell behaves as expected, same with a u64 or even (bool, u64). But a Box or a String inside of the RefCell will always print Loaded plugin A as if it were not reloading the shared library.

This also only seems to appear with TLS from the thread_local macro. When replacing the RefCell<HeapAllocatedObject> with a std::sync::OnceCell<HeapAllocatedObject>, the libraries / plugins are loaded as expected (printing Loaded plugin A then Loaded plugin B).

Here is my repo with the full example

This currently is occurring on Rust 1.77.2 and libloading 0.8.3 in WSL2

collinoc avatar Apr 26 '24 01:04 collinoc