Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stuck with lifetimes: How to write a wrapper struct around a shared library?

Tags:

rust

lifetime

I'm trying to write a wrapper struct around a shared libary in Rust:

pub struct LibraryWrapper<'lib> {
    lib: libloading::Library,
    func: libloading::Symbol<'lib, unsafe extern fn() -> u32>
}

impl<'lib> LibraryWrapper<'lib> { // lifetime `'lib` defined here
    pub fn new() -> libloading::Result<Self> {
        let lib: libloading::Library = libloading::Library::new(LIB_PATH)?;
        let func: libloading::Symbol<'lib, unsafe extern fn() -> u32> = unsafe { // type annotation requires that `lib` is borrowed for `'lib`
            lib.get(FUNC_NAME)? // borrowed value does not live long enough
        };

        Ok(Self {
            lib, func // `lib` dropped here while still borrowed
        })
    }
}

How do I tell the Rust compiler that func will not outlive lib because they're in the same struct? Thanks!

like image 282
malte-v Avatar asked Sep 16 '25 20:09

malte-v


1 Answers

As mentioned, the borrow checked needs to ensure you don't destructure something to keep the func and drop the lib. I've wrestled with the several times and came to the following conclusion:

Dynamically linked libloading stuff is one of the few things where I think it's acceptable to turn into a static. If it's a hardcoded so/dll/dylib I generally make a special static with Once for it so it takes legitimate effort to drop it. For unknown-at-compile-time loaded ones (e.g. managed via a config file) I generally Mutex-protect a static central registry that's just a HashMap with a key for the library name or path, and only allow the libraries to drop when the whole program terminates. Yes, this is basically a singleton, no I don't generally recommend doing this for almost anything else, this case is a special exception.

For added safety, I do recommend you isolate the static and only use functions or structs to refer to it, especially with the Hash Map registry because it's easy for someone to do a remove on it or overwrite the HashMap, or replace a lib with the same name carelessly. Since it's static, it still allows you to store the functions as another static/singleton as well without having to query the library for them every time if you really want (but I'm not sure there's even much performance overhead just requesting it every time, haven't benchmarked it).

I do this for a few reasons:

  1. You typically want to use the functions as if they're a normal library, no different from linking FFI or statically pulling in another library. You have to know the function names anyway, so it's in effect the same as just having a file with those functions normally.
  2. You don't have to pollute everything you want to reference the functionality in with a 'lib lifetime or library function argument or additional member when other than the fact the library you're linking is technically dynamic it's just referencing a dependency. Your program ends up agnostic if it becomes, e.g., a statically linked library later (just delete the setup lines in the file wrapping the static dynamic library and change them to functions that point elsewhere).
  3. You generally only need to/want to/should link a given dynamic library once in the whole program, which is pretty much what static is for if nothing else.
  4. It's rare you want to get rid of a library, dynamic or not, until the program terminates.
  5. It isolates the unsafe initialization and function loading code to one file (though a wrapper struct admittedly takes care of this too).

The most common exception to these reasons I'd see someone having is option 4, like if you have a highly configurable plugin system where the extra memory taken up by the plugins may legitimately matter if the user wants to disable old ones and activate new ones, and in that case devising a wrapper scheme may be the right call, or alternatively adding an unsafe fn close_dynamic_lib_no_seriously_do_you_really_want_to_do_this(self) in your library wrapper.

The only other exception I can think of is if you're writing something like a library for crates.io where you really want the user to be able to manage their dynamic libraries and supply them to your library for you (e.g. it may be in a non-standard path or they can choose between several implementations), and even then it's still probably beneficial to modify this way of doing it.

like image 115
Linear Avatar answered Sep 19 '25 20:09

Linear