How to avoid Library and Symbol drops in crate "libloading"

I want to liberally use Library and Symbol from the libloading crate, to load and retrieve symbols from a plugin library that I or other programmers may create.

However, libloading::Library::new() drops and closes the library as soon as it goes out of scope. Similarly, it seems Symbols can't be called when they go out of scope (I tried and there is a core dump).

I could dynamically allocate them, but I don't want to use unsafe code if at all possible.

What is the best way to keep Library and Symbol around?

static, Lazy, Mutex, HashSet<Library> comes to mind.

Box::leak

This is a bad idea because it guarantees that you can't unload the lib anymore, even if you drop the &'static Library that results from it.

Well either you want to drop it or you don't want to drop it ...

1 Like

My point is that it takes away flexibility rather than add it.

One alternative solution is to just wrap each Library instance in an Arc<T>. That way you can use it as normal, pass it around like an owned value, and if/when you're done with a Library, you simply drop all bindings to that Arc<Library>.

Well, you know, my actually day-work (in C#) is a use case where we do not want to unload a Plugin library, for stupuid™ Business reasons.

But in principle, I agree: Avoid Box::leak, if possible.

But also note, AFAIK, unloading a library is hard. On MacOS, it is Impossible if you use thread-local storage.

I don't think there's anything wrong with using Box::leak here if you don't want it to be unloaded. Another similar option is mem::forget, or putting it in a global variable.

2 Likes

You already can't rely on unloading to work across platforms. Musl implements dlclose as no-op, macOS only supports unloading in certain cases and thread locals (which libstd uses) can throw a wrench in unloading too.

1 Like

That's a bit of a nasty surprise. With basic building blocks lacking at the OS (and ABI) level, no wonder writing plugins in Rust is such a PITA.

If you are using bindgen for converting your C header to Rust function definitions, you should check out the --dynamic-loading flag (or the dynamic_library_name() method if calling bindgen from build.rs).

This generates a struct which contains your libloading::Library and pointers for any functions you are binding to. That way you don't run into issues around lifetimes and use-after-frees.

For example, I can use the following header file...

// people.h
struct person {
	const char name[16];
};

void print_name(struct person *p);

... and this is the output I get from bindgen --dynamic-loading people people.h:

/* automatically generated by rust-bindgen 0.63.0 */

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct person {
    pub name: [::std::os::raw::c_char; 16usize],
}
#[test]
fn bindgen_test_layout_person() {
    ...
}

extern crate libloading;

pub struct People {
    __library: ::libloading::Library,
    pub print_name: Result<unsafe extern "C" fn(p: *mut person), ::libloading::Error>,
}

impl People {
    pub unsafe fn new<P>(path: P) -> Result<Self, ::libloading::Error>
    where
        P: AsRef<::std::ffi::OsStr>,
    {
        let library = ::libloading::Library::new(path)?;
        Self::from_library(library)
    }

    pub unsafe fn from_library<L>(library: L) -> Result<Self, ::libloading::Error>
    where
        L: Into<::libloading::Library>,
    {
        let __library = library.into();
        let print_name = __library.get(b"print_name\0").map(|sym| *sym);
        Ok(People {
            __library,
            print_name,
        })
    }

    pub unsafe fn print_name(&self, p: *mut person) -> () {
        (self
            .print_name
            .as_ref()
            .expect("Expected function, got error."))(p)
    }
}

Unloading plugin and correctly handling TLS (and dtor) is almost impossible. It's much simpler to just leak the library at this point.