Reusable cargo-generate template for bootstrapping a plugin-oriented Rust workspace. It comes with a production-ready plugin manager crate, example plugins, and metadata-driven configuration so you can focus on behavior instead of wiring.
- Dynamic loading of plugins from shared libraries (
.so,.dll,.dylib) - Registration/deregistration APIs plus grouped plugin support
- Metadata-driven activation via the end-user’s
Cargo.toml - Sample plugin crates that illustrate best practices
- Automated name substitution handled by Rhai + shell hooks
- Latest stable Rust toolchain
cargo generate(install withcargo install cargo-generate)- A Git URL or local path to this repository
-
Install the tool once:
cargo install cargo-generate
-
Generate a new project (replace the repo URL and name as needed):
cargo generate \ --git https://github.com/Smertan/plugin-manager \ --branch main \ --name game_plugins
- Use
--path .instead of--git …when running from a local checkout. cargo-generatewill prompt forgithub-username; the value is used in the generated README badges.
- Use
-
Change into the newly created workspace and verify everything compiles:
cd game_plugins cargo test
| Placeholder | Source | Purpose |
|---|---|---|
project-name |
Inferred from --name (e.g. game_plugins) |
Becomes the crate/workspace identifier |
github-username |
Prompted at generation time | Used in README shields and docs |
During generation a Rhai hook (plugin_manager.rhai) calls plugin_manager_pre.sh, which normalizes names across Cargo.toml files (the manager crate and the sample plugins in tests/). You do not need to run these scripts manually.
.
├── {{ crate_name }}_plugin_manager # Library crate containing PluginManager + traits
├── tests/
│ ├── plugin_inventory # Example plugin crate
│ ├── plugin_mods # Example plugin crate
│ └── plugin_tasks # Example plugin crate
└── Cargo.toml # Workspace manifest already wired up- The library crate exports
PluginManager,Plugin, and helpers undersrc/. - Example plugin crates illustrate how to compile
cdylibartifacts and how metadata in an end-user project should map plugin names to shared objects. - You can remove the sample crates or adapt them as fixtures for integration tests.
Inside {{ crate_name }}_plugin_manager you will find a ready-to-publish crate. Key points:
src/lib.rsdocuments all APIs and describes how plugins should expose acreate_pluginsfunction returningVec<Box<dyn Plugin>>.src/plugin_types.rsdefines thePlugintrait and supporting types.src/plugin_structs.rsholds the runtime data structures used byPluginManager.
Run the workspace tests at the root to validate changes:
cargo testWhen authoring new plugins, implement the Plugin trait and export a factory:
use plugin_manager::plugin_types::Plugin;
use std::any::Any;
#[derive(Debug)]
struct MyPlugin;
impl Plugin for MyPlugin {
fn name(&self) -> String {
"my_plugin".to_string()
}
fn execute(&self, _context: &dyn Any) -> Result<(), Box<dyn std::error::Error>> {
println!("Executing MyPlugin");
Ok(())
}
}
#[unsafe(no_mangle)]
pub fn create_plugins() -> Vec<Box<dyn Plugin>> {
vec![Box::new(MyPlugin)]
}[package]
name = "my_plugin"
version = "0.1.0"
edition = "2021"
[dependencies]
plugin_manager = { path = "../{{ crate_name }}_plugin_manager" }
[lib]
name = "my_plugin"
crate-type = ["lib", "cdylib"]Compile plugins with cargo build --release so the resulting .so/.dll/.dylib can be loaded at runtime.
The base Plugin trait (see {{ crate_name }}_plugin_manager/src/plugin_types.rs) already enforces Send + Sync + Any and defines name, execute, and an overridable group. You can layer additional capabilities on top by creating supertraits that extend Plugin. This keeps shared functionality centralized while letting specialized plugins add new required methods.
Example: define an analytics-oriented plugin type with an extra flush_metrics method and a default group:
pub trait AnalyticsPlugin: Plugin {
fn flush_metrics(&self);
fn group(&self) -> String {
"AnalyticsPlugin".to_string()
}
}Concrete plugins implement the supertrait instead of the base trait directly:
pub struct MetricsPlugin;
impl Plugin for MetricsPlugin {
fn name(&self) -> String { "metrics".into() }
fn execute(&self, _: &dyn Any) -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
}
impl AnalyticsPlugin for MetricsPlugin {
fn flush_metrics(&self) {
println!("flushing stats");
}
}If you want the runtime to treat analytics plugins differently (for example, to expose flush_metrics through the Plugins enum), add a new enum variant and update PluginManager’s registration/execution paths to match:
pub enum Plugins {
Base(Box<dyn Plugin>),
Inventory(Box<dyn PluginInventory>),
Analytics(Box<dyn AnalyticsPlugin>),
}Because each supertrait still inherits from Plugin, the manager can fall back to the common execute flow while also opting into specialized behaviors when the variant matches.
End-user applications load plugins through package.metadata.plugins:
[package.metadata.plugins]
task_scheduler = "/absolute/path/to/libtask_scheduler.so"
[package.metadata.plugins.analytics]
metrics = "/path/to/libmetrics.so"
logger = "/path/to/liblogger.so"At runtime:
use plugin_manager::PluginManager;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut manager = PluginManager::new();
manager = manager.activate_plugins()?;
manager.execute_plugin("task_scheduler", &())?;
Ok(())
}- Update the generated README badges with your GitHub org/repo (the placeholder is already filled if you supplied
github-username). - Replace or expand the sample plugin crates with real integrations.
- Publish the
{{ crate_name }}_plugin_managercrate to crates.io or use it via a path/git dependency inside your applications.