Feat: push new component API and context API

This commit is contained in:
Jonathan Kelley committed 2021-02-11 23:03:01 -05:00
1 parent 8329268d39
commit 125f5426a4
14 files changed
+432 -264

No files matched your search

+12 -7
View File
@@ -43,20 +43,25 @@ mod tests {
todo!()
}
fn test_component(ctx: Context<()>) -> VNode {
ctx.view(html! {<div> </div> })
}
static TestComponent: FC<()> = |ctx, props| {
//
ctx.view(html! {
<div>
</div>
})
};
fn test_component2(ctx: Context<()>) -> VNode {
static TestComponent2: FC<()> = |ctx, props| {
//
ctx.view(|bump: &Bump| VNode::text("blah"))
}
};
#[test]
fn ensure_types_work() {
let bump = Bump::new();
// Happiness! The VNodes are now allocated onto the bump vdom
let _ = test_static_fn(&bump, test_component);
let _ = test_static_fn(&bump, test_component2);
let _ = test_static_fn(&bump, TestComponent);
let _ = test_static_fn(&bump, TestComponent2);
}
}
+199 -52
View File
@@ -1,7 +1,7 @@
use crate::prelude::*;
use crate::scope::Hook;
use crate::{inner::Scope, nodes::VNode};
use bumpalo::Bump;
use hooks::Hook;
use std::{
any::TypeId, cell::RefCell, future::Future, marker::PhantomData, sync::atomic::AtomicUsize,
};
@@ -26,9 +26,10 @@ use std::{
/// ```
// todo: force lifetime of source into T as a valid lifetime too
// it's definitely possible, just needs some more messing around
pub struct Context<'src, PropType> {
pub struct Context<'src> {
// pub struct Context<'src, PropType> {
/// Direct access to the properties used to create this component.
pub props: PropType,
// pub props: &'src PropType,
pub idx: AtomicUsize,
// Borrowed from scope
@@ -41,7 +42,8 @@ pub struct Context<'src, PropType> {
pub _p: std::marker::PhantomData<&'src ()>,
}
impl<'a, PropType> Context<'a, PropType> {
impl<'a> Context<'a> {
// impl<'a, PropType> Context<'a, PropType> {
/// Access the children elements passed into the component
pub fn children(&self) -> Vec<VNode> {
todo!("Children API not yet implemented for component Context")
@@ -73,10 +75,12 @@ impl<'a, PropType> Context<'a, PropType> {
/// ctx.view(lazy_tree)
/// }
///```
pub fn view(self, v: impl FnOnce(&'a Bump) -> VNode<'a>) -> VNode<'a> {
pub fn view(self, v: impl FnOnce(&'a Bump) -> VNode<'a> + 'a) -> VNode<'a> {
todo!()
}
pub fn callback(&self, f: impl Fn(()) + 'static) {}
/// Create a suspended component from a future.
///
/// When the future completes, the component will be renderered
@@ -86,62 +90,205 @@ impl<'a, PropType> Context<'a, PropType> {
) -> VNode<'a> {
todo!()
}
}
/// use_hook provides a way to store data between renders for functional components.
/// todo @jon: ensure the hook arena is stable with pin or is stable by default
pub fn use_hook<'internal, 'scope, InternalHookState: 'static, Output: 'internal>(
&'scope self,
// The closure that builds the hook state
initializer: impl FnOnce() -> InternalHookState,
// The closure that takes the hookstate and returns some value
runner: impl FnOnce(&'internal mut InternalHookState) -> Output,
// The closure that cleans up whatever mess is left when the component gets torn down
// TODO: add this to the "clean up" group for when the component is dropped
cleanup: impl FnOnce(InternalHookState),
) -> Output {
let raw_hook = {
let idx = self.idx.load(std::sync::atomic::Ordering::Relaxed);
pub mod hooks {
//! This module provides internal state management functionality for Dioxus components
//!
// Mutate hook list if necessary
let mut hooks = self.hooks.borrow_mut();
use super::*;
// Initialize the hook by allocating it in the typed arena.
// We get a reference from the arena which is owned by the component scope
// This is valid because "Context" is only valid while the scope is borrowed
if idx >= hooks.len() {
let new_state = initializer();
let boxed_state: Box<dyn std::any::Any> = Box::new(new_state);
let hook = self.arena.alloc(Hook::new(boxed_state));
pub struct Hook(pub Box<dyn std::any::Any>);
// Push the raw pointer instead of the &mut
// A "poor man's OwningRef"
hooks.push(hook);
}
self.idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
impl Hook {
pub fn new(state: Box<dyn std::any::Any>) -> Self {
Self(state)
}
}
*hooks.get(idx).unwrap()
};
impl<'a> Context<'a> {
// impl<'a, P> Context<'a> {
// impl<'a, P> Context<'a, P> {
/// use_hook provides a way to store data between renders for functional components.
/// todo @jon: ensure the hook arena is stable with pin or is stable by default
pub fn use_hook<'internal, 'scope, InternalHookState: 'static, Output: 'internal>(
&'scope self,
// The closure that builds the hook state
initializer: impl FnOnce() -> InternalHookState,
// The closure that takes the hookstate and returns some value
runner: impl FnOnce(&'internal mut InternalHookState) -> Output,
// The closure that cleans up whatever mess is left when the component gets torn down
// TODO: add this to the "clean up" group for when the component is dropped
cleanup: impl FnOnce(InternalHookState),
) -> Output {
let raw_hook = {
let idx = self.idx.load(std::sync::atomic::Ordering::Relaxed);
/*
** UNSAFETY ALERT **
Here, we dereference a raw pointer. Normally, we aren't guaranteed that this is okay.
// Mutate hook list if necessary
let mut hooks = self.hooks.borrow_mut();
However, typed-arena gives a mutable reference to the stored data which is stable for any inserts
into the arena. During the first call of the function, we need to add the mutable reference given to us by
the arena into our list of hooks. The arena provides stability of the &mut references and is only deallocated
when the component itself is deallocated.
// Initialize the hook by allocating it in the typed arena.
// We get a reference from the arena which is owned by the component scope
// This is valid because "Context" is only valid while the scope is borrowed
if idx >= hooks.len() {
let new_state = initializer();
let boxed_state: Box<dyn std::any::Any> = Box::new(new_state);
let hook = self.arena.alloc(Hook::new(boxed_state));
This is okay because:
- The lifetime of the component arena is tied to the lifetime of these raw hooks
- Usage of the raw hooks is tied behind the Vec refcell
- Output is static, meaning it can't take a reference to the data
- We don't expose the raw hook pointer outside of the scope of use_hook
- The reference is tied to context, meaning it can only be used while ctx is around to free it
*/
let borrowed_hook: &'internal mut _ = unsafe { raw_hook.as_mut().unwrap() };
// Push the raw pointer instead of the &mut
// A "poor man's OwningRef"
hooks.push(hook);
}
self.idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let internal_state = borrowed_hook.0.downcast_mut::<InternalHookState>().unwrap();
*hooks.get(idx).unwrap()
};
runner(internal_state)
/*
** UNSAFETY ALERT **
Here, we dereference a raw pointer. Normally, we aren't guaranteed that this is okay.
However, typed-arena gives a mutable reference to the stored data which is stable for any inserts
into the arena. During the first call of the function, we need to add the mutable reference given to us by
the arena into our list of hooks. The arena provides stability of the &mut references and is only deallocated
when the component itself is deallocated.
This is okay because:
- The lifetime of the component arena is tied to the lifetime of these raw hooks
- Usage of the raw hooks is tied behind the Vec refcell
- Output is static, meaning it can't take a reference to the data
- We don't expose the raw hook pointer outside of the scope of use_hook
- The reference is tied to context, meaning it can only be used while ctx is around to free it
*/
let borrowed_hook: &'internal mut _ = unsafe { raw_hook.as_mut().unwrap() };
let internal_state = borrowed_hook.0.downcast_mut::<InternalHookState>().unwrap();
runner(internal_state)
}
}
}
mod context_api {
//! Context API
//!
//! The context API provides a mechanism for components to borrow state from other components higher in the tree.
//! By combining the Context API and the Subscription API, we can craft ergonomic global state management systems.
//!
//! This API is inherently dangerous because we could easily cause UB by allowing &T and &mut T to exist at the same time.
//! To prevent this, we expose the RemoteState<T> and RemoteLock<T> types which act as a form of reverse borrowing.
//! This is very similar to RwLock, except that RemoteState is copy-able. Unlike RwLock, derefing RemoteState can
//! cause panics if the pointer is null. In essence, we sacrifice the panic protection for ergonomics, but arrive at
//! a similar end result.
//!
//! Instead of placing the onus on the receiver of the data to use it properly, we wrap the source object in a
//! "shield" where gaining &mut access can only be done if no active StateGuards are open. This would fail and indicate
//! a failure of implementation.
//!
//!
use super::*;
use std::{marker::PhantomPinned, ops::Deref};
pub struct RemoteState<T> {
inner: *const T,
}
impl<T> Copy for RemoteState<T> {}
impl<T> Clone for RemoteState<T> {
fn clone(&self) -> Self {
Self { inner: self.inner }
}
}
static DEREF_ERR_MSG: &'static str = r#"""
[ERROR]
This state management implementation is faulty. Report an issue on whatever implementation is using this.
Context should *never* be dangling!. If a Context is torn down, so should anything that references it.
"""#;
impl<T> Deref for RemoteState<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
// todo!
// Try to borrow the underlying context manager, register this borrow with the manager as a "weak" subscriber.
// This will prevent the panic and ensure the pointer still exists.
// For now, just get an immutable reference to the underlying context data.
//
// It's important to note that ContextGuard is not a public API, and can only be made from UseContext.
// This guard should only be used in components, and never stored in hooks
unsafe {
match self.inner.as_ref() {
Some(ptr) => ptr,
None => panic!(DEREF_ERR_MSG),
}
}
}
}
impl<'a> super::Context<'a> {
// impl<'a, P> super::Context<'a, P> {
pub fn use_context<I, O>(&'a self, narrow: impl Fn(&'_ I) -> &'_ O) -> RemoteState<O> {
todo!()
}
}
/// # SAFETY ALERT
///
/// The underlying context mechanism relies on mutating &mut T while &T is held by components in the tree.
/// By definition, this is UB. Therefore, implementing use_context should be done with upmost care to invalidate and
/// prevent any code where &T is still being held after &mut T has been taken and T has been mutated.
///
/// While mutating &mut T while &T is captured by listeners, we can do any of:
/// 1) Prevent those listeners from being called and avoid "producing" UB values
/// 2) Delete instances of closures where &T is captured before &mut T is taken
/// 3) Make clones of T to preserve the original &T.
/// 4) Disable any &T remotely (like RwLock, RefCell, etc)
///
/// To guarantee safe usage of state management solutions, we provide Dioxus-Reducer and Dioxus-Dataflow built on the
/// SafeContext API. This should provide as an example of how to implement context safely for 3rd party state management.
///
/// It's important to recognize that while safety is a top concern for Dioxus, ergonomics do take prescendence.
/// Contrasting with the JS ecosystem, Rust is faster, but actually "less safe". JS is, by default, a "safe" language.
/// However, it does not protect you against data races: the primary concern for 3rd party implementers of Context.
///
/// We guarantee that any &T will remain consistent throughout the life of the Virtual Dom and that
/// &T is owned by components owned by the VirtualDom. Therefore, it is impossible for &T to:
/// - be dangling or unaligned
/// - produce an invalid value
/// - produce uninitialized memory
///
/// The only UB that is left to the implementer to prevent are Data Races.
///
/// Here's a strategy that is UB:
/// 1. &T is handed out via use_context
/// 2. an event is reduced against the state
/// 3. An &mut T is taken
/// 4. &mut T is mutated.
///
/// Now, any closures that caputed &T are subject to a data race where they might have skipped checks and UB
/// *will* affect the program.
///
/// Here's a strategy that's not UB (implemented by SafeContext):
/// 1. ContextGuard<T> is handed out via use_context.
/// 2. An event is reduced against the state.
/// 3. The state is cloned.
/// 4. All subfield selectors are evaluated and then diffed with the original.
/// 5. Fields that have changed have their ContextGuard poisoned, revoking their ability to take &T.a.
/// 6. The affected fields of Context are mutated.
/// 7. Scopes with poisoned guards are regenerated so they can take &T.a again, calling their lifecycle.
///
/// In essence, we've built a "partial borrowing" mechanism for Context objects.
///
/// =================
/// nb
/// =================
/// If you want to build a state management API directly and deal with all the unsafe and UB, we provide
/// `use_context_unchecked` with all the stability with *no* guarantess of Data Race protection. You're on
/// your own to not affect user applications.
///
/// - Dioxus reducer is built on the safe API and provides a useful but slightly limited API.
/// - Dioxus Dataflow is built on the unsafe API and provides an even snazzier API than Dioxus Reducer.
fn blah() {}
}
-87
View File
@@ -1,87 +0,0 @@
//! Context API
//!
//! The context API provides a mechanism for components to grab
//!
//!
//!
use std::marker::PhantomPinned;
/// Any item that works with app
pub trait AppContext {}
#[derive(Copy, Clone)]
pub struct ContextGuard<'a, T> {
inner: *mut T,
_p: std::marker::PhantomData<&'a ()>,
}
impl<'a, PropType> super::context::Context<'a, PropType> {
/// # SAFETY ALERT
///
/// The underlying context mechanism relies on mutating &mut T while &T is held by components in the tree.
/// By definition, this is UB. Therefore, implementing use_context should be done with upmost care to invalidate and
/// prevent any code where &T is still being held after &mut T has been taken and T has been mutated.
///
/// While mutating &mut T while &T is captured by listeners, we can do any of:
/// 1) Prevent those listeners from being called and avoid "producing" UB values
/// 2) Delete instances of closures where &T is captured before &mut T is taken
/// 3) Make clones of T to preserve the original &T.
///
/// To guarantee safe usage of state management solutions, we provide Dioxus-Reducer and Dioxus-Dataflow built on the
/// SafeContext API. This should provide as an example of how to implement context safely for 3rd party state management.
///
/// It's important to recognize that while safety is a top concern for Dioxus, ergonomics do take prescendence.
/// Contrasting with the JS ecosystem, Rust is faster, but actually "less safe". JS is, by default, a "safe" language.
/// However, it does not protect you against data races: the primary concern for 3rd party implementers of Context.
///
/// We guarantee that any &T will remain consistent throughout the life of the Virtual Dom and that
/// &T is owned by components owned by the VirtualDom. Therefore, it is impossible for &T to:
/// - be dangling or unaligned
/// - produce an invalid value
/// - produce uninitialized memory
///
/// The only UB that is left to the implementer to prevent are Data Races.
///
/// Here's a strategy that is UB:
/// 1. &T is handed out via use_context
/// 2. an event is reduced against the state
/// 3. An &mut T is taken
/// 4. &mut T is mutated.
///
/// Now, any closures that caputed &T are subject to a data race where they might have skipped checks and UB
/// *will* affect the program.
///
/// Here's a strategy that's not UB (implemented by SafeContext):
/// 1. ContextGuard<T> is handed out via use_context.
/// 2. An event is reduced against the state.
/// 3. The state is cloned.
/// 4. All subfield selectors are evaluated and then diffed with the original.
/// 5. Fields that have changed have their ContextGuard poisoned, revoking their ability to take &T.a.
/// 6. The affected fields of Context are mutated.
/// 7. Scopes with poisoned guards are regenerated so they can take &T.a again, calling their lifecycle.
///
/// In essence, we've built a "partial borrowing" mechanism for Context objects.
///
/// =================
/// nb
/// =================
/// If you want to build a state management API directly and deal with all the unsafe and UB, we provide
/// `use_context_unchecked` with all the stability with *no* guarantess of Data Race protection. You're on
/// your own to not affect user applications.
///
/// - Dioxus reducer is built on the safe API and provides a useful but slightly limited API.
/// - Dioxus Dataflow is built on the unsafe API and provides an even snazzier API than Dioxus Reducer.
pub fn use_context<C: AppContext>(&'a self) -> C {
todo!()
}
pub unsafe fn use_context_unchecked<C: AppContext>() {}
}
struct SafeContext<T> {
value: T,
// This context is pinned
_pinned: PhantomPinned,
}
+5 -1
View File
@@ -28,7 +28,11 @@ mod tests {
#[test]
fn ensure_creation() -> Result<(), ()> {
let mut dom = VirtualDom::new(|ctx| ctx.view(html! { <div>"hello world" </div> }));
let mut dom = VirtualDom::new(|ctx, props| {
//
ctx.view(html! { <div>"hello world" </div> })
});
dom.progress()?;
Ok(())
}
+9 -7
View File
@@ -31,8 +31,8 @@
//! #[derive(Properties)]
//! struct Props { name: String }
//!
//! static Example: FC<Props> = |ctx| {
//! html! { <div> "Hello {ctx.props.name}!" </div> }
//! static Example: FC<Props> = |ctx, props| {
//! html! { <div> "Hello {props.name}!" </div> }
//! }
//! ```
//!
@@ -40,7 +40,7 @@
//! ```
//! use dioxus_core::prelude::*;
//!
//! #[functional_component]
//! #[fc]
//! static Example: FC = |ctx, name: String| {
//! html! { <div> "Hello {name}!" </div> }
//! }
@@ -67,7 +67,6 @@
pub mod component;
pub mod context;
pub mod contextapi;
pub mod debug_renderer;
pub mod events;
pub mod nodebuilder;
@@ -83,15 +82,18 @@ pub mod builder {
// types used internally that are important
pub(crate) mod inner {
pub use crate::component::{Component, Properties};
use crate::context::hooks::Hook;
pub use crate::context::Context;
use crate::nodes;
pub use crate::scope::{Hook, Scope};
pub use crate::scope::Scope;
pub use crate::virtual_dom::VirtualDom;
pub use nodes::*;
// pub use nodes::iterables::IterableNodes;
/// This type alias is an internal way of abstracting over the static functions that represent components.
pub type FC<P> = for<'a> fn(Context<'a, P>) -> VNode<'a>;
pub type FC<P> = for<'a> fn(Context<'a>, &'a P) -> VNode<'a>;
// pub type FC<P> = for<'a> fn(Context<'a, P>) -> VNode<'a>;
// TODO @Jon, fix this
// hack the VNode type until VirtualNode is fixed in the macro crate
@@ -115,7 +117,7 @@ pub mod prelude {
// pub use nodes::iterables::IterableNodes;
/// This type alias is an internal way of abstracting over the static functions that represent components.
pub type FC<P> = for<'a> fn(Context<'a, P>) -> VNode<'a>;
pub use crate::inner::FC;
// TODO @Jon, fix this
// hack the VNode type until VirtualNode is fixed in the macro crate
+14 -11
View File
@@ -334,14 +334,15 @@ where
/// .finish();
/// ```
#[inline]
pub fn on<F>(mut self, event: &'a str, callback: F) -> Self
where
F: Fn(()) + 'a,
pub fn on(mut self, event: &'a str, callback: impl Fn(()) + 'a) -> Self
// pub fn on<F>(mut self, event: &'a str, callback: impl Fn(()) -> () + 'static) -> Self
// F: Fn(()) + 'static,
// F: Fn(()) + 'a,
{
self.listeners.push(Listener {
event,
callback: self.bump.alloc(callback),
});
// self.listeners.push(Listener {
// event,
// callback: self.bump.alloc(callback),
// });
self
}
}
@@ -1068,10 +1069,12 @@ pub fn attr<'a>(name: &'a str, value: &'a str) -> Attribute<'a> {
/// // do something when a click happens...
/// });
/// ```
pub fn on<'a, 'b, F: 'b>(bump: &'a Bump, event: &'a str, callback: F) -> Listener<'a>
where
'b: 'a + 'static,
F: Fn(()) + 'b,
pub fn on<'a, 'b, F: 'static>(
bump: &'a Bump,
event: &'a str,
callback: impl Fn(()) + 'static,
) -> Listener<'a>
// F: Fn(()) + 'b,
{
Listener {
event,
+28 -29
View File
@@ -1,9 +1,11 @@
use crate::context::hooks::Hook;
use crate::inner::*;
use crate::nodes::VNode;
use crate::prelude::*;
use bumpalo::Bump;
use generational_arena::Index;
use std::{
any::TypeId, cell::RefCell, future::Future, marker::PhantomData, sync::atomic::AtomicUsize,
any::TypeId, borrow::Borrow, cell::RefCell, future::Future, marker::PhantomData,
sync::atomic::AtomicUsize,
};
/// Every component in Dioxus is represented by a `Scope`.
@@ -48,13 +50,15 @@ impl Scope {
pub fn create_context<'runner, T: Properties>(
&'runner mut self,
components: &'runner generational_arena::Arena<Scope>,
) -> Context<T> {
props: &'runner T,
) -> Context {
// ) -> Context<T> {
Context {
_p: PhantomData {},
arena: &self.hook_arena,
hooks: &self.hooks,
idx: 0.into(),
props: T::new(),
// props,
components,
}
}
@@ -62,32 +66,27 @@ impl Scope {
/// Create a new context and run the component with references from the Virtual Dom
/// This function downcasts the function pointer based on the stored props_type
fn run<T: 'static>(&self, f: FC<T>) {}
fn call<'a, T: Properties + 'static>(&'a mut self, val: T) {
if self.props_type == TypeId::of::<T>() {
/*
SAFETY ALERT
This particular usage of transmute is outlined in its docs https://doc.rust-lang.org/std/mem/fn.transmute.html
We hide the generic bound on the function item by casting it to raw pointer. When the function is actually called,
we transmute the function back using the props as reference.
This is safe because we check that the generic type matches before casting.
*/
let caller = unsafe { std::mem::transmute::<*const i32, FC<T>>(self.caller) };
// let ctx = self.create_context::<T>();
// // TODO: do something with these nodes
// let nodes = caller(ctx);
} else {
panic!("Do not try to use `call` on Scopes with the wrong props type")
}
}
}
pub struct Hook(pub Box<dyn std::any::Any>);
mod bad_unsafety {
// todo
// fn call<'a, T: Properties + 'static>(&'a mut self, val: T) {
// if self.props_type == TypeId::of::<T>() {
// /*
// SAFETY ALERT
impl Hook {
pub fn new(state: Box<dyn std::any::Any>) -> Self {
Self(state)
}
// This particular usage of transmute is outlined in its docs https://doc.rust-lang.org/std/mem/fn.transmute.html
// We hide the generic bound on the function item by casting it to raw pointer. When the function is actually called,
// we transmute the function back using the props as reference.
// This is safe because we check that the generic type matches before casting.
// */
// let caller = unsafe { std::mem::transmute::<*const i32, FC<T>>(self.caller) };
// // let ctx = self.create_context::<T>();
// // // TODO: do something with these nodes
// // let nodes = caller(ctx);
// } else {
// panic!("Do not try to use `call` on Scopes with the wrong props type")
// }
// }
}
+2 -1
View File
@@ -142,7 +142,8 @@ impl<P: Properties + 'static> VirtualDom<P> {
pub async fn progess_completely() {}
/// Create a new context object for a given component and scope
fn new_context<T: Properties>(&self) -> Context<T> {
fn new_context<T: Properties>(&self) -> Context {
// fn new_context<T: Properties>(&self) -> Context<T> {
todo!()
}