rust_libloading
rust_libloading copied to clipboard
TLS with RefCell with heap-allocated object produces unexpected DSL re-loading behavior
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