wip: threadsafe

This commit is contained in:
Jonathan Kelley committed 2021-10-01 02:07:12 -04:00
1 parent 71f0df6374
commit 82953f2ac3
22 files changed
+414 -545

No files matched your search

+4
View File
@@ -1,4 +1,8 @@
//! Public APIs for managing component state, tasks, and lifecycles.
//!
//! This module is separate from `Scope` to narrow what exactly is exposed to user code.
//!
//! We unsafely implement `send` for the VirtualDom, but those guarantees can only be
use crate::innerlude::*;
use std::{any::TypeId, ops::Deref, rc::Rc};
+4 -3
View File
@@ -14,6 +14,7 @@ use std::{
fmt::Debug,
ops::Deref,
rc::Rc,
sync::Arc,
};
#[derive(Debug)]
@@ -216,13 +217,13 @@ pub mod on {
)* ) => {
$(
$(#[$attr])*
pub struct $wrapper(pub Rc<dyn $eventdata>);
pub struct $wrapper(pub Arc<dyn $eventdata>);
// todo: derefing to the event is fine (and easy) but breaks some IDE stuff like (go to source)
// going to source in fact takes you to the source of Rc which is... less than useful
// Either we ask for this to be changed in Rust-analyzer or manually impkement the trait
impl Deref for $wrapper {
type Target = Rc<dyn $eventdata>;
type Target = Arc<dyn $eventdata>;
fn deref(&self) -> &Self::Target {
&self.0
}
@@ -598,7 +599,7 @@ pub mod on {
];
}
pub struct GenericEvent(pub Rc<dyn GenericEventInner>);
pub struct GenericEvent(pub Arc<dyn GenericEventInner>);
pub trait GenericEventInner {
/// Return a reference to the raw event. User will need to downcast the event to the right platform-specific type.
+3 -1
View File
@@ -29,6 +29,7 @@ pub mod scheduler;
pub mod scope;
pub mod tasks;
pub mod test_dom;
pub mod threadsafe;
pub mod util;
pub mod virtual_dom;
@@ -50,6 +51,7 @@ pub(crate) mod innerlude {
pub use crate::scope::*;
pub use crate::tasks::*;
pub use crate::test_dom::*;
pub use crate::threadsafe::*;
pub use crate::util::*;
pub use crate::virtual_dom::*;
@@ -60,7 +62,7 @@ pub(crate) mod innerlude {
pub use crate::innerlude::{
Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority, LazyNodes, MountType,
Mutations, NodeFactory, Properties, ScopeId, SuspendedContext, SyntheticEvent, TaskHandle,
TestDom, UserEvent, VNode, VirtualDom, FC,
TestDom, ThreadsafeVirtualDom, UserEvent, VNode, VirtualDom, FC,
};
pub mod prelude {
+1
View File
@@ -7,6 +7,7 @@ use std::{
future::Future,
pin::Pin,
rc::Rc,
sync::Arc,
};
/// Every component in Dioxus is represented by a `Scope`.
+58
View File
@@ -0,0 +1,58 @@
//! A threadsafe wrapper for the VirtualDom
use std::sync::{Arc, Mutex, MutexGuard};
use crate::VirtualDom;
/// A threadsafe wrapper for the Dioxus VirtualDom.
///
/// The Dioxus VirtualDom is not normally `Send` because user code can contain non-`Send` types. However, it is important
/// to have a VirtualDom that is `Send` when used in server-side code since very few web frameworks support non-send
/// handlers.
///
/// To address this, we have the `ThreadsafeVirtualDom` type which is a threadsafe wrapper for the VirtualDom. To access
/// the VirtualDom, it must be first unlocked using the `lock` method. This locks the VirtualDom through a mutex and
/// prevents any user code from leaking out. It is not possible to acquire any non-`Send` types from inside the VirtualDom.
///
/// The only way data may be accessed through the VirtualDom is from the "root props" method or by accessing a `Scope`
/// directly. Even then, it's not possible to access any hook data. This means that non-Send types are only "in play"
/// while the VirtualDom is locked with a non-Send marker.
///
/// Note that calling "wait for work" on the regular VirtualDom is inherently non-Send. If there are async tasks that
/// need to be awaited, they must be done thread-local since we don't place any requirements on user tasks. This can be
/// done with the function "spawn_local" in either tokio or async_std.
pub struct ThreadsafeVirtualDom {
inner: Arc<Mutex<VirtualDom>>,
}
impl ThreadsafeVirtualDom {
pub fn new(inner: VirtualDom) -> Self {
let inner = Arc::new(Mutex::new(inner));
Self { inner }
}
pub fn lock(&self) -> Option<VirtualDomGuard> {
let locked = self.inner.lock().unwrap();
Some(VirtualDomGuard { guard: locked })
}
}
unsafe impl Send for ThreadsafeVirtualDom {}
pub struct VirtualDomGuard<'a> {
guard: MutexGuard<'a, VirtualDom>,
}
impl<'a> std::ops::Deref for VirtualDomGuard<'a> {
type Target = MutexGuard<'a, VirtualDom>;
fn deref(&self) -> &Self::Target {
&self.guard
}
}
impl<'a> std::ops::DerefMut for VirtualDomGuard<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.guard
}
}
+60 -49
View File
@@ -20,8 +20,7 @@
//! Additional functionality is defined in the respective files.
use crate::innerlude::*;
use futures_util::Future;
use std::any::Any;
use std::{any::Any, rc::Rc, sync::Arc};
/// An integrated virtual node system that progresses events and diffs UI trees.
///
@@ -60,10 +59,10 @@ pub struct VirtualDom {
root_fc: Box<dyn Any>,
root_props: Box<dyn Any>,
root_props: Rc<dyn Any + Send>,
// we need to keep the allocation around, but we don't necessarily use it
_root_caller: Box<dyn for<'b> Fn(&'b Scope) -> DomTree<'b> + 'static>,
_root_caller: RootCaller,
}
impl VirtualDom {
@@ -121,16 +120,15 @@ impl VirtualDom {
/// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
/// let mutations = dom.rebuild();
/// ```
pub fn new_with_props<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
pub fn new_with_props<P: 'static + Send>(root: FC<P>, root_props: P) -> Self {
let root_fc = Box::new(root);
let root_props: Box<dyn Any> = Box::new(root_props);
let props_ptr = root_props.downcast_ref::<P>().unwrap() as *const P;
let root_props: Rc<dyn Any + Send> = Rc::new(root_props);
let _p = root_props.clone();
// Safety: this callback is only valid for the lifetime of the root props
let root_caller: Box<dyn Fn(&Scope) -> DomTree> = Box::new(move |scope: &Scope| unsafe {
let props: &'_ P = &*(props_ptr as *const P);
let root_caller: Rc<dyn Fn(&Scope) -> DomTree> = Rc::new(move |scope: &Scope| unsafe {
let props = _p.downcast_ref::<P>().unwrap();
std::mem::transmute(root(Context { scope }, props))
});
@@ -148,7 +146,7 @@ impl VirtualDom {
});
Self {
_root_caller: root_caller,
_root_caller: RootCaller(root_caller),
root_fc,
base_scope,
scheduler,
@@ -189,14 +187,14 @@ impl VirtualDom {
/// ```
pub fn update_root_props<P>(&mut self, root_props: P) -> Option<Mutations>
where
P: 'static,
P: 'static + Send,
{
let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap();
// Pre-emptively drop any downstream references of the old props
root_scope.ensure_drop_safety(&self.scheduler.pool);
let mut root_props: Box<dyn Any> = Box::new(root_props);
let mut root_props: Rc<dyn Any + Send> = Rc::new(root_props);
if let Some(props_ptr) = root_props.downcast_ref::<P>().map(|p| p as *const P) {
// Swap the old props and new props
@@ -345,46 +343,47 @@ impl VirtualDom {
/// Waits for the scheduler to have work
/// This lets us poll async tasks during idle periods without blocking the main thread.
pub async fn wait_for_work(&mut self) {
if self.scheduler.has_any_work() {
log::debug!("No need to wait for work, we already have some");
return;
}
todo!("making vdom send right now");
// if self.scheduler.has_any_work() {
// log::debug!("No need to wait for work, we already have some");
// return;
// }
log::debug!("No active work.... waiting for some...");
use futures_util::StreamExt;
// log::debug!("No active work.... waiting for some...");
// use futures_util::StreamExt;
// right now this won't poll events if there is ongoing work
// in the future we want to prioritize some events over ongoing work
// this is coming in the "priorities" PR
// // right now this won't poll events if there is ongoing work
// // in the future we want to prioritize some events over ongoing work
// // this is coming in the "priorities" PR
// Wait for any new events if we have nothing to do
// todo: poll the events once even if there is work to do to prevent starvation
futures_util::select! {
_ = self.scheduler.async_tasks.next() => {}
msg = self.scheduler.receiver.next() => {
match msg.unwrap() {
SchedulerMsg::Task(t) => todo!(),
SchedulerMsg::Immediate(im) => {
self.scheduler.dirty_scopes.insert(im);
}
SchedulerMsg::UiEvent(evt) => {
self.scheduler.ui_events.push_back(evt);
}
}
},
}
// // Wait for any new events if we have nothing to do
// // todo: poll the events once even if there is work to do to prevent starvation
// futures_util::select! {
// _ = self.scheduler.async_tasks.next() => {}
// msg = self.scheduler.receiver.next() => {
// match msg.unwrap() {
// SchedulerMsg::Task(t) => todo!(),
// SchedulerMsg::Immediate(im) => {
// self.scheduler.dirty_scopes.insert(im);
// }
// SchedulerMsg::UiEvent(evt) => {
// self.scheduler.ui_events.push_back(evt);
// }
// }
// },
// }
while let Ok(Some(msg)) = self.scheduler.receiver.try_next() {
match msg {
SchedulerMsg::Task(t) => todo!(),
SchedulerMsg::Immediate(im) => {
self.scheduler.dirty_scopes.insert(im);
}
SchedulerMsg::UiEvent(evt) => {
self.scheduler.ui_events.push_back(evt);
}
}
}
// while let Ok(Some(msg)) = self.scheduler.receiver.try_next() {
// match msg {
// SchedulerMsg::Task(t) => todo!(),
// SchedulerMsg::Immediate(im) => {
// self.scheduler.dirty_scopes.insert(im);
// }
// SchedulerMsg::UiEvent(evt) => {
// self.scheduler.ui_events.push_back(evt);
// }
// }
// }
}
}
@@ -407,3 +406,15 @@ impl std::fmt::Display for VirtualDom {
renderer.render(self, root, f, 0)
}
}
/*
Send safety...
The VirtualDom can only be "send" if the internals exposed to user code are also "send".
IE it's okay to move an Rc from one thread to another thread as long as it's only used internally
*/
// we never actually use the contents of this root caller
struct RootCaller(Rc<dyn for<'b> Fn(&'b Scope) -> DomTree<'b> + 'static>);