🦀 Plugin System in TuskLang for Rust
Plugin System in TuskLang for Rust
The plugin system in TuskLang for Rust provides a powerful, type-safe way to extend application functionality through dynamic loading and trait-based interfaces, leveraging Rust's trait system and dynamic linking capabilities.
Basic Plugin Architecture
// Core plugin trait
pub trait Plugin: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn description(&self) -> &str;
fn initialize(&mut self, context: &PluginContext) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
fn shutdown(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}// Plugin context for shared resources
pub struct PluginContext {
pub config: std::sync::Arc<tokio::sync::RwLock<serde_json::Value>>,
pub database: std::sync::Arc<Database>,
pub cache: std::sync::Arc<Cache>,
pub logger: std::sync::Arc<Logger>,
}
// Plugin registry
pub struct PluginRegistry {
plugins: std::collections::HashMap<String, Box<dyn Plugin>>,
context: PluginContext,
}
impl PluginRegistry {
pub fn new(context: PluginContext) -> Self {
Self {
plugins: std::collections::HashMap::new(),
context,
}
}
pub fn register_plugin(&mut self, plugin: Box<dyn Plugin>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let name = plugin.name().to_string();
// Initialize the plugin
plugin.initialize(&self.context)?;
self.plugins.insert(name.clone(), plugin);
@log.info(&format!("Plugin '{}' registered successfully", name));
Ok(())
}
pub fn get_plugin<T: Plugin + 'static>(&self, name: &str) -> Option<&T> {
self.plugins.get(name)?.as_any().downcast_ref::<T>()
}
pub fn list_plugins(&self) -> Vec<PluginInfo> {
self.plugins
.iter()
.map(|(name, plugin)| PluginInfo {
name: name.clone(),
version: plugin.version().to_string(),
description: plugin.description().to_string(),
})
.collect()
}
pub async fn shutdown_all(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
for (name, plugin) in self.plugins.iter_mut() {
if let Err(e) = plugin.shutdown() {
@log.error(&format!("Failed to shutdown plugin '{}': {}", name, e));
}
}
Ok(())
}
}
Dynamic Plugin Loading
// Dynamic plugin loader using libloading
use libloading::{Library, Symbol};pub struct DynamicPluginLoader {
libraries: Vec<Library>,
}
impl DynamicPluginLoader {
pub fn new() -> Self {
Self {
libraries: Vec::new(),
}
}
pub unsafe fn load_plugin(&mut self, path: &str) -> Result<Box<dyn Plugin>, Box<dyn std::error::Error + Send + Sync>> {
let library = Library::new(path)?;
// Get the plugin creation function
let create_plugin: Symbol<fn() -> Box<dyn Plugin>> = library.get(b"create_plugin")?;
let plugin = create_plugin();
// Store the library to keep it loaded
self.libraries.push(library);
Ok(plugin)
}
pub fn load_plugins_from_directory(&mut self, directory: &str) -> Result<Vec<Box<dyn Plugin>>, Box<dyn std::error::Error + Send + Sync>> {
let mut plugins = Vec::new();
for entry in std::fs::read_dir(directory)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("so") {
match unsafe { self.load_plugin(path.to_str().unwrap()) } {
Ok(plugin) => plugins.push(plugin),
Err(e) => {
@log.error(&format!("Failed to load plugin from {:?}: {}", path, e));
}
}
}
}
Ok(plugins)
}
}
// Plugin creation function signature (must be exported by plugins)
#[no_mangle]
pub extern "C" fn create_plugin() -> Box<dyn Plugin> {
Box::new(MyPlugin::new())
}
Specialized Plugin Traits
// Database plugin trait
pub trait DatabasePlugin: Plugin {
async fn execute_query(&self, query: &str, params: &[serde_json::Value])
-> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>>;
async fn create_table(&self, table_name: &str, schema: &str)
-> Result<(), Box<dyn std::error::Error + Send + Sync>>;
async fn backup_database(&self, backup_path: &str)
-> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}// HTTP plugin trait
pub trait HttpPlugin: Plugin {
async fn handle_request(&self, request: &HttpRequest)
-> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>>;
fn register_routes(&self, router: &mut Router);
fn middleware(&self) -> Option<Box<dyn Middleware + Send + Sync>>;
}
// Authentication plugin trait
pub trait AuthPlugin: Plugin {
async fn authenticate(&self, credentials: &AuthCredentials)
-> Result<AuthResult, Box<dyn std::error::Error + Send + Sync>>;
async fn authorize(&self, user: &User, resource: &str, action: &str)
-> Result<bool, Box<dyn std::error::Error + Send + Sync>>;
fn get_user_permissions(&self, user_id: u32) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>>;
}
// Notification plugin trait
pub trait NotificationPlugin: Plugin {
async fn send_notification(&self, notification: &Notification)
-> Result<(), Box<dyn std::error::Error + Send + Sync>>;
fn supported_channels(&self) -> Vec<String>;
async fn test_connection(&self) -> Result<bool, Box<dyn std::error::Error + Send + Sync>>;
}
Plugin Implementation Examples
// Database plugin implementation
pub struct PostgresPlugin {
connection_pool: Option<sqlx::PgPool>,
config: serde_json::Value,
}impl PostgresPlugin {
pub fn new() -> Self {
Self {
connection_pool: None,
config: serde_json::Value::Null,
}
}
}
impl Plugin for PostgresPlugin {
fn name(&self) -> &str { "postgres" }
fn version(&self) -> &str { "1.0.0" }
fn description(&self) -> &str { "PostgreSQL database plugin" }
fn initialize(&mut self, context: &PluginContext) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = context.config.read().await;
let db_url = config["database"]["postgres_url"].as_str()
.ok_or("Missing postgres_url configuration")?;
let pool = sqlx::PgPool::connect(db_url).await?;
self.connection_pool = Some(pool);
self.config = config.clone();
@log.info("PostgreSQL plugin initialized successfully");
Ok(())
}
fn shutdown(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Some(pool) = &self.connection_pool {
pool.close().await;
}
@log.info("PostgreSQL plugin shutdown successfully");
Ok(())
}
}
impl DatabasePlugin for PostgresPlugin {
async fn execute_query(&self, query: &str, params: &[serde_json::Value])
-> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
let pool = self.connection_pool.as_ref()
.ok_or("Database not initialized")?;
let result = sqlx::query(query)
.execute(pool)
.await?;
Ok(serde_json::to_value(result)?)
}
async fn create_table(&self, table_name: &str, schema: &str)
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let pool = self.connection_pool.as_ref()
.ok_or("Database not initialized")?;
sqlx::query(schema)
.execute(pool)
.await?;
@log.info(&format!("Table '{}' created successfully", table_name));
Ok(())
}
async fn backup_database(&self, backup_path: &str)
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = &self.config;
let db_name = config["database"]["name"].as_str()
.ok_or("Missing database name")?;
let output = std::process::Command::new("pg_dump")
.arg("-h").arg(&config["database"]["host"])
.arg("-U").arg(&config["database"]["user"])
.arg("-d").arg(db_name)
.arg("-f").arg(backup_path)
.output()?;
if output.status.success() {
@log.info(&format!("Database backup created at: {}", backup_path));
Ok(())
} else {
Err(format!("Backup failed: {}", String::from_utf8_lossy(&output.stderr)).into())
}
}
}
// HTTP plugin implementation
pub struct ApiPlugin {
routes: Vec<Route>,
middleware: Option<Box<dyn Middleware + Send + Sync>>,
}
impl ApiPlugin {
pub fn new() -> Self {
Self {
routes: Vec::new(),
middleware: None,
}
}
}
impl Plugin for ApiPlugin {
fn name(&self) -> &str { "api" }
fn version(&self) -> &str { "1.0.0" }
fn description(&self) -> &str { "REST API plugin" }
fn initialize(&mut self, _context: &PluginContext) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Register default routes
self.routes.push(Route::new("GET", "/api/health", Self::health_check));
self.routes.push(Route::new("GET", "/api/version", Self::get_version));
@log.info("API plugin initialized successfully");
Ok(())
}
fn shutdown(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
@log.info("API plugin shutdown successfully");
Ok(())
}
}
impl HttpPlugin for ApiPlugin {
async fn handle_request(&self, request: &HttpRequest)
-> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>> {
// Find matching route
for route in &self.routes {
if route.matches(request) {
return route.handler(request).await;
}
}
// Return 404 if no route matches
Ok(HttpResponse::not_found())
}
fn register_routes(&self, router: &mut Router) {
for route in &self.routes {
router.add_route(route.clone());
}
}
fn middleware(&self) -> Option<Box<dyn Middleware + Send + Sync>> {
self.middleware.clone()
}
}
impl ApiPlugin {
async fn health_check(_request: &HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>> {
Ok(HttpResponse::json(serde_json::json!({
"status": "healthy",
"timestamp": @date.now()
})))
}
async fn get_version(_request: &HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>> {
Ok(HttpResponse::json(serde_json::json!({
"version": env!("CARGO_PKG_VERSION"),
"name": env!("CARGO_PKG_NAME")
})))
}
}
Plugin Configuration Management
// Plugin configuration system
pub struct PluginConfig {
plugins: std::collections::HashMap<String, PluginSettings>,
}#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct PluginSettings {
pub enabled: bool,
pub config: serde_json::Value,
pub dependencies: Vec<String>,
pub load_order: Option<u32>,
}
impl PluginConfig {
pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let content = @file.read(path)?;
let config: PluginConfig = serde_json::from_str(&content)?;
Ok(config)
}
pub fn from_tusk_config(config: &str) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let config_map = @json_decode(config);
let mut plugin_config = PluginConfig {
plugins: std::collections::HashMap::new(),
};
for (name, settings) in config_map {
if let Ok(plugin_settings) = serde_json::from_value(settings) {
plugin_config.plugins.insert(name, plugin_settings);
}
}
Ok(plugin_config)
}
pub fn get_plugin_settings(&self, name: &str) -> Option<&PluginSettings> {
self.plugins.get(name)
}
pub fn is_plugin_enabled(&self, name: &str) -> bool {
self.plugins.get(name)
.map(|settings| settings.enabled)
.unwrap_or(false)
}
pub fn get_load_order(&self) -> Vec<String> {
let mut plugins: Vec<_> = self.plugins.iter().collect();
plugins.sort_by_key(|(_, settings)| settings.load_order.unwrap_or(u32::MAX));
plugins.into_iter().map(|(name, _)| name.clone()).collect()
}
}
// Usage with TuskLang configuration
let plugin_config = PluginConfig::from_tusk_config(r#"{
"postgres": {
"enabled": true,
"config": {
"host": "localhost",
"port": 5432,
"database": "mydb",
"user": "postgres"
},
"dependencies": [],
"load_order": 1
},
"api": {
"enabled": true,
"config": {
"port": 8080,
"host": "0.0.0.0"
},
"dependencies": ["postgres"],
"load_order": 2
},
"auth": {
"enabled": true,
"config": {
"jwt_secret": "@env.get('JWT_SECRET')",
"session_timeout": 3600
},
"dependencies": ["postgres"],
"load_order": 3
}
}"#)?;
Plugin Dependency Management
// Plugin dependency resolver
pub struct DependencyResolver {
plugins: std::collections::HashMap<String, PluginSettings>,
}impl DependencyResolver {
pub fn new(plugins: std::collections::HashMap<String, PluginSettings>) -> Self {
Self { plugins }
}
pub fn resolve_load_order(&self) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let mut visited = std::collections::HashSet::new();
let mut temp_visited = std::collections::HashSet::new();
let mut order = Vec::new();
for plugin_name in self.plugins.keys() {
if !visited.contains(plugin_name) {
self.dfs(plugin_name, &mut visited, &mut temp_visited, &mut order)?;
}
}
Ok(order)
}
fn dfs(&self, plugin_name: &str, visited: &mut std::collections::HashSet<String>,
temp_visited: &mut std::collections::HashSet<String>, order: &mut Vec<String>)
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if temp_visited.contains(plugin_name) {
return Err(format!("Circular dependency detected: {}", plugin_name).into());
}
if visited.contains(plugin_name) {
return Ok(());
}
temp_visited.insert(plugin_name.to_string());
if let Some(settings) = self.plugins.get(plugin_name) {
for dependency in &settings.dependencies {
self.dfs(dependency, visited, temp_visited, order)?;
}
}
temp_visited.remove(plugin_name);
visited.insert(plugin_name.to_string());
order.push(plugin_name.to_string());
Ok(())
}
pub fn validate_dependencies(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
for (plugin_name, settings) in &self.plugins {
for dependency in &settings.dependencies {
if !self.plugins.contains_key(dependency) {
return Err(format!("Plugin '{}' depends on '{}' which is not available",
plugin_name, dependency).into());
}
}
}
Ok(())
}
}
Plugin Hot Reloading
// Hot reloading plugin manager
pub struct HotReloadPluginManager {
registry: PluginRegistry,
config: PluginConfig,
plugin_paths: std::collections::HashMap<String, String>,
file_watcher: notify::Watcher,
reload_tx: tokio::sync::mpsc::UnboundedSender<ReloadEvent>,
}impl HotReloadPluginManager {
pub fn new(registry: PluginRegistry, config: PluginConfig) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let (reload_tx, reload_rx) = tokio::sync::mpsc::unbounded_channel();
let mut watcher = notify::recommended_watcher(move |res| {
if let Ok(event) = res {
if let Err(e) = reload_tx.send(ReloadEvent::FileChanged(event)) {
@log.error(&format!("Failed to send reload event: {}", e));
}
}
})?;
Ok(Self {
registry,
config,
plugin_paths: std::collections::HashMap::new(),
file_watcher: watcher,
reload_tx,
})
}
pub async fn start_hot_reload(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Watch plugin directories
for (plugin_name, settings) in &self.config.plugins {
if settings.enabled {
if let Some(plugin_path) = self.get_plugin_path(plugin_name) {
self.file_watcher.watch(&plugin_path, notify::RecursiveMode::NonRecursive)?;
self.plugin_paths.insert(plugin_name.clone(), plugin_path);
}
}
}
// Start reload handler
tokio::spawn(async move {
self.handle_reload_events().await;
});
Ok(())
}
async fn handle_reload_events(&mut self) {
while let Some(event) = self.reload_rx.recv().await {
match event {
ReloadEvent::FileChanged(notify_event) => {
for path in notify_event.paths {
if let Some(plugin_name) = self.get_plugin_name_from_path(&path) {
@log.info(&format!("Reloading plugin: {}", plugin_name));
if let Err(e) = self.reload_plugin(&plugin_name).await {
@log.error(&format!("Failed to reload plugin {}: {}", plugin_name, e));
}
}
}
}
}
}
}
async fn reload_plugin(&mut self, plugin_name: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Shutdown existing plugin
if let Some(plugin) = self.registry.plugins.remove(plugin_name) {
plugin.shutdown()?;
}
// Load new plugin
if let Some(plugin_path) = self.plugin_paths.get(plugin_name) {
let mut loader = DynamicPluginLoader::new();
let plugin = unsafe { loader.load_plugin(plugin_path)? };
self.registry.register_plugin(plugin)?;
}
Ok(())
}
}
enum ReloadEvent {
FileChanged(notify::Event),
}
Plugin Testing Framework
// Plugin testing utilities
pub struct PluginTestFramework {
test_context: PluginContext,
}impl PluginTestFramework {
pub fn new() -> Self {
let config = std::sync::Arc::new(tokio::sync::RwLock::new(serde_json::json!({
"test_mode": true,
"database": {
"url": "sqlite::memory:"
}
})));
let test_context = PluginContext {
config,
database: std::sync::Arc::new(Database::new_test()),
cache: std::sync::Arc::new(Cache::new_test()),
logger: std::sync::Arc::new(Logger::new_test()),
};
Self { test_context }
}
pub async fn test_plugin<P: Plugin>(&self, plugin: &mut P) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Test initialization
plugin.initialize(&self.test_context)?;
// Test plugin functionality
self.test_plugin_functionality(plugin).await?;
// Test shutdown
plugin.shutdown()?;
Ok(())
}
async fn test_plugin_functionality<P: Plugin>(&self, plugin: &P) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Test database plugin
if let Some(db_plugin) = plugin.as_any().downcast_ref::<dyn DatabasePlugin>() {
self.test_database_plugin(db_plugin).await?;
}
// Test HTTP plugin
if let Some(http_plugin) = plugin.as_any().downcast_ref::<dyn HttpPlugin>() {
self.test_http_plugin(http_plugin).await?;
}
// Test auth plugin
if let Some(auth_plugin) = plugin.as_any().downcast_ref::<dyn AuthPlugin>() {
self.test_auth_plugin(auth_plugin).await?;
}
Ok(())
}
async fn test_database_plugin(&self, plugin: &dyn DatabasePlugin) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Test query execution
let result = plugin.execute_query("SELECT 1 as test", &[]).await?;
assert!(result["test"].as_i64() == Some(1));
// Test table creation
plugin.create_table("test_table", "CREATE TABLE test_table (id INTEGER PRIMARY KEY)").await?;
Ok(())
}
async fn test_http_plugin(&self, plugin: &dyn HttpPlugin) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let request = HttpRequest::new("GET", "/test");
let response = plugin.handle_request(&request).await?;
assert!(response.status_code() == 200 || response.status_code() == 404);
Ok(())
}
async fn test_auth_plugin(&self, plugin: &dyn AuthPlugin) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let credentials = AuthCredentials::new("test_user", "test_password");
let result = plugin.authenticate(&credentials).await?;
assert!(result.is_success() || result.is_failure());
Ok(())
}
}
#[cfg(test)]
mod plugin_tests {
use super::*;
#[tokio::test]
async fn test_postgres_plugin() {
let framework = PluginTestFramework::new();
let mut plugin = PostgresPlugin::new();
framework.test_plugin(&mut plugin).await.unwrap();
}
#[tokio::test]
async fn test_api_plugin() {
let framework = PluginTestFramework::new();
let mut plugin = ApiPlugin::new();
framework.test_plugin(&mut plugin).await.unwrap();
}
#[tokio::test]
async fn test_dependency_resolution() {
let config = PluginConfig::from_tusk_config(r#"{
"plugin_a": {
"enabled": true,
"dependencies": ["plugin_b"],
"load_order": 2
},
"plugin_b": {
"enabled": true,
"dependencies": [],
"load_order": 1
}
}"#).unwrap();
let resolver = DependencyResolver::new(config.plugins);
let order = resolver.resolve_load_order().unwrap();
assert_eq!(order, vec!["plugin_b", "plugin_a"]);
}
}
Plugin Security
// Plugin security manager
pub struct PluginSecurityManager {
allowed_apis: std::collections::HashSet<String>,
sandbox_config: SandboxConfig,
}#[derive(Clone)]
pub struct SandboxConfig {
pub allow_network: bool,
pub allow_file_system: bool,
pub allow_database: bool,
pub max_memory_mb: usize,
pub max_execution_time_sec: u64,
}
impl PluginSecurityManager {
pub fn new() -> Self {
let mut allowed_apis = std::collections::HashSet::new();
allowed_apis.insert("log".to_string());
allowed_apis.insert("config".to_string());
Self {
allowed_apis,
sandbox_config: SandboxConfig {
allow_network: false,
allow_file_system: false,
allow_database: false,
max_memory_mb: 100,
max_execution_time_sec: 30,
},
}
}
pub fn validate_plugin(&self, plugin: &dyn Plugin) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Check plugin signature
self.verify_plugin_signature(plugin)?;
// Validate plugin permissions
self.validate_plugin_permissions(plugin)?;
// Check for malicious code patterns
self.scan_for_malicious_code(plugin)?;
Ok(())
}
fn verify_plugin_signature(&self, _plugin: &dyn Plugin) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Implement digital signature verification
// This would check if the plugin was signed by a trusted authority
Ok(())
}
fn validate_plugin_permissions(&self, plugin: &dyn Plugin) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Check if plugin requests permissions it shouldn't have
let plugin_name = plugin.name();
// Example: Only allow database plugins to access database
if plugin_name.contains("database") && !self.sandbox_config.allow_database {
return Err("Plugin requires database access but it's not allowed".into());
}
Ok(())
}
fn scan_for_malicious_code(&self, _plugin: &dyn Plugin) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Implement code scanning for malicious patterns
// This could include checking for:
// - Unsafe code blocks
// - Network access attempts
// - File system access
// - System calls
Ok(())
}
pub fn create_sandboxed_context(&self, context: PluginContext) -> SandboxedPluginContext {
SandboxedPluginContext {
inner: context,
security_manager: self.clone(),
}
}
}
pub struct SandboxedPluginContext {
inner: PluginContext,
security_manager: PluginSecurityManager,
}
impl SandboxedPluginContext {
pub async fn execute_with_timeout<F, T>(&self, timeout: std::time::Duration, operation: F)
-> Result<T, Box<dyn std::error::Error + Send + Sync>>
where
F: std::future::Future<Output = Result<T, Box<dyn std::error::Error + Send + Sync>>> + Send,
T: Send,
{
tokio::time::timeout(timeout, operation).await
.map_err(|_| "Operation timed out".into())?
}
pub fn check_memory_usage(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let current_memory = @memory.get_usage();
let max_memory = self.security_manager.sandbox_config.max_memory_mb 1024 1024;
if current_memory > max_memory {
return Err("Memory usage exceeded limit".into());
}
Ok(())
}
}
This comprehensive guide covers Rust-specific plugin system patterns, ensuring type safety, security, and integration with Rust's trait system while maintaining the power and flexibility of TuskLang's plugin capabilities.