Sometimes you need to run some functions that are provided by a 3rd party, or the code must be configurable at runtime. This is where dynamic libraries come in handy. In Linux those libraries end with the extension .so
while in macOS they have the extension .dylib
. Windows uses the extension .dll
for dynamic libraries.
Dynamic libraries in Rust
According to the Rust documentation there are two types of dynamic libaries:
- Dynamic Rust library (dylib)
- Dynamic system library (cdylib)
For this example, we will implement and use a dynamic Rust library, so we can use all the good Rust stuff.
Project structure
The idea is to have a trait that is defined in a common static library, used in a binary and implemented in a plugin (the dynamic library). For this, we need three projects, that we will group together in a Cargo workspace.
First we create a directory for our workspace and create the Cargo project:
$> mkdir rust-plugin
$> cd rust-plugin
In this project we then create the Cargo.toml
:
[workspace]
resolver = "2"
When we create new cargo
projects in a workspace directory, cargo
will automatically add the new project as a member to the workspace. So we create the common library first, that contains all the traits that we want to use in our binary, and that will get implemented in our plugin:
$> cargo new library --lib
Now we can add the binary project, that will use the traits, and the plugin project, that will implement the traits:
$> cargo new binary
$> cargo new plugin --lib
After all of that, our directory should look like the following:
$> tree .
.
├── binary
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── Cargo.toml
├── library
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
└── plugin
├── Cargo.toml
└── src
└── lib.rs
Defining the common trait
As a simple example, we will create a trait, that takes a &str
parameter and returns a String
:
// in library/src/lib.rs
pub trait Greeter {
fn say_hello(&self, name: &str) -> String;
}
Implementing the trait in the plugin
To implement the Greeter
trait, we need to add the library
as a dependency in our Cargo.toml
. And since this plugin should be a dynamic Rust library, we need to add a [lib]
section:
# plugin/Cargo.toml
[dependencies]
library = { path="../library"}
[lib]
name = "plugin"
crate-type = ["dylib"]
With the crate-type
set to dylib
, we tell Rust to compile it as a dynamic Rust library. This will create a library file in the target directory:
libplugin.so
for Linuxlibplugin.dylib
for macOSplugin.dll
for Windows
The implementaion in the plugin/src/lib.rs
is very straight forward:
use library::Greeter;
pub struct DynamicGreeter;
impl Greeter for DynamicGreeter {
fn say_hello(&self, name: &str) -> String {
format!("A dynamic hello, {name}")
}
}
Implementing the new
function in the plugin
Since we need a way to create a Greeter
instance from our binary project, we need to implement a function, that we call new
. This function will return a Box<dyn Greeter>
and is implemented in plugin/src/lib.rs
.
// plugin/src/lib.rs
impl DynamicGreeter {
#[no_mangle]
fn new() -> Box<dyn Greeter> {
Box::new(Self)
}
}
Notice the attribute #[no_mangle]
, it tells the compiler to not mangle the name, so code that tries to load the function can be sure, the name is as it is. For details, see the section in the Rust documentation.
Using the trait in our binary
To use the trait in our binary, we have to create a dependency to the library
project in our binary/Cargo.toml
:
# binary/Cargo.toml
[dependencies]
library = { path="../library"}
In the binary we want to load the dynamic library from the plugin
project. When we run cargo build
, Cargo will create the file in the directory target/debug/libplugin.so
(when compiling in Linux)
Loading the dynamic library
To be able to load the dynamic library, we use the crate libloading
, so we add the following to the Cargo.toml
in our binary
project, by calling cargo add libloading
:
[dependencies]
libloading = "0.8.5"
First, we need to load the library at runtime:
let lib_path = "target/debug/libplugin.so"; // for macOS and Windows the name must be changed
let lib = unsafe {
match Library::new(lib_path) {
Ok(lib) => lib,
Err(e) => panic!("Could not load library: {e}"),
}
};
Loading a library is an unsafe operation, so it has to be inside an unsafe
block. If you want to learn more about unsafe Rust, you can read the chapter about it the Rust book.
When everything works, the match
expression returns the library handle. If any error occurs, we simply panic in this example. In production code we would of course properly handle such situations!
To instantiate the Greeter
implementation in the dynamic library, we load the function called new
, which returns a Box<dyn Greeter>
:
let constructor: Symbol<fn() -> Box<dyn Greeter>> = unsafe {
match lib.get(b"new") {
Ok(constructor) => constructor,
Err(e) => panic!("Could not load function: {e}"),
}
};
Calling the functions from the plugin
This code will return a function, that we can call as a regular function:
let greeter = constructor();
Now that we have our instance of a Greeter
from the plugin
, we can call it:
let greeting = greeter.say_hello("dear developer");
The complete picture
Now we can put all things together and implement the main
function in our binary:
use libloading::{Library, Symbol};
use library::Greeter;
fn main() {
let lib_path = "target/debug/libplugin.so";
let lib = unsafe {
match Library::new(lib_path) {
Ok(lib) => lib,
Err(e) => panic!("Could not load library: {e}"),
}
};
let constructor: Symbol<fn() -> Box<dyn Greeter>> = unsafe {
match lib.get(b"new") {
Ok(constructor) => constructor,
Err(e) => panic!("Could not load function: {e}"),
}
};
let greeter = constructor();
println!("{}", greeter.say_hello("World!"));
}
Possible problems and solutions
Segmentation fault when calling say_hello
For a cleaner code, you might want to extract the loading of the library part into a separate function like this:
// Do *NOT* do this, because it will cause a segmentation fault when calling say_hello()
fn create_greeter(lib_path: &str) -> Result<Box<dyn Greeter>, Box<dyn Error>> {
let lib = unsafe { Library::new(lib_path)? };
let creator: Symbol<GreeterCreator> = unsafe { lib.get(b"new")? };
let greeter = unsafe { creator() };
Ok(greeter)
}
But when you call the returned Greeter
like this:
fn main() {
let lib_path = "target/debug/libplugin.so";
match create_greeter(&lib_path) {
Ok(greeter) => {
let greeting = greeter.say_hello("segfault!");
println!("{greeting}");
}
Err(e) => eprintln!("{e}"),
};
}
You will get a segmentation fault at runtime. This is because the variable lib
in the function create_greeter
will be dropped after the function ends. This is because of the Rust ownership. And as soon as the lib
gets dropped, the library is also closed and freed. Calling any function from the library will then fail.
To solve this issue, we have to make sure, that the library has the same lifetime like all the functions used.
This can easily done, by bundeling the Greeter
instance and the library handle in one struct:
pub struct GreeterBundle {
greeter: Box<dyn Greeter>,
// Make sure _lib is sorted after greeter, so that it gets dropped after it
_lib: Library,
}
This way, the library will be available as long as the GreeterBundle
is valid. Now we can implement a loading function like this:
// binary/src/main.rs
pub type GreeterCreator = unsafe fn() -> Box<dyn Greeter>;
impl GreeterBundle {
pub fn new(path: &str, name: &str) -> Result<Self, Box<dyn std::error::Error>> {
// name is the "base" name of the library without any platform specific parts
#[cfg(target_os = "linux")]
let os_lib_name = format!("lib{name}.so");
#[cfg(target_os = "macos")]
let os_lib_name = format!("lib{name}.dylib");
#[cfg(target_os = "windows")]
let os_lib_name = format!("{name}.dll");
let lib_path = format!("{path}/{os_lib_name}");
let _lib = unsafe { Library::new(lib_path)? };
let creator: Symbol<GreeterCreator> = unsafe { _lib.get(b"new")? };
let greeter = unsafe { creator() };
Ok(Self { _lib, greeter })
}
}
In the new
method here, we also make it platform agnostic, when it comes to loading the library.
For easier usage, we let the GreeterBundle
also implement the Greeter
trait, so that it forwards the call to the Greeter
member:
// binary/src/main.rs
impl Greeter for GreeterBundle {
fn say_hello(&self, name: &str) -> String {
self.greeter.say_hello(name)
}
}
Now we can safely call the Greeter
method in our main function:
// binary/src/main.rs
pub fn main() {
let lib_path = "target/debug";
let lib_name = "plugin";
match GreeterBundle::new(lib_path, lib_name) {
Ok(bundle) => {
let greeting = bundle.say_hello("bundle");
println!("{greeting}");
}
Err(e) => eprintln!("{e}"),
}
}
Segmentation fault when dropping the GreeterBundle
In the example above, the fields in the struct GreeterBundle
are correctly sorted. But if the field _lib
is the first element, it can happen, that the library is dropped before the field greeter
is dropped. This would cause a segmentation fault.