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) leaking Send and Sync, 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 the Send + 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, but lignin itself has no preference in this regard.

In all examples and the above, except for those in the More lenient conversions with From and Into section below, Node can be replaced by any other Vdom 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 auto_safe::AutoSafe_alias.
Creates a custom-visibility alias for auto_safety::AutoSafe.

Traits

Align

Contextually thread-binds an instance, or not. Use only without qualification.

AutoSafe

Deanonymize towards the general (ThreadBound) case. Used as -> impl AutoSafe<…>.

Deanonymize

Deanonymize towards the special (ThreadSafe) case. This trait must be in scope for correct inference!