refactor: Overhaul Rhai scripting with multi-file hot reloading
This commit represents a major refactoring of our Rhai scripting system, transforming it from a factory-based approach to a more robust system-based architecture with improved hot reloading capabilities. Key Changes: - Renamed package from rhai_factory to rhai_system to better reflect its purpose - Renamed system_factory.rs to factory.rs for consistency and clarity - Implemented support for multiple script files in hot reloading - Added cross-script function calls, allowing functions in one script to call functions in another - Improved file watching to monitor all script files for changes - Enhanced error handling for script compilation failures - Simplified the API with a cleaner create_hot_reloadable_system function - Removed unused modules (error.rs, factory.rs, hot_reload_old.rs, module_cache.rs, relative_resolver.rs) - Updated all tests to work with the new architecture The new architecture: - Uses a System struct that holds references to script paths and provides a clean API - Compiles and merges multiple Rhai script files into a single AST - Automatically detects changes to any script file and recompiles them - Maintains thread safety with proper synchronization primitives - Provides better error messages when scripts fail to compile This refactoring aligns with our BasePathModuleResolver approach for module imports, making the resolution process more predictable and consistent. The hot reload example has been updated to demonstrate the new capabilities, showing how to: 1. Load and execute multiple script files 2. Watch for changes to these files 3. Automatically reload scripts when they change 4. Call functions across different script files All tests are passing, and the example demonstrates the improved functionality.
This commit is contained in:
69
rhai_system/src/factory.rs
Normal file
69
rhai_system/src/factory.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use rhai::Engine;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use crate::system::System;
|
||||
use crate::hot_reload::hot_reload_callback;
|
||||
|
||||
/// Creates a hot-reloadable system from script files
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `script_paths` - A slice of paths to Rhai script files
|
||||
/// * `main_script_index` - Optional index of the main script in the paths slice. If None, the first script is used.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A Result containing either the System or an error
|
||||
pub fn create_hot_reloadable_system<P: AsRef<std::path::Path> + Clone>(
|
||||
script_paths: &[P],
|
||||
main_script_index: Option<usize>
|
||||
) -> Result<System, Box<dyn std::error::Error>> {
|
||||
if script_paths.is_empty() {
|
||||
return Err("No script paths provided".into());
|
||||
}
|
||||
|
||||
// Determine which script is the main script
|
||||
let main_index = main_script_index.unwrap_or(0);
|
||||
if main_index >= script_paths.len() {
|
||||
return Err(format!("Invalid main script index: {}, max index: {}", main_index, script_paths.len() - 1).into());
|
||||
}
|
||||
|
||||
// Create a new engine
|
||||
let engine = Engine::new();
|
||||
|
||||
// Compile the main script first
|
||||
let main_script_path = script_paths[main_index].as_ref();
|
||||
let mut combined_ast = engine.compile_file(main_script_path.to_path_buf())?;
|
||||
|
||||
// Compile and merge all other scripts
|
||||
for (i, script_path) in script_paths.iter().enumerate() {
|
||||
if i == main_index {
|
||||
continue; // Skip the main script as it's already compiled
|
||||
}
|
||||
|
||||
// Compile the additional script
|
||||
let path = script_path.as_ref();
|
||||
let ast = engine.compile_file(path.to_path_buf())?;
|
||||
|
||||
// Merge the AST with the main AST
|
||||
// This appends statements and functions from the additional script
|
||||
// Functions with the same name and arity will override previous definitions
|
||||
combined_ast = combined_ast.merge(&ast);
|
||||
}
|
||||
|
||||
// Wrap the combined AST in a thread-safe container
|
||||
let shared_ast = Arc::new(RwLock::new(combined_ast));
|
||||
|
||||
// Convert script paths to PathBuf
|
||||
let script_paths_vec: Vec<std::path::PathBuf> = script_paths.iter()
|
||||
.map(|path| path.as_ref().to_path_buf())
|
||||
.collect();
|
||||
|
||||
// Create the system with the engine, AST, and script paths
|
||||
let system = System::new(engine, shared_ast, script_paths_vec);
|
||||
|
||||
// Watch for script file change using the hot_reload_callback
|
||||
system.watch(hot_reload_callback)?;
|
||||
|
||||
// Return a thread-safe version of the system
|
||||
Ok(system.clone_for_thread())
|
||||
}
|
15
rhai_system/src/hot_reload.rs
Normal file
15
rhai_system/src/hot_reload.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use crate::system::System;
|
||||
|
||||
/// Callback function for hot reloading a script
|
||||
///
|
||||
/// This function is called when a script file is modified.
|
||||
/// It compiles the new script and replaces the old AST with the new one.
|
||||
pub fn hot_reload_callback(sys: &System, file: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Compile the new script
|
||||
let ast = sys.engine.compile_file(file.into())?;
|
||||
|
||||
// Hot reload - just replace the old script!
|
||||
*sys.script.write().unwrap() += ast;
|
||||
|
||||
Ok(())
|
||||
}
|
41
rhai_system/src/lib.rs
Normal file
41
rhai_system/src/lib.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! A thread-safe system for creating and managing Rhai script engines.
|
||||
//!
|
||||
//! This crate provides a system for creating thread-safe Rhai engines with
|
||||
//! pre-compiled scripts. It supports hot reloading of scripts and handles
|
||||
//! multiple script files.
|
||||
|
||||
mod hot_reload;
|
||||
mod system;
|
||||
mod factory;
|
||||
|
||||
pub use system::System;
|
||||
pub use factory::create_hot_reloadable_system;
|
||||
pub use hot_reload::hot_reload_callback;
|
||||
|
||||
/// Re-export commonly used Rhai types for convenience
|
||||
pub use rhai::{Engine, AST, Scope, Module};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Test fixture for common setup
|
||||
struct TestFixture {
|
||||
engine: Engine,
|
||||
}
|
||||
|
||||
impl TestFixture {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
engine: Engine::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_can_evaluate_simple_expressions() {
|
||||
let fixture = TestFixture::new();
|
||||
let result: i64 = fixture.engine.eval("40 + 2").unwrap();
|
||||
assert_eq!(result, 42);
|
||||
}
|
||||
}
|
101
rhai_system/src/system.rs
Normal file
101
rhai_system/src/system.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use rhai::{Engine, Scope, AST};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::path::PathBuf;
|
||||
use notify::{Watcher, RecursiveMode};
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
/// System struct to encapsulate the engine and script AST
|
||||
pub struct System {
|
||||
pub engine: Engine,
|
||||
pub script: Arc<RwLock<AST>>,
|
||||
pub script_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl System {
|
||||
/// Create a new System with the given script
|
||||
pub fn new(engine: Engine, script: Arc<RwLock<AST>>, script_paths: Vec<PathBuf>) -> Self {
|
||||
Self { engine, script, script_paths }
|
||||
}
|
||||
|
||||
/// Execute a function from the script
|
||||
pub fn call_fn<T: Clone + Send + Sync + 'static>(&self, fn_name: &str, args: impl rhai::FuncArgs) -> Result<T, Box<dyn std::error::Error>> {
|
||||
let mut scope = Scope::new();
|
||||
let ast_guard = self.script.read().unwrap();
|
||||
let result = self.engine.call_fn(&mut scope, &ast_guard, fn_name, args)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get a reference to the shared AST
|
||||
pub fn get_script(&self) -> &Arc<RwLock<AST>> {
|
||||
&self.script
|
||||
}
|
||||
|
||||
/// Clone this system for another thread
|
||||
pub fn clone_for_thread(&self) -> Self {
|
||||
Self {
|
||||
engine: Engine::new(),
|
||||
script: Arc::clone(&self.script),
|
||||
script_paths: self.script_paths.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Watch for script file changes and automatically reload the AST
|
||||
pub fn watch<F>(&self, callback: F) -> Result<(), Box<dyn std::error::Error>>
|
||||
where
|
||||
F: Fn(&System, &str) -> Result<(), Box<dyn std::error::Error>> + Send + 'static
|
||||
{
|
||||
if self.script_paths.is_empty() {
|
||||
return Err("No script paths available to watch".into());
|
||||
}
|
||||
|
||||
let system_clone = self.clone_for_thread();
|
||||
|
||||
// Create a channel to receive file system events
|
||||
let (tx, rx) = channel();
|
||||
|
||||
// Create a watcher that will watch the specified paths for changes
|
||||
let mut watcher = notify::recommended_watcher(tx)?;
|
||||
|
||||
// Clone script paths for the thread
|
||||
let script_paths = self.script_paths.clone();
|
||||
|
||||
// Watch all script files for changes
|
||||
for script_path in &script_paths {
|
||||
watcher.watch(script_path, RecursiveMode::NonRecursive)?;
|
||||
println!("🔍 Watching for changes to script file: {:?}", script_path);
|
||||
}
|
||||
|
||||
// Start a thread to handle file system events
|
||||
thread::spawn(move || {
|
||||
// Move watcher into the thread to keep it alive
|
||||
let _watcher = watcher;
|
||||
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(event) => {
|
||||
println!("📝 Detected file system event: {:?}", event);
|
||||
|
||||
// Extract the path from the event
|
||||
// The event is a Result<Event, Error>, so we need to unwrap it first
|
||||
if let Ok(event_data) = event {
|
||||
if let Some(path) = event_data.paths.first() {
|
||||
// Convert path to string
|
||||
if let Some(path_str) = path.to_str() {
|
||||
// Call the callback with the system and script path
|
||||
match callback(&system_clone, path_str) {
|
||||
Ok(_) => println!("✅ Script reloaded successfully"),
|
||||
Err(err) => println!("❌ Error reloading script: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => println!("❌ Error receiving file system event: {:?}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user