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
One thing that jumps out to me is you’re using different ABIs between definition and loading.
The definition of
get_animal
has noextern
qualifier, meaning it is using Rust’s own ABI.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 itselfextern
, 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.The problem is that you drop
_b
after the library is freed. However, because you told Rust you only have a reference toT
(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: