dioxus/packages/core/src/lazynodes.rs

320 lines
9.9 KiB
Rust
Raw Normal View History

//! Support for storing lazy-nodes on the stack
//!
//! This module provides support for a type called `LazyNodes` which is a micro-heap located on the stack to make calls
//! to `rsx!` more efficient.
//!
//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on NodeFactory closures.
//!
//! This can be done either through boxing directly, or by using dynamic-sized-types and a custom allocator. In our case,
//! we build a tiny alloactor in the stack and allocate the closure into that.
//!
//! The logic for this was borrowed from <https://docs.rs/stack_dst/0.6.1/stack_dst/>. Unfortunately, this crate does not
//! support non-static closures, so we've implemented the core logic of `ValueA` in this module.
use crate::innerlude::{NodeFactory, VNode};
2021-11-07 06:57:13 +00:00
use std::mem;
/// A concrete type provider for closures that build VNode structures.
///
/// This struct wraps lazy structs that build VNode trees Normally, we cannot perform a blanket implementation over
/// closures, but if we wrap the closure in a concrete type, we can maintain separate implementations of IntoVNode.
///
///
2021-11-12 02:34:20 +00:00
/// ```rust, ignore
/// LazyNodes::new(|f| f.element("div", [], [], [] None))
/// ```
2021-11-01 06:41:23 +00:00
pub struct LazyNodes<'a, 'b> {
inner: StackNodeStorage<'a, 'b>,
}
2021-11-11 21:36:51 +00:00
type StackHeapSize = [usize; 16];
2021-11-01 07:13:31 +00:00
enum StackNodeStorage<'a, 'b> {
Stack(LazyStack),
2021-11-07 06:57:13 +00:00
Heap(Box<dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> + 'b>),
2021-11-01 07:13:31 +00:00
}
2021-11-01 06:41:23 +00:00
impl<'a, 'b> LazyNodes<'a, 'b> {
2022-02-20 22:37:46 +00:00
/// Create a new LazyNodes closure, optimistically placing it onto the stack.
///
/// If the closure cannot fit into the stack allocation (16 bytes), then it
/// is placed on the heap. Most closures will fit into the stack, and is
/// the most optimal way to use the creation function.
pub fn new(val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self {
2021-12-19 04:03:59 +00:00
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
2022-02-20 22:37:46 +00:00
let mut slot = Some(val);
2021-12-19 04:03:59 +00:00
let val = move |fac: Option<NodeFactory<'a>>| {
let inner = slot.take().unwrap();
fac.map(inner)
};
2022-02-20 22:37:46 +00:00
// miri does not know how to work with mucking directly into bytes
// just use a heap allocated type when miri is running
if cfg!(miri) {
Self {
inner: StackNodeStorage::Heap(Box::new(val)),
}
} else {
unsafe { LazyNodes::new_inner(val) }
}
2021-12-19 04:03:59 +00:00
}
2022-02-20 22:37:46 +00:00
/// Create a new LazyNodes closure, but force it onto the heap.
pub fn new_boxed<F>(_val: F) -> Self
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
{
2021-11-07 14:58:19 +00:00
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
2021-11-07 06:49:53 +00:00
let mut slot = Some(_val);
2021-11-01 07:13:31 +00:00
2021-11-07 14:58:19 +00:00
let val = move |fac: Option<NodeFactory<'a>>| {
let inner = slot.take().unwrap();
fac.map(inner)
2021-11-07 06:49:53 +00:00
};
2021-11-01 07:13:31 +00:00
2022-02-20 22:37:46 +00:00
Self {
inner: StackNodeStorage::Heap(Box::new(val)),
2021-11-12 02:34:20 +00:00
}
2021-11-07 06:49:53 +00:00
}
2021-11-01 07:13:31 +00:00
2021-11-07 06:49:53 +00:00
unsafe fn new_inner<F>(val: F) -> Self
where
2021-11-07 06:57:13 +00:00
F: FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> + 'b,
2021-11-07 06:49:53 +00:00
{
2021-11-07 06:57:13 +00:00
let mut ptr: *const _ = &val as &dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>>;
2021-11-07 06:49:53 +00:00
assert_eq!(
ptr as *const u8, &val as *const _ as *const u8,
"MISUSE: Closure returned different pointer"
);
assert_eq!(
std::mem::size_of_val(&*ptr),
std::mem::size_of::<F>(),
"MISUSE: Closure returned a subset pointer"
);
let words = ptr_as_slice(&mut ptr);
assert!(
words[0] == &val as *const _ as usize,
"BUG: Pointer layout is not (data_ptr, info...)"
);
// - Ensure that Self is aligned same as data requires
assert!(
std::mem::align_of::<F>() <= std::mem::align_of::<Self>(),
"TODO: Enforce alignment >{} (requires {})",
std::mem::align_of::<Self>(),
std::mem::align_of::<F>()
);
let info = &words[1..];
let data = words[0] as *mut ();
let size = mem::size_of::<F>();
let stored_size = info.len() * mem::size_of::<usize>() + size;
let max_size = mem::size_of::<StackHeapSize>();
if stored_size > max_size {
Self {
inner: StackNodeStorage::Heap(Box::new(val)),
}
} else {
let mut buf: StackHeapSize = StackHeapSize::default();
2021-11-01 07:13:31 +00:00
2021-11-07 06:49:53 +00:00
assert!(info.len() + round_to_words(size) <= buf.as_ref().len());
2021-11-01 07:13:31 +00:00
2021-11-07 06:49:53 +00:00
// Place pointer information at the end of the region
// - Allows the data to be at the start for alignment purposes
{
let info_ofs = buf.as_ref().len() - info.len();
let info_dst = &mut buf.as_mut()[info_ofs..];
for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) {
*d = *v;
2021-11-01 07:13:31 +00:00
}
2021-11-07 06:49:53 +00:00
}
2021-11-01 07:13:31 +00:00
2021-11-07 06:49:53 +00:00
let src_ptr = data as *const u8;
let dataptr = buf.as_mut()[..].as_mut_ptr() as *mut u8;
for i in 0..size {
*dataptr.add(i) = *src_ptr.add(i);
}
2021-11-01 07:13:31 +00:00
2021-11-07 06:49:53 +00:00
std::mem::forget(val);
2021-11-03 19:13:50 +00:00
2021-11-07 06:49:53 +00:00
Self {
inner: StackNodeStorage::Stack(LazyStack {
_align: [],
buf,
dropped: false,
}),
2021-11-01 07:13:31 +00:00
}
2021-11-01 06:41:23 +00:00
}
}
2022-02-20 22:37:46 +00:00
/// Call the closure with the given factory to produce real VNodes.
///
/// ```rust, ignore
/// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None));
///
/// let fac = NodeFactory::new(&cx);
///
/// let node = f.call(cac);
/// ```
2021-11-07 06:57:13 +00:00
pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> {
2021-11-01 06:41:23 +00:00
match self.inner {
2021-11-07 06:57:13 +00:00
StackNodeStorage::Heap(mut lazy) => lazy(Some(f)).unwrap(),
2021-11-07 06:49:53 +00:00
StackNodeStorage::Stack(mut stack) => stack.call(f),
2021-11-01 06:41:23 +00:00
}
}
}
2021-11-01 07:13:31 +00:00
struct LazyStack {
_align: [u64; 0],
buf: StackHeapSize,
2021-11-07 06:49:53 +00:00
dropped: bool,
2021-11-01 07:13:31 +00:00
}
2021-11-01 07:13:31 +00:00
impl LazyStack {
2021-11-07 06:49:53 +00:00
fn call<'a>(&mut self, f: NodeFactory<'a>) -> VNode<'a> {
let LazyStack { buf, .. } = self;
let data = buf.as_ref();
2021-11-07 06:57:13 +00:00
let info_size =
2021-11-07 14:58:19 +00:00
mem::size_of::<*mut dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>>>()
2021-11-07 06:57:13 +00:00
/ mem::size_of::<usize>()
- 1;
2021-11-07 06:49:53 +00:00
let info_ofs = data.len() - info_size;
2021-11-07 06:57:13 +00:00
let g: *mut dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> =
2021-11-07 06:49:53 +00:00
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
self.dropped = true;
let clos = unsafe { &mut *g };
2021-11-07 06:57:13 +00:00
clos(Some(f)).unwrap()
2021-11-01 07:13:31 +00:00
}
}
impl Drop for LazyStack {
fn drop(&mut self) {
2021-11-07 06:49:53 +00:00
if !self.dropped {
2021-11-07 06:57:13 +00:00
let LazyStack { buf, .. } = self;
let data = buf.as_ref();
let info_size = mem::size_of::<
2021-11-07 14:58:19 +00:00
*mut dyn FnMut(Option<NodeFactory<'_>>) -> Option<VNode<'_>>,
2021-11-07 06:57:13 +00:00
>() / mem::size_of::<usize>()
- 1;
let info_ofs = data.len() - info_size;
let g: *mut dyn FnMut(Option<NodeFactory<'_>>) -> Option<VNode<'_>> =
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
self.dropped = true;
let clos = unsafe { &mut *g };
clos(None);
2021-11-07 06:49:53 +00:00
}
2021-11-01 07:13:31 +00:00
}
}
2021-11-01 07:13:31 +00:00
/// Obtain mutable access to a pointer's words
fn ptr_as_slice<T>(ptr: &mut T) -> &mut [usize] {
assert!(mem::size_of::<T>() % mem::size_of::<usize>() == 0);
let words = mem::size_of::<T>() / mem::size_of::<usize>();
// SAFE: Points to valid memory (a raw pointer)
unsafe { core::slice::from_raw_parts_mut(ptr as *mut _ as *mut usize, words) }
}
/// Re-construct a fat pointer
unsafe fn make_fat_ptr<T: ?Sized>(data_ptr: usize, meta_vals: &[usize]) -> *mut T {
let mut rv = mem::MaybeUninit::<*mut T>::uninit();
{
let s = ptr_as_slice(&mut rv);
s[0] = data_ptr;
s[1..].copy_from_slice(meta_vals);
}
let rv = rv.assume_init();
assert_eq!(rv as *const (), data_ptr as *const ());
rv
}
fn round_to_words(len: usize) -> usize {
(len + mem::size_of::<usize>() - 1) / mem::size_of::<usize>()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::innerlude::{Element, Scope, VirtualDom};
#[test]
fn it_works() {
fn app(cx: Scope<()>) -> Element {
2022-02-20 22:37:46 +00:00
cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
}
let mut dom = VirtualDom::new(app);
dom.rebuild();
let g = dom.base_scope().root_node();
dbg!(g);
}
#[test]
fn it_drops() {
use std::rc::Rc;
struct AppProps {
inner: Rc<i32>,
}
fn app(cx: Scope<AppProps>) -> Element {
struct DropInner {
id: i32,
}
impl Drop for DropInner {
fn drop(&mut self) {
2022-03-02 22:54:11 +00:00
eprintln!("dropping inner");
}
}
let caller = {
let it = (0..10)
.map(|i| {
let val = cx.props.inner.clone();
2022-02-20 22:37:46 +00:00
LazyNodes::new(move |f| {
2022-03-02 22:54:11 +00:00
eprintln!("hell closure");
let inner = DropInner { id: i };
f.text(format_args!("hello world {:?}, {:?}", inner.id, val))
})
})
.collect::<Vec<_>>();
2022-02-20 22:37:46 +00:00
LazyNodes::new(|f| {
2022-03-02 22:54:11 +00:00
eprintln!("main closure");
f.fragment_from_iter(it)
})
};
cx.render(caller)
}
2021-11-07 06:49:53 +00:00
let inner = Rc::new(0);
let mut dom = VirtualDom::new_with_props(
app,
AppProps {
inner: inner.clone(),
},
);
dom.rebuild();
drop(dom);
assert_eq!(Rc::strong_count(&inner), 1);
}
2021-11-07 06:49:53 +00:00
}