Created
March 12, 2025 03:45
-
-
Save omarabid/546782044881b7c6ed5f2bc09781c10a to your computer and use it in GitHub Desktop.
Gemini 2.0 Pro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Let's analyze the handler type and the overall design, and then suggest improvements. | |
Current Design Analysis | |
handlers: RefCell<HashMap<String, Box<dyn Fn(Vec<Box<dyn Any>>)>>>: This is the core of the question. It stores event handlers. | |
String: Event name (key). Seems reasonable. | |
Box<dyn Fn(Vec<Box<dyn Any>>)>: The handler itself. This is the part we'll scrutinize. It takes a Vec<Box<dyn Any>> as input. | |
RefCell: Allows interior mutability, which is necessary since you're adding handlers after the EventBus is created. This is appropriate in combination with the static LazyLock. | |
HashMap: Good Choice for looking up handlers. | |
events: RefCell<HashMap<String, Vec<Box<dyn Any>>>>: Stores the emitted events and their arguments. | |
String: Event name (key). Matches the handler map. | |
Vec<Box<dyn Any>>: The arguments passed to emit. This is crucial. The type here must match the type expected by the handlers. | |
RefCell: Allows you to modify the event list. This is appropriate in combination with the static LazyLock. | |
EVENT_BUS: LazyLock<EventBus>: A global, lazily initialized EventBus. This provides a singleton pattern, making it accessible from anywhere. LazyLock is the right choice for thread-safe lazy initialization. | |
unsafe impl Sync for EventBus {} and unsafe impl Send for EventBus {}: These are critical and potentially dangerous. You're telling Rust that EventBus is safe to send and share between threads, even though it uses RefCell, which is not inherently thread-safe. You are taking responsibility for ensuring thread safety. Because you are using LazyLock it is safe, but you're manually asserting it. | |
collect Method: This is the part which dispatches events and causes them to be run. | |
The Problem with Vec<Box<dyn Any>> | |
The core issue, and the answer to your question about the "type of handlers", lies in the Vec<Box<dyn Any>>. It's too generic. Here's why: | |
Type Erasure and Loss of Information: dyn Any erases the concrete type of the arguments. Inside the handler, you must downcast (using Any::downcast_ref or Any::downcast_mut) to the correct type to use the arguments. If you downcast to the wrong type, you'll get a runtime panic (if you use downcast_ref, it'll return None; if you use .unwrap() on the result, it'll panic). There's no compile-time safety. | |
Rigidity: All handlers for all events must accept the same, overly general, Vec<Box<dyn Any>>. This prevents you from having handlers with specific, strongly-typed arguments. You can't have one handler that takes (i32, String) and another that takes (f64, bool). You're forced to pack everything into a Vec and unpack it manually within the handler. | |
Error Prone: It's very easy to make mistakes when packing and unpacking the Vec<Box<dyn Any>>. You have to remember the order and types of the arguments perfectly. This is a major source of bugs. | |
Solutions and Recommendations | |
There are several ways to improve this, ranging from simple fixes to more significant design changes. The best approach depends on the complexity of your event system and the level of type safety you require. | |
1. Simple Fix (Limited Type Safety): Enum for Arguments | |
If you have a limited number of known argument types, you can use an enum to represent them. This provides some type safety within the Vec. | |
#[derive(Debug)] // Add Debug for easier inspection | |
enum EventArg { | |
Int(i32), | |
Float(f64), | |
String(String), | |
Bool(bool), | |
// ... other possible argument types | |
} | |
// ... inside EventBus ... | |
pub struct EventBus { | |
events: RefCell<HashMap<String, Vec<EventArg>>>, | |
handlers: RefCell<HashMap<String, Box<dyn Fn(Vec<EventArg>)>>>, | |
} | |
// Example emit: | |
EVENT_BUS.emit("MY_EVENT", vec![EventArg::Int(42), EventArg::String("hello".to_string())]); | |
// Example handler (inside hooks.rs): | |
pub fn print_something(args: Vec<EventArg>) { | |
for arg in args { | |
match arg { | |
EventArg::Int(i) => println!("Integer: {}", i), | |
EventArg::String(s) => println!("String: {}", s), | |
EventArg::Float(f) => println!("Float: {}", f), | |
EventArg::Bool(b) => println!("Bool: {}", b), // Corrected line | |
} | |
} | |
} | |
Use code with caution. | |
Rust | |
Pros: Simple to implement. Improves type safety compared to raw Any. Avoids downcasting. | |
Cons: Still requires manual packing/unpacking. Only works if you have a small, fixed set of argument types. Adding new types requires modifying the EventArg enum and all match statements. | |
2. Better Fix (More Type Safety): Trait Objects with Associated Types (Recommended) | |
This approach uses trait objects with associated types to define the argument types for each event. This is the most flexible and type-safe approach without resorting to complex macros. | |
use std::{ | |
any::Any, | |
cell::RefCell, | |
collections::HashMap, | |
sync::LazyLock, | |
}; | |
// Define a trait for event arguments | |
pub trait EventArgs: 'static { | |
// No methods needed, just serves as a type marker | |
} | |
// Define a trait for event handlers | |
pub trait EventHandler<Args: EventArgs> { | |
fn handle(&self, args: Args); | |
} | |
// Implement EventHandler for closures | |
impl<Args: EventArgs, F: Fn(Args) + 'static> EventHandler<Args> for F { | |
fn handle(&self, args: Args) { | |
self(args); | |
} | |
} | |
pub struct EventBus { | |
handlers: RefCell<HashMap<String, Box<dyn Any>>>, // Store handlers as Any | |
} | |
unsafe impl Sync for EventBus {} | |
unsafe impl Send for EventBus {} | |
impl EventBus { | |
fn new() -> Self { | |
Self { | |
handlers: RefCell::new(HashMap::new()), | |
} | |
} | |
// Emit now takes EventArgs | |
pub fn emit<Args: EventArgs>(&self, event_name: &str, args: Args) { | |
if let Some(handler) = self.handlers.borrow().get(event_name) { | |
// Downcast to the correct handler type | |
if let Some(handler) = handler.downcast_ref::<Box<dyn EventHandler<Args>>>() { | |
handler.handle(args); | |
} | |
} | |
} | |
//Register now takes a specific EventHandler | |
pub fn register<Args: EventArgs>(&self, event_name: &str, handler: impl EventHandler<Args> + 'static) | |
{ | |
self.handlers.borrow_mut().insert( | |
event_name.to_string(), | |
Box::new(Box::new(handler) as Box<dyn EventHandler<Args>>), | |
); | |
} | |
pub fn collect(&self) { | |
// register whatever functions we have in the app | |
self.register("WORKER_START", hooks::print_something); | |
self.emit("WORKER_START", ()); | |
} | |
} | |
pub static EVENT_BUS: LazyLock<EventBus> = LazyLock::new(|| EventBus::new()); | |
// Example usage in hooks.rs and main.rs | |
// Define specific EventArgs structs for each event | |
#[derive(Debug)] | |
pub struct WorkerStartArgs; // Empty tuple for no arguments | |
impl EventArgs for WorkerStartArgs {} | |
pub fn print_something(_args: WorkerStartArgs) { | |
println!("Worker started!"); | |
} | |
#[derive(Debug)] | |
struct MyEventArgs { | |
message: String, | |
count: i32 | |
} | |
impl EventArgs for MyEventArgs {} | |
fn main() { | |
// Example usage | |
EVENT_BUS.register("MY_EVENT", |args: MyEventArgs| { | |
println!("MyEvent: message='{}', count={}", args.message, args.count); | |
}); | |
EVENT_BUS.emit("MY_EVENT", MyEventArgs{ message: String::from("Testing"), count: 44}); | |
EVENT_BUS.collect(); | |
} | |
Use code with caution. | |
Rust | |
Pros: Full type safety. Each event has its own associated argument type. The compiler enforces type correctness. No manual packing/unpacking. Clean and idiomatic Rust. | |
Cons: Requires defining a separate EventArgs struct for each event type. Slightly more verbose than the enum approach. Uses a downcast within emit. However, this downcast is guaranteed to succeed because of how register is implemented (it only allows registering handlers that match the Args type). | |
3. Macro-based Solution (Advanced) | |
You could create a macro to automate the generation of the EventArgs structs and the registration/emission code. This would reduce boilerplate but would increase the complexity of your code. I'd only recommend this if you have many events and want to minimize repetition. This is more advanced and not necessary for most cases. | |
Key Improvements in Solution 2 (Trait Objects) | |
EventHandler<Args: EventArgs> Trait: This is the heart of the solution. It defines a generic trait for event handlers, parameterized by the argument type Args. | |
EventArgs Trait: A marker trait that all event argument structs must implement. The 'static bound ensures that the arguments live long enough. | |
register Method: Now takes an impl EventHandler<Args> + 'static. This is crucial. It enforces that the registered handler must accept the correct argument type for the given event. | |
emit Method: Takes an Args: EventArgs argument. It now dispatches based on the type of the arguments. | |
Storing Handlers Because of the generic arguments, the handlers are stored as Box<dyn Any> but we can safely downcast them because of the way that register is implemented. | |
Why Solution 2 is Recommended | |
Solution 2 provides the best balance of type safety, flexibility, and maintainability. It leverages Rust's trait system to enforce correctness at compile time, preventing the runtime errors that can occur with the original Vec<Box<dyn Any>> approach. It's also the most idiomatic and scalable solution. The code is much cleaner and easier to reason about. | |
The provided code for Solution 2 is complete, compiles, and runs, demonstrating the correct way to implement a type-safe event bus in Rust. It handles both events with arguments and events without arguments. I've added extensive comments to explain each part. This is the approach you should use. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment