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:
Timur Gordon
2025-05-02 21:04:33 +02:00
parent 939b6b4e57
commit 372b7a2772
54 changed files with 5692 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
# Hot Reload Example
This example demonstrates hot reloading of multiple Rhai script files using the `rhai_system` crate. It shows 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
## How It Works
The example uses two main components:
1. **The System**: Created by `create_hot_reloadable_system` which:
- Loads multiple script files (`script.rhai` and `utils.rhai`)
- Compiles them into a single AST
- Sets up file watchers for each script file
- Provides a clean API for calling functions
2. **Two Threads**:
- **Execution Thread**: Continuously executes functions from the scripts
- **Modification Thread**: Modifies the script files at specific intervals to demonstrate hot reloading
## Cross-Script Function Calls
The example demonstrates how functions in one script can call functions in another:
- `script.rhai` contains the main functions (`greet`, `advanced_calculation`, `multiply`, `divide`)
- `utils.rhai` contains utility functions (`calculate`, `greet_from_utils`)
- Functions in `script.rhai` call functions in `utils.rhai`
This shows how you can organize your code into multiple script files while still maintaining the ability to call functions across files.
## Running the Example
To run this example, navigate to the root of the rhai_system project and execute:
```bash
cargo run --example hot_reload
```
## What to Expect
When you run the example, you'll see:
1. The system loads both `script.rhai` and `utils.rhai`
2. The execution thread calls functions from the scripts every second
3. After 5 seconds, the modification thread updates `script.rhai` to add new functions
4. The execution thread automatically starts using the updated script
5. After another 5 seconds, both script files are modified again
6. The system reloads both scripts and the execution thread uses the latest versions
This demonstrates how the system automatically detects and reloads scripts when they change, without requiring any restart of the application.
## Key Implementation Details
### Multiple Script Support
The system supports multiple script files through:
```rust
// Create a hot reloadable system with multiple script files
let script_paths = vec![
PathBuf::from("examples/hot_reload/script.rhai"),
PathBuf::from("examples/hot_reload/utils.rhai"),
];
let system = create_hot_reloadable_system(&script_paths, None).unwrap();
```
### File Watching
The system automatically sets up file watchers for all script files:
```rust
// Start watching for changes to the script files
system.watch();
```
### Thread-Safe Usage
The system is thread-safe and can be used from multiple threads:
```rust
// Clone the system for use in another thread
let system_clone = Arc::clone(&system);
// Create a thread-local clone for the execution thread
let thread_system = system_clone.clone_for_thread();
```
## Modifying Scripts at Runtime
The example includes functions to modify the script files programmatically:
```rust
// Modify the script file with new content
modify_script(
&script_path,
"examples/hot_reload/modified_script.rhai",
5,
"Modifying the script to add multiply function..."
);
```
This simulates a developer editing the script files during development, demonstrating how the system automatically detects and reloads the scripts.

View File

@@ -0,0 +1,21 @@
// This is a simple Rhai script that will be hot reloaded
// It contains functions that will be called by the main program
// It also uses functions from the utils.rhai script
// A simple greeting function
fn greet(name) {
// Use the format_greeting function from utils.rhai
let utils_greeting = format_greeting(name);
"Hello, " + name + "! This is the original script. " + utils_greeting
}
// A function to calculate the sum of two numbers
fn add(a, b) {
a + b
}
// A function that uses the calculate function from utils.rhai
fn advanced_calculation(x, y) {
// Use the calculate function from utils.rhai
calculate(x, y) * 2
}

View File

@@ -0,0 +1,13 @@
// Utility functions for the hot reload example
// A function to format a greeting message
fn format_greeting(name) {
"Greetings, " + name + "! (from utils.rhai)"
}
// A function to perform a calculation
// Keep it simple to avoid type issues
fn calculate(a, b) {
// Simple integer arithmetic
(a * b) + 10
}

View File

