skip to Main Content

I am beginning with Rust and I design a plugin mechanism based on libloading as learning opportunity, beside the Rust book. I am unsure if the issue I have is due to misunderstanding with Rust or libloading, or if there is an issue with either. In any case, here we go…

I defined a crate composed of one library (lib.rs) and one binary (bin.rs). Both import and use a module defined in a third file animal.rs, via import animal at the top of their source.
animal.rs defines a single train Animal with a single method speak(), that takes no argument and returns nothing:

pub trait Animal
{
    fn speak(&self);
}

The library (lib.rs) provides an implementation for Animal, with a concrete struct Dog, holding a single String, its associated implementation and an implementation of the trait Animal for Dog:

mod animal;

struct Dog {
    name: String,
}

impl Dog {
    fn new(name: &str) -> Dog {
        Dog {
            name: name.to_string(),
        }
    }
}

impl animal::Animal for Dog {
    fn speak(&self) {
        println!("{}: ruff, ruff!", self.name);
    }
}

Finally, I add a function that takes a string, and returns a dynamic type Animal (via Box<dyn animal::Animal>). I make sure the name is not mangled so that libloading can find and load it:

#[no_mangle]
pub fn get_animal(name: &str) -> Box<dyn animal::Animal>
{
    Box::new(Dog::new(name))
}

Now, the binary bin.rs reuse the libloading documentation example. I try to wrap it into a convenient struct Dynlib<T>, that instanciates and holds libloading::Library from a filename and store the name of the function that creates and returns a new instance of an implementation of trait T, i.e., Animal. For usage convenience, I use PhantomData to pin the generic type to the structure definition.

struct Dynlib<T>
{
    lib: libloading::Library,
    symbol: String,
    phantom: std::marker::PhantomData<T>,
}

impl<T> Dynlib<T>
{
    fn new(lib_file: &str, symbol: &str) -> Result<Dynlib<T>, Box<dyn std::error::Error>>
    {
        unsafe {
            Ok(Dynlib {
                lib: libloading::Library::new(lib_file)?,
                symbol: String::from(symbol),
                phantom: std::marker::PhantomData,
            })
        }
    }
}

Actual instances are fetched with Dynlib<T>::run(&self, &str). To make sure that instances returned can be used safely, I defined struct Ref to pin their lifetime to the instance of Dynlib used to get the new instance, since (as I understand), the lifetime of the instance returned cannot exceed the lifetime of Dynlib<T>::lib it comes from. For convenience, I implement trat Deref for Ref:

struct Ref<'a, T: 'a>{
    pub obj: T,
    phantom: std::marker::PhantomData<&'a T>,
}

impl<T> std::ops::Deref for Ref<'_, T>
{
    type Target = T;
    fn deref(&self) -> &Self::Target
    {
        &self.obj
    }
}

impl<'a, T> Ref<'a, T>
{
    fn new(obj: T) -> Ref<'a, T>
    {
        Ref{
            obj,
            phantom: std::marker::PhantomData,
        }
    }
}

impl<T> Dynlib<T>
...
    fn run<'a>(& 'a mut self, name: &str) -> Result<Ref<'a, T>, Box<dyn std::error::Error>>
    {
        unsafe {
            let func: libloading::Symbol::<unsafe extern fn(&str) -> T> = self.lib.get(self.symbol.as_bytes())?;
            let obj = func(name);
            Ok(Ref::<'a, T>::new(obj))
        }
    }
...
}

Now I expect that the Rust compiler will not let me define any variables owning any value returned by Dynlib<T>::run(...) to have a lifetime that exceed that of said `Dynlib instance:

fn main()
{
    let _b;
    {
        let mut _a = Dynlib::<Box<dyn animal::Animal>>::new("/home/debian/plugin/target/debug/libplugin.so", "get_animal").unwrap();
        _b = _a.run("Stig").unwrap();
    }
    _b.speak();
}

Compiling:

error[E0597]: `_a` does not live long enough
  --> src/bin.rs:64:14
   |
64 |         _b = _a.run("Stig").unwrap();
   |              ^^ borrowed value does not live long enough
65 |     }
   |     - `_a` dropped here while still borrowed
66 |         _b.speak();
   |         -- borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0597`.
