Module lignin::auto_safety [−][src]
Expand description
Transitive (across function boundaries) ThreadSafety
inference, mainly for use by frameworks.
All methods in this module are always-inlined no-ops, meaning that there is zero runtime cost to them.
The following is a long explanation that you probably don’t have to read.
In hand-written code, you can always use From
or Into
to cast a …<ThreadSafe>
type to the matching …<ThreadBound>
type where necessary.
If you receive an opaque type, use lignin::auto_safety::{AutoSafe as _, Deanonymize as _};
and call .deanonymize()
on it, then politely ask the author to consider being more specific.
If you do intend to use this module, please still declare ThreadSafe
explicitly at crate boundaries, or encourage developers using your library to do so.
You can find more information on this near the end of this page.
This feature relies on opaque return types (
-> impl Trait
) leakingSend
andSync
, so the theoretical limit here, even after specialization lands, are four distinct ‘real’ types with restrictions on conversion incompatibilities. Fortunately,lignin
only needs two of these slots with straightforward compatibility, the!Send + !Sync
and theSend + Sync
one.Please refer to the item documentation for implementation details.
Examples / Usage
All examples share the following definitions:
use lignin::{ auto_safety::{Align as _, AutoSafe, Deanonymize as _}, // <-- Important! Node, ThreadBound, ThreadSafe, }; fn safe<'a>() -> Node::<'a, ThreadSafe> { Node::Multi(&[]) } fn bound<'a>() -> Node::<'a, ThreadBound> { Node::Multi(&[]) } fn inferred_safe<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { safe() } fn inferred_bound<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { bound() } fn allocate<'a, T>(value: T) -> &'a T { // … }I recommend using
bumpalo
as VDOM allocator since it is fast and versatile, butlignin
itself has no preference in this regard.
In all examples and the above, except for those in the More lenient conversions with
From
andInto
section below,Node
can be replaced by any otherVdom
type.
Basic Forwarding
To mark the ThreadSafety
of a function as inferred, return AutoSafe
wrapping the ThreadBound
version of the VDOM node you want to return.
This works with manually-defined sources…:
fn safe_1<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { safe() } fn bound_1<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { bound() }
…as well as ones where the original return type is inferred (opaque):
fn safe_2<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { inferred_safe() } fn bound_2<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { inferred_bound() }
Deanonymization
Rust doesn’t allow consumption of the inferred concrete return type of a function directly, so while the following works fine…:
fn safe_1<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { Node::Multi(allocate([safe()])) } fn bound_1<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { Node::Multi(allocate([bound()])) }
…each of these fails to compile:
fn safe_2<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { Node::Ref(allocate(inferred_safe())) // ^^^^^^^^^^^^^^^ expected enum `Node`, found opaque type }
fn bound_2<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { Node::Ref(allocate(inferred_bound())) // ^^^^^^^^^^^^^^^^ expected enum `Node`, found opaque type }
.deanonymize()
Call .deanonymize()
without qualification on an opaquely-typed value to cast it to the underlying named type.
This method resolves either through AutoSafe
or Deanonymize
, so it’s important for both traits to be in scope at the call site!
fn safe_2<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { Node::Multi(allocate([inferred_safe().deanonymize()])) } fn bound_2<'a>() -> impl AutoSafe<Node::<'a, ThreadBound>> { Node::Multi(allocate([inferred_bound().deanonymize()])) }
You also have to do this to annotate the type of local variables…:
let safe_node: Node<_> = inferred_safe().deanonymize(); let bound_node: Node<_> = inferred_bound().deanonymize();…or to specify a
ThreadSafety
in the return type:fn strictly_safe<'a>() -> Node::<'a, ThreadSafe> { inferred_safe().deanonymize() } fn strictly_bound<'a>() -> Node::<'a, ThreadBound> { inferred_bound().deanonymize() }
Identity Cast
Calling .deanonymize()
on named types is valid but ultimately useless, so it produces a warning if resolved like that:
let safe_node: Node<ThreadSafe> = safe().deanonymize(); // ^^^^^^^^^^^ let bound_node: Node<ThreadBound> = bound().deanonymize(); // ^^^^^^^^^^^ // // warning: // use of deprecated associated function `lignin::auto_safety::<impl lignin::Node<'a, S>>::deanonymize`: // Call of `.deanonymize()` on named type.
Macros should suppress this warning as specifically as possible:
{ let rendered = // … #[deny(warnings)] { // Use `$crate` if possible, and ideally don't leak these imports into caller code. use ::lignin::auto_safety::{AutoSafe as _, Deanonymize as _}; #[allow(deprecated)] rendered.deanonymize() } }
No Coercion
Calls to .deanonymize()
can’t be coerced, so each of the following fails to compile:
let safe_node: Node::<ThreadSafe> = inferred_bound().deanonymize(); // ------------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // | expected struct `ThreadSafe`, found struct `ThreadBound` // expected due to this // // note: expected enum `Node<'_, ThreadSafe>` // found enum `Node<'_, ThreadBound>`
let bound_node: Node::<ThreadBound> = inferred_safe().deanonymize(); // ------------------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // | expected struct `ThreadBound`, found struct `ThreadSafe` // expected due to this // // note: expected enum `Node<'_, ThreadBound>` // found enum `Node<'_, ThreadSafe>`
Alignment
The Align
trait behaves a lot like Into
, so identity conversions are possible.
However, unlike Into
, it’s implemented so that it can change only a type’s ThreadSafety
and isn’t warned about by Clippy on identity conversion.
This makes it ideal to combine Node
instances with different or unknown ThreadSafety
into a single VDOM:
let safe_to_bound = Node::Multi(allocate([ safe().align(), bound(), ])); let safe_to_inferred = Node::Multi(allocate([ safe().align(), inferred_safe().deanonymize(), ])); let inferred_to_bound = Node::Multi(allocate([ bound(), inferred_safe().deanonymize().align(), ]));
More lenient conversions with From
and Into
From
and Into
can both be used to change the ThreadSafety
of Vdom
values from ThreadSafe
to ThreadBound
:
let safe_to_bound: Node<ThreadBound> = safe().into();
Direct Node
conversions, which can also adjust ThreadSafety
, are additionally available for references to
[Node]
(into Node::Multi
) and str
(into Node::Text
):
let empty: &[Node<ThreadSafe>] = &[]; let empty_node: Node<ThreadSafe> = empty.into(); let text_node: Node<ThreadSafe> = "Hello VDOM!".into();
ThreadSafety
alignment is possible at the same time, but this also means relevant annotations or at least nudges (see below) are often necessary.
ThreadSafe
Preference
The Rust compiler can usually infer the correct ThreadSafety
without annotations if valid choices are in any way limited in this regard.
However, this isn’t the case for most Vdom
expressions without inputs with definite ThreadSafety
…:
let attempt_1 = Node::Multi(&[]); // --------- ^^^^^^^^^^^ // See below.
…or if all inputs are thread-safe and .align()
is called on each of them:
let attempt_2 = Node::Multi(allocate([safe().align(), inferred_safe().deanonymize().align()])); // --------- ^^^^^^^^^^^ cannot infer type for type parameter `S` declared on the enum `Node` // consider giving `attempt_2` the explicit type `Node<'_, S>`, where the type parameter `S` is specified // // note: cannot satisfy `_: ThreadSafety` // note: required by `Multi`
In these cases, you can call .prefer_thread_safe()
on the indeterminate expression to nudge the compiler in the right direction.
let safe_1 = Node::Multi(&[]).prefer_thread_safe(); let safe_2 = Node::Multi(allocate([ safe().align(), inferred_safe().deanonymize().align(), ])).prefer_thread_safe();
This is implemented directly on the individual
Vdom
type variants, so no additional trait imports are necessary to use it.
Limiting AutoSafe
Exposure
Thread-safety inference is powerful, but also dangerous: A change deep in a library could cause a public function return type to shift, breaking compatibility with downstream crates.
For this reason, and because of its worse ergonomics, -> impl AutoSafe<…>
should not be exposed in a crate’s public API.
A front-end template language or framework author may still want to avoid requiring explicit threading annotations in most cases. Even in that case, it’s possible to limit this feature to functions not externally visible, by aliasing it with a generated less visible trait:
use lignin::{ auto_safety::AutoSafe_alias, Node, ThreadBound, }; AutoSafe_alias!(pub(crate) InternalAutoSafe); //------------------------------------------- //`InternalAutoSafe<Node<'static, ThreadBound>>` declared as private pub fn public() -> impl InternalAutoSafe<Node<'static, ThreadBound>> { //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //can't leak private trait Node::Multi(&[]).prefer_thread_safe() }
// Same imports. AutoSafe_alias!(pub(crate) InternalAutoSafe); pub(crate) fn less_visible() -> impl InternalAutoSafe<Node<'static, ThreadBound>> { Node::Multi(&[]).prefer_thread_safe() }
As the generated trait is a subtrait of AutoSafe
, its instances can be treated the same as that trait’s,
as long as AutoSafe
and Deanonymize
are in scope.
Macros
AutoSafe_alias | Mainly for use by frameworks. Canonically located at |
Traits
Align | Contextually thread-binds an instance, or not. Use only without qualification. |
AutoSafe | Deanonymize towards the general ( |
Deanonymize | Deanonymize towards the special ( |