A loadable plugin system for Rust that uses .so shared libraries without requiring #[repr(C)] on data structures. This works by leveraging Rust's native ABI, which is stable when using the same rustc version.
The system consists of three main components:
- plugin-interface: Defines the
Plugintrait that all plugins must implement - example-plugin: An example plugin implementation
- host: The host application that loads and uses plugins
- No
#[repr(C)]required: Uses Rust's native ABI for trait objects - Type-safe: Leverages Rust's trait system for plugin interfaces
- Dynamic loading: Uses
libloadingto load.sofiles at runtime - ABI version checking: Automatically verifies that plugins were compiled with the same rustc version as the host
# Build everything
./build.sh
# Or build individually
cargo build -p plugin-interface
cargo build -p example-plugin
cargo build -p hostcargo run -p host- Create a new crate in the workspace:
[package]
name = "my-plugin"
version = "0.1.0"
edition = "2021"
[lib]
name = "my_plugin"
crate-type = ["cdylib"]
[build-dependencies]
rustc_version = "0.4"
[dependencies]
plugin-interface = { path = "../plugin-interface" }- Create a
build.rsfile to embed the rustc version at compile time:
// build.rs
fn main() {
let version = rustc_version::version()
.map(|v| v.to_string())
.unwrap_or_else(|_| "unknown".to_string());
println!("cargo:rustc-env=RUSTC_VERSION={}", version);
println!("cargo:rerun-if-changed-env=RUSTC");
}- Implement the
Plugintrait:
use plugin_interface::{Plugin, PluginMetadata};
pub struct MyPlugin;
impl Plugin for MyPlugin {
fn name(&self) -> &str {
"My Plugin"
}
fn version(&self) -> &str {
"1.0.0"
}
fn execute(&self, input: &str) -> String {
// Your plugin logic here
format!("Processed: {}", input)
}
fn metadata(&self) -> PluginMetadata {
PluginMetadata {
name: self.name().to_string(),
version: self.version().to_string(),
author: "Your Name".to_string(),
description: "Plugin description".to_string(),
}
}
fn rustc_version(&self) -> &str {
// Version is embedded at compile time via build.rs
env!("RUSTC_VERSION")
}
}
#[no_mangle]
pub extern "Rust" fn create_plugin() -> Box<dyn Plugin> {
Box::new(MyPlugin)
}
/// Export the rustc version used to compile this plugin
/// This is checked before loading to ensure ABI compatibility
#[no_mangle]
pub extern "Rust" fn plugin_rustc_version() -> &'static str {
env!("RUSTC_VERSION")
}- Build the plugin:
cargo build -p my-plugin- Load it in the host application by updating the path in
host/src/main.rs
- Same rustc version: Plugins must be compiled with the same rustc version as the host.
- ABI version checking: The rustc version is embedded in the
.so/.dylibmetadata and is checked before loading the plugin. If versions don't match, the plugin will fail to load with a clear error message, preventing ABI incompatibility issues at runtime. - Version export: All plugins must export a
plugin_rustc_version()function (orrupture_rustc_version()for rupture blocks) that returns the rustc version used to compile them. This is automatically generated viabuild.rs. - Library lifetime: The current implementation leaks the library handle. In production, you'd want to manage this more carefully, possibly using
Arcor similar - ABI stability: While this avoids
#[repr(C)], it relies on Rust's native ABI which is not officially stable. However, it works reliably when using the same compiler version - Platform differences: On Linux, plugins are built as
.sofiles. On macOS, they're built as.dylibfiles. The host application handles both automatically. If you specifically need.sofiles on macOS, you can create a symlink or use a custom build script
- Plugins must be compiled with the same rustc version as the host
- The library handle is leaked to keep the plugin loaded (can be improved with better lifetime management)
- No hot-reloading support (would require unloading/reloading libraries)