Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guards! #47

Open
wants to merge 29 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d859e40
Formatting
Tamschi Feb 13, 2022
76300d5
Added drop-`Guard`s
Tamschi Feb 13, 2022
6781e8f
Updated CHANGELOG
Tamschi Feb 13, 2022
b26ada5
Changed `Guard` to store a `Node` by value, added examples to its doc…
Tamschi Feb 13, 2022
af3a95b
Example comment formatting
Tamschi Feb 13, 2022
ba169fa
Removed unnecessary import of `alloc::boxed::Box` from the second `Gu…
Tamschi Feb 13, 2022
e9809a7
Implemented `Deref` and `DerefMut` on `Guard`
Tamschi Feb 13, 2022
15b7012
Upgraded minimum Rust version to 1.55, and added a TODO to backport t…
Tamschi Feb 13, 2022
a079492
Merge branch 'develop' into vdom-guards
Tamschi Feb 14, 2022
3127529
Added an auto-safety implementation specifically for Guard
Tamschi Feb 14, 2022
083b7ed
Removed unnecessary lifetime parameter on `guard::auto_safe::AutoSafe`
Tamschi Feb 14, 2022
986c60a
Implemented `Send` and `Sync` on `Guard<ThreadSafe>`
Tamschi Feb 14, 2022
57cffd8
Added `guard::auto_safety::AutoSafe::into_auto_safe` for duck-typing
Tamschi Feb 14, 2022
aa2096d
Moved `AutoSafety` conversion into separate trait
Tamschi Feb 14, 2022
f6cd0c4
`AutoSafe` now implies `IntoAutoSafe`
Tamschi Feb 14, 2022
a961af5
Revert "`AutoSafe` now implies `IntoAutoSafe`"
Tamschi Feb 14, 2022
4bc3983
Fixed(?) `guard_AutoSafe_alias`
Tamschi Feb 14, 2022
1811b28
Fixed `guard_AutoSafe_alias`
Tamschi Feb 14, 2022
e782980
Revert "Revert "`AutoSafe` now implies `IntoAutoSafe`""
Tamschi Feb 14, 2022
1f8f9e2
Exposed `IntoAutoSafe::AutoSafe` through `AutoSafe`
Tamschi Feb 14, 2022
1c6eb0d
Revert "Removed unnecessary lifetime parameter on `guard::auto_safe::…
Tamschi Feb 15, 2022
d618b29
Hopefully fixed `guard_AutoSafe_alias`
Tamschi Feb 15, 2022
070a6a0
Constrained `AutoSafe<'a>::BoundOrActual: 'a`
Tamschi Feb 15, 2022
12f6b52
Removed left-over erroneous `'a`s
Tamschi Feb 15, 2022
8984210
Removed lifetime parameter again
Tamschi Feb 15, 2022
2479f66
Maybe this will work
Tamschi Feb 15, 2022
f4075c5
A different approach
Tamschi Feb 15, 2022
279bb1f
Fixed `guard_AutoSafe_alias`
Tamschi Feb 15, 2022
2f237cc
Looks like this needs to be a method after all
Tamschi Feb 15, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ If applicable, add screenshots to help explain your problem.

**please complete the following information:**

- `rustc --version`: [e.g. 1.54.0]
- `rustc --version`: [e.g. 1.55.0]
- Crate version (if applicable): [e.g. 0.1.0]

**Additional context**
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
strategy:
matrix:
os: [macos, ubuntu, windows]
rust: ['1.54', stable, beta, nightly]
rust: ['1.55', stable, beta, nightly]
env:
target: ${{matrix.target && format('--target={0}', matrix.target)}}
workspace: ${{matrix.no-workspace || '--workspace'}}
Expand Down
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@

<!-- markdownlint-disable no-trailing-punctuation -->

TODO: Backport feature level 1 to the 0.1 branch.

## next

TODO: Date

- **Breaking changes:**
- Increased minimum Rust version to 1.54,
> which comes with the project template update.
- Increased minimum Rust version to 1.55,
> in order to use `MaybeUninit::write`.
>
> A Rust version upgrade had also been incurred by the updated Rust-template.

- Features:
- Added `Guard` struct that acts as type-erased drop guard for shared `Node`s.