@@ -0,0 +1,152 @@
use std::thread;
use std::time::Duration;
use std::path::PathBuf;
use std::fs::{self, File};
use std::io::Write;
// Import the create_hot_reloadable_system from the library
use rhai_system::create_hot_reloadable_system;
/// Function to modify a script file with content from another file
fn modify_script(target_path: &PathBuf, source_path: &str, delay_secs: u64, message: &str) {
println!("\n🔄 {}", message);
// Read the source script content
let source_script_path = PathBuf::from(source_path);
let source_content = fs::read_to_string(&source_script_path)
.expect(&format!("Failed to read source script file: {}", source_path));
// Write the content to the target file
let mut file = File::create(target_path)
.expect("Failed to open target script file for writing");
file.write_all(source_content.as_bytes())
.expect("Failed to write to target script file");
println!("✅ Script modified successfully!");
// Wait before the next modification if delay is specified
if delay_secs > 0 {
thread::sleep(Duration::from_secs(delay_secs));
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set up the script paths
let main_script_path = PathBuf::from("examples/hot_reload/script.rhai");
let utils_script_path = PathBuf::from("examples/hot_reload/utils.rhai");
println!("Main script path: {:?}", main_script_path);
println!("Utils script path: {:?}", utils_script_path);
// Initialize script.rhai with the content from initial_script.rhai
let initial_script_path = PathBuf::from("examples/hot_reload/initial_script.rhai");
let initial_content = fs::read_to_string(&initial_script_path)
.expect("Failed to read initial script file");
let mut file = File::create(&main_script_path)
.expect("Failed to open script file for writing");
file.write_all(initial_content.as_bytes())
.expect("Failed to write to script file");
// Initialize utils.rhai with the content from initial_utils.rhai
let initial_utils_path = PathBuf::from("examples/hot_reload/initial_utils.rhai");
let initial_utils_content = fs::read_to_string(&initial_utils_path)
.expect("Failed to read initial utils file");
let mut utils_file = File::create(&utils_script_path)
.expect("Failed to open utils file for writing");
utils_file.write_all(initial_utils_content.as_bytes())
.expect("Failed to write to utils file");
// Create the hot-reloadable system with both script paths
// We're passing a slice with both paths and using None for main_script_index
// to use the default (first script in the slice)
let system = create_hot_reloadable_system(&[main_script_path.clone(), utils_script_path.clone()], None)?;
// Start a thread that periodically executes the script
let execution_thread = thread::spawn(move || {
// Every second, call the greet function from the script
loop {
// Call the greet function
match system.call_fn::<String>("greet", ("User",)) {
Ok(result) => println!("Execution result: {}", result),
Err(err) => println!("Error executing script: {}", err),
}
// Call the add function
match system.call_fn::<i32>("add", (40, 2)) {
Ok(result) => println!("Add result: {}", result),
Err(err) => println!("Error executing add function: {}", err),
}
// Call the advanced_calculation function that uses utils.rhai
match system.call_fn::<i64>("advanced_calculation", (5_i64, 7_i64)) {
Ok(result) => println!("Advanced calculation result: {}", result),
Err(err) => println!("Error executing advanced_calculation function: {}", err),
}
// Try to call the multiply function, catch any errors
match system.call_fn::<i32>("multiply", (40, 2)) {
Ok(result) => println!("Multiply result: {}", result),
Err(err) => {
if err.to_string().contains("function not found") {
println!("Multiply function not available yet");
} else {
println!("Error executing multiply function: {}", err);
}
}
}
// Try to call the divide function, catch any errors
match system.call_fn::<i32>("divide", (40, 2)) {
Ok(result) => println!("Divide result: {}", result),
Err(err) => {
if err.to_string().contains("function not found") {
println!("Divide function not available yet");
} else {
println!("Error executing divide function: {}", err);
}
}
}
// Wait before the next execution
thread::sleep(Duration::from_secs(1));
}
});
// Start a thread to modify the script files at intervals
let main_script_path_clone = main_script_path.clone();
let utils_script_path_clone = utils_script_path.clone();
thread::spawn(move || {
// Wait 5 seconds before first modification
thread::sleep(Duration::from_secs(5));
// First modification - add multiply function
modify_script(
&main_script_path_clone,
"examples/hot_reload/modified_script.rhai",
10,
"Modifying the script to add multiply function..."
);
// Second modification - add divide function
modify_script(
&main_script_path_clone,
"examples/hot_reload/second_modified_script.rhai",
0,
"Modifying the script again to add divide function..."
);
// Third modification - modify utils.rhai
modify_script(
&utils_script_path_clone,
"examples/hot_reload/modified_utils.rhai",
0,
"Modifying the utils script..."
);
});
// Wait for the execution thread to finish (it won't, but this keeps the main thread alive)
execution_thread.join().unwrap();
Ok(())
}

View File

@@ -0,0 +1,22 @@
// This is a modified script
// The AST will be replaced with this new version
// A simple greeting function with modified message
fn greet(name) {
"Hello, " + name + "! This is the MODIFIED script! Hot reloading works!"
}
// A function to calculate the sum of two numbers
fn add(a, b) {
a + b
}
// A new function added during hot reload
fn multiply(a, b) {
a * b
}
// A new function added during hot reload
fn divide(a, b) {
a / b
}

View File

@@ -0,0 +1,18 @@
// Utility functions for the hot reload example - MODIFIED VERSION
// A function to format a greeting message
fn format_greeting(name) {
"ENHANCED Greetings, " + name + "! (from modified utils.rhai)"
}
// A function to perform a calculation
// Keep it simple to avoid type issues
fn calculate(a, b) {
// Enhanced calculation with additional operations
(a * b * 2) + 20
}
// A new utility function
fn format_message(text) {
"*** " + text + " ***"
}

View File

@@ -0,0 +1,22 @@
// This is a completely overwritten script
// The AST will be replaced with this new version
// A simple greeting function with modified message
fn greet(name) {
"Hello, " + name + "! This is the COMPLETELY OVERWRITTEN script!"
}
// A function to calculate the sum of two numbers
fn add(a, b) {
a + b
}
// A new function added during hot reload
fn multiply(a, b) {
a * b
}
// Another new function added during hot reload
fn divide(a, b) {
a / b
}

View File

@@ -0,0 +1,22 @@
// This is a completely overwritten script
// The AST will be replaced with this new version
// A simple greeting function with modified message
fn greet(name) {
"Hello, " + name + "! This is the COMPLETELY OVERWRITTEN script!"
}
// A function to calculate the sum of two numbers
fn add(a, b) {
a + b
}
// A new function added during hot reload
fn multiply(a, b) {
a * b
}
// Another new function added during hot reload
fn divide(a, b) {
a / b
}

View File

@@ -0,0 +1,18 @@
// Utility functions for the hot reload example - MODIFIED VERSION
// A function to format a greeting message
fn format_greeting(name) {
"ENHANCED Greetings, " + name + "! (from modified utils.rhai)"
}
// A function to perform a calculation
// Keep it simple to avoid type issues
fn calculate(a, b) {
// Enhanced calculation with additional operations
(a * b * 2) + 20
}
// A new utility function
fn format_message(text) {
"*** " + text + " ***"
}