In many pieces of software, it can be beneficial to allow other developers to add functionality that you have not considered. In order to do this, plugin-like systems are often used.
Rust is a powerful language with a strong emphasis on safety, however this can make working with plugins quite challenging as you can quickly lose a lot of the compile-time checking that Rust offers. The goal of this library is to reintroduce the safety that Rust is built upon when writing plugin libraries (DLLs, DyLibs or SOs) that are loaded at runtime.
┌───────────┐
Safe │ │ Compile-time
Interface│ Reusable │ Checking
┌──────►│ Plugin │◄─────┬────────┐
│ │ Interface │ │ │
│ │ │ │ │
┌────┴───┐ └───────────┘ ┌────┴─────┐ │
│ │ │ │ │
│ Plugin │◄───────────────►│ Plugin A │ │
│ Host │ Runtime calls │ │ │
│ │◄───────┐ └──────────┘ │
└────────┘ │ │
│ ┌──────────┐ │
│ │ │ │
└───────►│ Plugin B ├──┘
│ │
└──────────┘
The plugin host is the part of the system that will find and load the plugins at runtime and will call upon their functionality. This is usually your main software package.
The plugin client(s) are the plugins that are written by yourself/other developers. These must be written to match the plugin interface provided by the host and allow a safe way to call upon other code.
To write a plugin system, you will first need to decide upon your interface. For this example, we'll demonstrate with this interface:
┌───────────────────────────────┐
│ExamplePlugin │
├───────────────────────────────┤
│do_a_thing() │
│say_hello(name: string) -> bool│
└───────────────────────────────┘
The do_a_thing
function here will just be triggered to do whatever the plugin author decides. The say_hello
function should display a message to the named person, then return a boolean as to whether that was successful. Admittedly, this isn't exactly a complex interface!
In your project, add the dynamic-plugin
library:
cargo add dynamic-plugin --features host
Now in your main.rs
file, you can define your interface:
use dynamic_plugin::{libc::c_char, plugin_interface};
plugin_interface! {
extern struct ExamplePlugin {
/// Ask the plugin to do a thing
fn do_a_thing();
/// Say hello to a person
fn say_hello(to: *const c_char) -> bool;
}
}
Note that we can't just send strings around! As this depends upon FFI, we need to use C-compatible data. Rust will warn you if you do not do this!
That is almost it! We can now write some code to actually use these plugins:
fn main() -> dynamic_plugin::Result<()> {
let plugins = ExamplePlugin::find_plugins("./plugins")?;
for plugin in plugins {
plugin.do_a_thing()?;
let s = std::ffi::CString::new("Jens").unwrap();
plugin.say_hello(s.as_ptr())?;
}
Ok(())
}
You can now write plugins for your interface! Create a new library project:
cargo new --lib example-plugin
In Cargo.toml
, specify that this should build as a C-compatible library:
[lib]
crate-type = [ "cdylib" ]
[dependencies]
dynamic-plugin = { version = "x.x.x", features = [ "client" ] }
You can now define your plugin implementation:
use std::ffi::CStr;
use dynamic_plugin::{libc::c_char, plugin_interface, plugin_impl};
plugin_interface! {
extern struct ExamplePlugin {
/// Ask the plugin to do a thing
fn do_a_thing();
/// Say hello to a person
fn say_hello(to: *const c_char) -> bool;
}
}
plugin_impl! {
ExamplePlugin,
fn do_a_thing() {
println!("A thing has been done!");
}
fn say_hello(name: *const c_char) -> bool {
unsafe {
let name = CStr::from_ptr(name);
println!("Hello, {}!", name.to_string_lossy());
}
true
}
}
The plugin is now ready to build and distribute.
You can also avoid reusing the plugin definition by putting it in it's own library. An implementation that does this is available in the example-plugin
and example-plugin-host
folders of the source repository.
If you come across this compile-time error, this indicates that the implementation you are writing does not match the expected implementation for the plugin definition. Please check that you:
- Are using the correct definition.
- Have all the functions you need to meet the definition.
- That all the functions are named correctly (identically to the definition).
- That all the function arguments are the same order and types as the definition.
- That all the function return types are the same as the definition.