- Revisions:
- Adjusted CHANGELOG formatting.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Crates.io](https://img.shields.io/crates/v/lignin)](https://crates.io/crates/lignin)
[![Docs.rs](https://docs.rs/lignin/badge.svg)](https://docs.rs/lignin)

![Rust 1.54](https://img.shields.io/static/v1?logo=Rust&label=&message=1.54&color=grey)
![Rust 1.55](https://img.shields.io/static/v1?logo=Rust&label=&message=1.55&color=grey)
[![CI](https://github.com/Tamschi/lignin/workflows/CI/badge.svg?branch=develop)](https://github.com/Tamschi/lignin/actions?query=workflow%3ACI+branch%3Adevelop)
![Crates.io - License](https://img.shields.io/crates/l/lignin/0.1.0)

Expand Down
237 changes: 237 additions & 0 deletions src/guard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
//! A VDOM [`Drop`] guard for compatibility between caching components and containers in general.
//!
//! If a [`Node`] producer neither caches nor can act as container for other components which may, then it's fine to return a plain [`Node`] or [`&Node`](https://doc.rust-lang.org/stable/std/primitive.reference.html).

use crate::{Node, ThreadSafe, ThreadSafety};
use core::{
marker::PhantomData,
mem::MaybeUninit,
ops::{Deref, DerefMut},
};

pub mod auto_safety;

/// A type-erased callback that's consumed upon calling and doesn't need to be allocated inside a `Box<_>`.
///
/// > This type could technically implement [`Send`] and [`Sync`],
/// > but passing it to another thread separately is quite error-prone.
/// >
/// > *This theoretical thread-safety is not part of the public API.*
///
/// Use [`Guard`] instead if you require thread-safety, for example with [`Node::Comment{ comment: "PLACEHOLDER", dom_binding: None }`](`Node::Comment`) as placeholder.
///
/// > This should really be either a trait callable with `self: *const Self` or better yet
/// > a `Pin<Box<dyn Send + Sync + Guarded>, Pointing>` where [`Pointing: Allocator`](https://doc.rust-lang.org/stable/core/alloc/trait.Allocator.html)
/// > does absolution nothing. Both is unstable, though.
#[must_use = "Dropping a `ConsumeCallback` does not call it, potentially leaking memory."]
pub struct ConsumedCallback<'a> {
call: fn(*const ()),
with: *const (),
_phantom: PhantomData<&'a ()>,
}
impl<'a> ConsumedCallback<'a> {
/// Creates a new instance of [`ConsumedCallback`].
///
/// # Safety
///
/// `call` may be called up to once, **from any thread**, with `with`, but only within `'a`.
pub unsafe fn new(call: fn(*const ()), with: *const ()) -> Self {
Self {
call,
with,
_phantom: PhantomData,
}
}

/// Invokes the callback.
pub fn call(self) {
(self.call)(self.with)
}
}

/// A drop guard for a shared [`Node`].
///
/// # Implementation Contract
///
/// [`Guard`] consumers **may** delay dropping them arbitrarily, so [`Guard`] producers **should not** rely on that for correctness.
///
/// [`Guard`] consumers **should not** leak them, as [`Guard`] producers **may** leak memory in such a case.
///
/// > These are strong suggestions, since those "**may**"s are likely to be quite common.
/// >
/// > For example, a double-buffering differ running in a browser, as of writing e.g. [lignin-dom](https://docs.rs/lignin-dom/latest/lignin_dom/),
/// > will always delay dropping past the rendering of the updated VDOM.
/// >
/// > On a server, it may make sense to create an atomically- and periodically updated cache for part of the page,
/// > if it renders very slowly for some reason. (I.e. an app could render out a VDOM while calculating or retrieving data synchronously, even if it *probably shouldn't*.)
/// >
/// > In terms of leaks, a good example is subtree caching, which due to delayed [`Guard`] drops **must** store any number of states as necessary or panic if it won't/can't at some point.
///
/// # Examples
///
/// ```rust
/// extern crate alloc;
///
/// use lignin::{guard::ConsumedCallback, Guard, Node, ThreadSafe};
/// use alloc::boxed::Box;
///
/// /// This is quite inefficient; use a better allocator if possible.
/// fn boxed() -> Guard<'static, ThreadSafe> {
/// let raw = Box::into_raw(Box::new([Node::Text {
/// text: "Hello from the heap!",
/// dom_binding: None,
/// }]));
/// unsafe {
/// //SAFETY: `Guard::new` satisfies `ConsumedCallback::new`'s safety contract.
/// Guard::new(
/// Node::Multi(&*raw),
/// Some(ConsumedCallback::new(
/// |boxed| drop(Box::from_raw(boxed as *mut [Node<'static, ThreadSafe>; 1])),
/// raw as *const (),
/// )),
/// )
/// }
/// }
/// ```
///
/// ```rust
/// use lignin::{guard::ConsumedCallback, Guard, Node, ThreadSafety};
///
/// /// An efficient allocator that can reclaim instances leaked into it.
/// fn allocate<'a, T>() -> &'a mut core::mem::MaybeUninit<T> {
/// unimplemented!()
/// }
///
/// fn with_content<'a, S: ThreadSafety>(
/// c1: impl FnOnce() -> Guard<'a, S>,
/// c2: impl FnOnce() -> Guard<'a, S>,
/// ) -> Guard<'a, S> {
/// let mut callback;
/// let raw = allocate().write(unsafe {
/// //SAFETY:
/// // `callback` is rejoined with the peeled `Node`s below,
/// // as `Guard::new` satisfies `ConsumedCallback::leak`'s and
/// // `ConsumedCallback::peel`'s safety contracts.
/// [
/// {
/// let (node, callback_) = c1().leak();
/// callback = callback_;
/// node
/// },
/// c2().peel(&mut callback, allocate),
/// ]
/// }) as *mut _;
///
/// Guard::new(
/// Node::Multi(unsafe{ &*raw }),
/// callback,
/// )
/// }
/// ```
///
/// There are also `.map(…)` methods on [`Guard`] that are easier to use in certain circumstances.
pub struct Guard<'a, S: ThreadSafety> {
vdom: Node<'a, S>,
guarded: Option<ConsumedCallback<'a>>,
}
/// Sound due to [`ConsumedCallback::new`]'s safety contract.
unsafe impl Send for Guard<'_, ThreadSafe> {}
/// Sound due to [`ConsumedCallback::new`]'s safety contract.
unsafe impl Sync for Guard<'_, ThreadSafe> {}
impl<'a, S: ThreadSafety> Guard<'a, S> {
/// Creates a new instance of [`Guard`] which calls `guarded` once only when dropped.
#[must_use]
pub fn new(vdom: Node<'a, S>, guarded: Option<ConsumedCallback<'a>>) -> Self {
Self { vdom, guarded }
}

/// Splits this [`Guard`] into its [`Node`] and [optional](`Option`) [`ConsumedCallback`].
///
/// # Safety
///
/// The returned [`Node`] reference becomes invalid once the returned [`ConsumedCallback`] is called.
#[must_use = "Calling this method may leak memory unless any returned `ConsumedCallback` is called later on."]
pub unsafe fn leak(mut self) -> (Node<'a, S>, Option<ConsumedCallback<'a>>) {
(self.vdom, self.guarded.take())
}

/// Splits off and stores this [`Guard`]'s [`ConsumedCallback`], leaving a [`Node`].
///
/// # Safety
///
/// The returned [`Node`] becomes invalid once `add_to`'s value is called, if [`Some`] after this call.
pub unsafe fn peel(
mut self,
add_to: &mut Option<ConsumedCallback<'a>>,
allocate: impl FnOnce() -> &'a mut MaybeUninit<[ConsumedCallback<'a>; 2]>,
) -> Node<'a, S> {
if let Some(peel) = self.guarded.take() {
*add_to = Some(match add_to.take() {
Some(previous) => {
fn call_both(both: *const ()) {
let [first, second] =
unsafe { both.cast::<[ConsumedCallback<'static>; 2]>().read() };
first.call();
second.call();
}
let both = (allocate().write([previous, peel])
as *const [ConsumedCallback<'a>; 2])
.cast();
ConsumedCallback::new(call_both, both)
}
None => peel,
});
}
self.vdom
}

/// Transforms the guarded [`Node`] without manipulating the callback.
pub fn map<S2: ThreadSafety>(
mut self,
f: impl for<'any> FnOnce(Node<'any, S>) -> Node<'any, S2>,
) -> Guard<'a, S2> {
Guard {
vdom: f(self.vdom),
guarded: self.guarded.take(),
}
}

/// Transforms the guarded [`Node`], optionally adding on another callback.
pub fn flat_map<S2: ThreadSafety>(
mut self,
allocate: impl FnOnce() -> &'a mut MaybeUninit<[ConsumedCallback<'a>; 2]>,
f: impl for<'any> FnOnce(Node<'any, S>) -> Guard<'any, S2>,
) -> Guard<'a, S2> {
unsafe {
//SAFETY:
// `self.vdom` can't escape from `f` due to its `'any` lifetime there.
// The peeled callback is immediately recombined.
Guard {
vdom: f(self.vdom).peel(&mut self.guarded, allocate),
guarded: self.guarded.take(),
}
}
}
}

impl<'a, S: ThreadSafety> Deref for Guard<'a, S> {
type Target = Node<'a, S>;

fn deref(&self) -> &Self::Target {
&self.vdom
}
}

impl<S: ThreadSafety> DerefMut for Guard<'_, S> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.vdom
}
}

impl<S: ThreadSafety> Drop for Guard<'_, S> {
fn drop(&mut self) {
if let Some(guarded) = self.guarded.take() {
guarded.call()
}
}
}