error: could not compile `plugin`.

As intended. If I fix the lifetime issue:

fn main()
{
    {   
        let mut _a = Dynlib::<Box<dyn animal::Animal>>::new("/home/debian/plugin/target/debug/libplugin.so", "get_animal").unwrap();
        let _b = _a.run("Stig").unwrap();
        _b.speak();
    }
}

The not only it compile, but it runs as expected:

debian@bullseyebuilder:~/plugin$ cargo run
   Compiling cfg-if v1.0.0
   Compiling libloading v0.7.4
   Compiling plugin v0.1.0 (/home/debian/plugin)
    Finished dev [unoptimized + debuginfo] target(s) in 2.22s
     Running `target/debug/pluginrun`
Stig: ruff, ruff!
debian@bullseyebuilder:~/plugin$

Things get more interesting when I try something like this:

fn main()
{
    let _b;
    {   
        let mut _a = Dynlib::<Box<dyn animal::Animal>>::new("/home/debian/plugin/target/debug/libplugin.so", "get_animal").unwrap();
        _b = _a.run("Stig").unwrap();
        _b.speak();
    }
}

_b has a larger lifetime than _a, so I would expect Rust to reject this code. On the other hand, _b is never used outside the lifetime of _a, so Rust might accept it after all, and everything should run smooth. TO sum up, either Rust rejects it, or it accepts it and it runs well. Turns out things happen differently:

