Skip to content

Commit

Permalink
chore: add info to explain how arc to box works (#195)
Browse files Browse the repository at this point in the history
* chore: add info to explain how arc to box works

* add comments

* update state comments
  • Loading branch information
elcharitas authored Sep 26, 2024
1 parent d4d6fa3 commit fa2a940
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 11 deletions.
13 changes: 10 additions & 3 deletions crates/shared/src/server/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,19 @@ impl<T: AppState> AppState for Box<T> {
}
}

/// # Panics
/// Panics if the state has been dropped. This should never happen unless the state is dropped manually.
impl From<&Arc<Box<dyn AppState>>> for Box<dyn AppState> {
fn from(value: &Arc<Box<dyn AppState>>) -> Self {
// creating a clone is essential since this ref will be dropped after this function returns
let arc_clone = value.clone();
let state_ref: &dyn AppState = &**arc_clone;

let state_ptr: *const dyn AppState = state_ref as *const dyn AppState;

let nn_ptr = std::ptr::NonNull::new(state_ptr as *mut dyn AppState).unwrap();
// SAFETY: state_ptr is not null, it is safe to convert it to a NonNull pointer, this way we can safely convert it back to a Box
let nn_ptr = std::ptr::NonNull::new(state_ptr as *mut dyn AppState)
.expect("State has been dropped, ensure it is being cloned correctly."); // This should never happen, if it does, it's a bug
let raw_ptr = nn_ptr.as_ptr();

unsafe { Box::from_raw(raw_ptr) }
Expand Down Expand Up @@ -448,12 +453,14 @@ impl NgynContext {
/// context.execute(&mut response).await;
/// ```
pub(crate) async fn execute(&mut self, res: &mut NgynResponse) {
// safely consume the route information, it will be set again if needed
let (handler, controller) = match self.route_info.take() {
Some((handler, ctrl)) => (handler, ctrl),
None => return,
};
let mut controller =
ManuallyDrop::<Box<dyn NgynController>>::new(controller.clone().into());
// allow the controller to live even after the request is handled, until the server is stopped or crashes or in weird cases, the controller is dropped.
// If the controller is dropped, the server will panic.
let mut controller = ManuallyDrop::<Box<dyn NgynController>>::new(controller.into()); // panics if the controller has been dropped
controller.handle(&handler, self, res).await;
}
}
Expand Down
66 changes: 58 additions & 8 deletions crates/shared/src/traits/controller_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,68 @@ pub trait NgynController: NgynInjectable + Sync + Send {
}

/// In Ngyn, controllers are stored as `Arc<Box<dyn NgynController>>`.
/// This is because controllers are shared across threads and need to be cloned easily.
/// And we do this because controllers are shared across threads and an arc guarantees
/// that the controller is not dropped until all references to it are dropped.
///
/// Here's how we convert an `Arc<Box<dyn NgynController>>` to a `Box<dyn NgynController>`.
/// This conversion allows us to mutably borrow the controller and handle routing logic.
/// When working with controllers, you'd quickly notice that Ngyn allows you to define routes that require mutable access to the controller.
/// For instance, take this sample controller:
/// ```rust ignore
/// #[controller]
/// struct TestController;
///
/// #[routes]
/// impl TestController {
/// #[get("/")]
/// async fn index(&mut self) -> String {
/// "Hello, World!".to_string()
/// }
/// }
/// ```
///
/// In the above example, the `index` method requires mutable access to the controller. This pattern, though not encouraged (check app states), is allowed in Ngyn.
/// You could for instance create a localized state in the controller that is only accessible to the controller and its routes.
/// The way Ngyn allows this without performance overhead is through a specialized `Arc -> Box` conversion that only works so well becasue of how Ngyn is designed.
///
/// HOW DOES IT WORK?
///
/// ```text
/// +-----------------+ +-----------------+ +-----------------+
/// | Arc<Box<Ctrl>> | | Arc<Box<Ctrl>> | | Arc<Box<Ctrl>> |
/// +-----------------+ +-----------------+ +-----------------+
/// | | |
/// +-----------------+ +-----------------+ +-----------------+
/// | &Box<Ctrl> | | &Box<Ctrl> | | &Box<Ctrl> |
/// +-----------------+ +-----------------+ +-----------------+
/// | | |
/// +-----------------+ +-----------------+ +-----------------+
/// | &mut Ctrl | | &mut Ctrl | | &mut Ctrl |
/// +-----------------+ +-----------------+ +-----------------+
/// | | |
/// +-----------------+ +-----------------+ +-----------------+
/// | *mut Ctrl | | *mut Ctrl | | *mut Ctrl |
/// +-----------------+ +-----------------+ +-----------------+
/// | | |
/// +-----------------+ +-----------------+ +-----------------+
/// | Box<Ctrl> | | Box<Ctrl> | | Box<Ctrl> |
/// +-----------------+ +-----------------+ +-----------------+
///
/// ```
///
///
/// When a controller is created, we box it and then wrap it in an Arc. This way, the controller is converted to a trait object and can be shared across threads.
/// The trait object is what allows us to call the controller's methods from the server. But when we need mutable access to the controller, we convert it back to a Box.
/// Rather than making use of a mutex, what we do is get the raw pointer of the initial controller, ensure it's not null, and then convert it back to a Box.
///
/// # Panics
/// Panics if the controller has been dropped. This should never happen unless the controller is dropped manually.
impl From<Arc<Box<dyn NgynController>>> for Box<dyn NgynController> {
fn from(arc: Arc<Box<dyn NgynController>>) -> Self {
let arc_clone = arc.clone();
let controller_ref: &dyn NgynController = &**arc_clone;

fn from(controller_arc: Arc<Box<dyn NgynController>>) -> Self {
let controller_ref: &dyn NgynController = &**controller_arc;
let controller_ptr: *const dyn NgynController = controller_ref as *const dyn NgynController;

let nn_ptr = NonNull::new(controller_ptr as *mut dyn NgynController).unwrap();
// SAFETY: controller_ptr is not null, it is safe to convert it to a NonNull pointer, this way we can safely convert it back to a Box
let nn_ptr = NonNull::new(controller_ptr as *mut dyn NgynController)
.expect("Controller has been dropped, ensure it is being cloned correctly.");
let raw_ptr = nn_ptr.as_ptr();

unsafe { Box::from_raw(raw_ptr) }
Expand Down

0 comments on commit fa2a940

Please sign in to comment.