debian@bullseyebuilder:~/plugin$ cargo run
   Compiling plugin v0.1.0 (/home/debian/plugin)
    Finished dev [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/pluginrun`
Stig: ruff, ruff!
Segmentation fault
debian@bullseyebuilder:~/plugin$ 

Rust does accept the code, but the code fails to run, with an ugly Segmentation fault that Rust promised they would be near old-history told in fairy tales to scare the kids. Why is that so, and what I can do to have sufficient compile-time check to either refuse it or accept and it runs?

Here are variants of the same issue:

fn main()
{
    let _b;
    {   
        let mut _a = Dynlib::<Box<dyn animal::Animal>>::new("/home/debian/plugin/target/debug/libplugin.so", "get_animal").unwrap();
        _b = _a.run("Stig");
        _b.unwrap().speak();
    }
}

Gives:

debian@bullseyebuilder:~/plugin$ cargo run
   Compiling plugin v0.1.0 (/home/debian/plugin)
    Finished dev [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/pluginrun`
Stig: ruff, ruff!
debian@bullseyebuilder:~/plugin$ 

Now that looks like what I expected, except I wish I could unwrap the result of run() right away and let Ref<T> track the lifetime for me, and I don’t need to unwrap every time I need to use the instance.

More interesting:

fn main()
{
    {   
        let _b;
        let mut _a = Dynlib::<Box<dyn animal::Animal>>::new("/home/debian/plugin/target/debug/libplugin.so", "get_animal").unwrap();
        _b = _a.run("Stig").unwrap();
        _b.speak();
    }
}
debian@bullseyebuilder:~/plugin$ cargo run
   Compiling plugin v0.1.0 (/home/debian/plugin)
    Finished dev [unoptimized + debuginfo] target(s) in 0.57s
     Running `target/debug/pluginrun`
Stig: ruff, ruff!
Segmentation fault
debian@bullseyebuilder:~/plugin

Here again, Rust accepts some code that does not run well, although _b has the same lifetime as _a. This works:

fn main()
{
    {   
        let mut _a = Dynlib::<Box<dyn animal::Animal>>::new("/home/debian/plugin/target/debug/libplugin.so", "get_animal").unwrap();
        let _b;
        _b = _a.run("Stig").unwrap();
        _b.speak();
    }
}
debian@bullseyebuilder:~/plugin$ cargo run
   Compiling plugin v0.1.0 (/home/debian/plugin)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/pluginrun`
Stig: ruff, ruff!
debian@bullseyebuilder:~/plugin$ 

Although this is the same as the second example as above (the first compiling and working as expected).

My question is: what can I do to make Rust to have a reliable compile-time check of the values fetched via libloading? Why is the solution I have for now is only covering a subset of lifetime-related issues? Why moving unwrap() from a line to another changes the outcome?

Bonus points:
Is there any struct in Rust’s standard library that can play the role of Ref<'a, T> shown above, i.e., pinning the instance contained to some lifetime?

"Attempted solutions" field below:
Since most details given on the problem description field, I opted to add here complete source and Cargo files

I tried Rust 1.48 (Debian Bullseye default) and Rust 1.60 (latest on Easybuild) with the same results.

Cargo.toml:

[package]
name = "plugin"
version = "0.1.0"
authors = ["Foo Bar <[email protected]>"]
#edition = "2022"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type=["dylib"]
name = "plugin"
path = "src/lib.rs"

[[bin]]
name="pluginrun"
path="src/bin.rs"

[dependencies]
libloading = "0.7.4"

src/lib.rs
:

mod animal;

struct Dog {
    name: String,
}

impl Dog {
    fn new(name: &str) -> Dog {
        Dog {
            name: name.to_string(),
        }
    }
}

impl animal::Animal for Dog {
    fn speak(&self) {
        println!("{}: ruff, ruff!", self.name);
    }
}

#[no_mangle]
pub fn get_animal(name: &str) -> Box<dyn animal::Animal>
{
    Box::new(Dog::new(name))
}

src/animal.rs:

pub trait Animal
{
    fn speak(&self);
}

src/bin.rs:

mod animal;

struct Ref<'a, T: 'a>{
    pub obj: T,
    phantom: std::marker::PhantomData<&'a T>,
}

impl<T> std::ops::Deref for Ref<'_, T>
{
    type Target = T;
    fn deref(&self) -> &Self::Target
    {
        &self.obj
    }
}

impl<'a, T> Ref<'a, T>
{
    fn new(obj: T) -> Ref<'a, T>
    {
        Ref{
            obj,
            phantom: std::marker::PhantomData,
        }
    }
}            

struct Dynlib<T>
{
    lib: libloading::Library,
    symbol: String,
    phantom: std::marker::PhantomData<T>,
}


impl<T> Dynlib<T>
{
    fn new(lib_file: &str, symbol: &str) -> Result<Dynlib<T>, Box<dyn std::error::Error>>
    {
        unsafe {
            Ok(Dynlib {
                lib: libloading::Library::new(lib_file)?,
                symbol: String::from(symbol),
                phantom: std::marker::PhantomData,
            })
        }
    }

    fn run<'a>(& 'a mut self, name: &str) -> Result<Ref<'a, T>, Box<dyn std::error::Error>>
    {
        unsafe {
            let func: libloading::Symbol::<unsafe extern fn(&str) -> T> = self.lib.get(self.symbol.as_bytes())?;
            let obj = func(name);
            Ok(Ref::<'a, T>::new(obj))
        }
    }
}

fn main()
{
    {
        let mut _a = Dynlib::<Box<dyn animal::Animal>>::new("/home/debian/plugin/target/debug/libplugin.so", "get_animal").unwrap();
        let _b;
        _b = _a.run("Stig").unwrap();
        _b.speak();
    }
}

2

Answers


  1. One thing that jumps out to me is you’re using different ABIs between definition and loading.

    pub fn get_animal(name: &str) -> Box<dyn animal::Animal>
    

    The definition of get_animal has no extern qualifier, meaning it is using Rust’s own ABI.

    let func: Symbol::<unsafe extern fn(&str) -> T> = ...;
    

    Using extern on the function pointer means it will use C’s ABI.

    Try adding extern to the definition or remove it from the loaded type (though if you make the function itself extern, the compiler may complain about your types not being FFI-safe).


    I’m skeptical whether this will actually solve the problem though since the data and vtable pointers in Box work well enough to call .speak(), but it is wrong and should be fixed nevertheless.

    Login or Signup to reply.
  2. The problem is that you drop _b after the library is freed. However, because you told Rust you only have a reference to T (PhantomData<&'a T>), it thinks you don’t drop it and therefore it can shrink 'a as needed.

    A way to solve that is to force Rust to consider your type as having a drop glue, by providing a dummy Drop impl:

    impl<T> Drop for Ref<'_, T> {
        fn drop(&mut self) {}
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search