diff --git a/packages/engine/Cargo.lock b/packages/engine/Cargo.lock index 200d1d2c70b..da96f9ba6b7 100644 --- a/packages/engine/Cargo.lock +++ b/packages/engine/Cargo.lock @@ -1614,6 +1614,10 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "provider" +version = "0.0.0" + [[package]] name = "quick-error" version = "1.2.3" diff --git a/packages/engine/lib/provider/Cargo.toml b/packages/engine/lib/provider/Cargo.toml new file mode 100644 index 00000000000..137675f5b85 --- /dev/null +++ b/packages/engine/lib/provider/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "provider" +version = "0.0.0" +edition = "2021" +description = "`Provider` trait and accompanying API" +publish = false diff --git a/packages/engine/lib/provider/src/internal.rs b/packages/engine/lib/provider/src/internal.rs new file mode 100644 index 00000000000..be3fc14988a --- /dev/null +++ b/packages/engine/lib/provider/src/internal.rs @@ -0,0 +1,65 @@ +//! Internal API + +use super::{TypeId, TypeTag}; + +/// Sealed trait representing a type-erased tagged object. +/// +/// # Safety +/// +/// This trait must be exclusively implemented by [`TagValue`] as [`Tagged::is`] relies on `tag_id` +/// and the [`TypeId`] must not be overwritten. +/// +/// [`Tagged::is`]: #method.is +pub(super) unsafe trait Tagged<'p>: 'p { + /// The [`TypeId`] of the [`TypeTag`] this value was tagged with. + fn tag_id(&self) -> TypeId; +} + +/// A concrete tagged value for a given tag `I`. +/// +/// This is the only type which implements the [`Tagged`] trait, and encodes additional information +/// about the specific [`TypeTag`] into the type. This allows for multiple different tags to support +/// overlapping value ranges, for example, both the [`Ref`] and [`Value<&'static str>`] tags +/// can be used to tag a value of type [`&'static str`]. +/// +/// [`Ref`]: crate::tags::Ref +/// [`Value<&'static str>`]: crate::tags::Value +/// [`&'static str`]: str +#[repr(transparent)] +pub(super) struct TagValue<'p, I: TypeTag<'p>>(pub(super) I::Type); + +unsafe impl<'p, I> Tagged<'p> for TagValue<'p, I> +where + I: TypeTag<'p>, +{ + fn tag_id(&self) -> TypeId { + TypeId::of::() + } +} + +impl<'p> dyn Tagged<'p> { + /// Returns `true` if the dynamic type is tagged with `I`. + #[inline] + pub(super) fn is(&self) -> bool + where + I: TypeTag<'p>, + { + self.tag_id() == TypeId::of::() + } + + /// Returns some reference to the dynamic value if it is tagged with `I`, or [`None`] if it + /// isn't. + #[inline] + pub(super) fn downcast_mut(&mut self) -> Option<&mut TagValue<'p, I>> + where + I: TypeTag<'p>, + { + if self.is::() { + let tag_value = (self as *mut Self).cast::>(); + // SAFETY: Just checked whether we're pointing to a `TagValue<'p, I>` + unsafe { Some(&mut *tag_value) } + } else { + None + } + } +} diff --git a/packages/engine/lib/provider/src/lib.rs b/packages/engine/lib/provider/src/lib.rs new file mode 100644 index 00000000000..488eafccffa --- /dev/null +++ b/packages/engine/lib/provider/src/lib.rs @@ -0,0 +1,253 @@ +//! Contains the [`Provider`] trait and accompanying API, which enable trait objects to provide data +//! based on typed requests, an alternate form of runtime reflection. +//! +//! [`Provider`] and the associated APIs support generic, type-driven access to data, and a +//! mechanism for implementers to provide such data. The key parts of the interface are the +//! [`Provider`] trait for objects which can provide data, and the [`request_by_type_tag`] function +//! for data from an object which implements [`Provider`]. Note that end users should not call +//! requesting [`request_by_type_tag`] directly, it is a helper function for intermediate +//! implementers to use to implement a user-facing interface. +//! +//! Typically, a data provider is a trait object of a trait which extends [`Provider`]. A user will +//! request data from the trait object by specifying the type or a type tag (a type tag is a type +//! used only as a type parameter to identify the type which the user wants to receive). +//! +//! ## Data flow +//! +//! * A user requests an object, which is delegated to [`request_by_type_tag`] +//! * [`request_by_type_tag`] creates a [`Requisition`] object and passes it to +//! [`Provider::provide`] +//! * The object provider's implementation of `Provider::provide` tries providing values of +//! different types using `Requisition::provide_*`. If the type tag matches the type requested by +//! the user, it will be stored in the [`Requisition`] object. +//! * [`request_by_type_tag`] unpacks the [`Requisition`] object and returns any stored value to the +//! user. +//! +//! # Examples +// Taken from https://github.com/rust-lang/rfcs/pull/3192 +//! +//! To provide data for example on an error type, the [`Provider`] API enables: +//! +//! ```rust +//! # #![feature(backtrace)] +//! use std::backtrace::Backtrace; +//! +//! use provider::{tags, Provider, Requisition, TypeTag}; +//! +//! struct MyError { +//! backtrace: Backtrace, +//! suggestion: String, +//! } +//! +//! impl Provider for MyError { +//! fn provide<'p>(&'p self, mut req: Requisition<'p, '_>) { +//! req.provide_ref(&self.backtrace) +//! .provide_ref(self.suggestion.as_str()); +//! } +//! } +//! +//! trait MyErrorTrait: Provider {} +//! +//! impl MyErrorTrait for MyError {} +//! +//! impl dyn MyErrorTrait { +//! fn request_ref(&self) -> Option<&T> { +//! provider::request_by_type_tag::<'_, tags::Ref, _>(self) +//! } +//! } +//! ``` +//! +//! In another module or crate, this can be requested for any `dyn MyErrorTrait`, not just +//! `MyError`: +//! ```rust +//! # #![feature(backtrace)] +//! # use std::backtrace::Backtrace; +//! # use provider::{Provider, Requisition, TypeTag, tags}; +//! # struct MyError { backtrace: Backtrace, suggestion: String } +//! # impl Provider for MyError { +//! # fn provide<'p>(&'p self, mut req: Requisition<'p, '_>) { +//! # req.provide_ref(&self.backtrace) +//! # .provide_ref(self.suggestion.as_str()); +//! # } +//! # } +//! # trait MyErrorTrait: Provider {} +//! # impl MyErrorTrait for MyError {} +//! # impl dyn MyErrorTrait { +//! # fn request_ref(&self) -> Option<&T> { +//! # provider::request_by_type_tag::<'_, tags::Ref, _>(self) +//! # } +//! # } +//! fn report_error(e: &(dyn MyErrorTrait + 'static)) { +//! // Generic error handling +//! // ... +//! +//! // print backtrace +//! if let Some(backtrace) = e.request_ref::() { +//! println!("{backtrace:?}") +//! } +//! # assert!(e.request_ref::().is_some()); +//! +//! // print suggestion text +//! if let Some(suggestions) = e.request_ref::() { +//! println!("Suggestion: {suggestions}") +//! } +//! # assert_eq!(e.request_ref::().unwrap(), "Do it correctly next time!"); +//! } +//! +//! fn main() { +//! let error = MyError { +//! backtrace: Backtrace::capture(), +//! suggestion: "Do it correctly next time!".to_string(), +//! }; +//! +//! report_error(&error); +//! } +//! ``` + +// Heavily inspired by https://github.com/rust-lang/project-error-handling/issues/3: +// The project-error-handling tries to improves the error trait. In order to move the trait into +// `core`, an alternative solution to backtrace provisioning had to be found. This is, where the +// provider API comes from. +// +// TODO: replace library with https://github.com/rust-lang/project-error-handling/issues/3. + +#![warn(clippy::pedantic, clippy::nursery)] + +pub mod tags; + +mod internal; +mod requisition; + +use core::any::TypeId; + +use self::internal::{TagValue, Tagged}; +use crate::requisition::{ConcreteRequisition, RequisitionImpl}; + +/// Trait implemented by a type which can dynamically provide tagged values. +pub trait Provider { + /// Object providers should implement this method to provide *all* values they are able to + /// provide using `req`. + fn provide<'p>(&'p self, req: Requisition<'p, '_>); +} + +/// Request a specific value by a given tag from the `Provider`. +pub fn request_by_type_tag<'p, I, P: Provider + ?Sized>(provider: &'p P) -> Option +where + I: TypeTag<'p>, +{ + let mut req: ConcreteRequisition<'p, I> = RequisitionImpl { + tagged: TagValue(None), + }; + provider.provide(Requisition(&mut req)); + req.tagged.0 +} + +/// This trait is implemented by specific `TypeTag` types in order to allow describing a type which +/// can be requested for a given lifetime `'p`. +/// +/// A few example implementations for type-driven `TypeTag`s can be found in the [`tags`] module, +/// although crates may also implement their own tags for more complex types with internal +/// lifetimes. +pub trait TypeTag<'p>: Sized + 'static { + /// The type of values which may be tagged by this `TypeTag` for the given lifetime. + type Type: 'p; +} + +/// A helper object for providing objects by type. +/// +/// An object provider provides values by calling this type's provide methods. Note, that +/// `Requisition` is a wrapper around a mutable reference to a [`TypeTag`]ged value. +pub struct Requisition<'p, 'r>(&'r mut RequisitionImpl + 'p>); + +#[cfg(test)] +pub(crate) mod tests { + use crate::{tags, Provider, Requisition, TypeTag}; + + struct CustomTagA; + impl<'p> TypeTag<'p> for CustomTagA { + type Type = usize; + } + + struct CustomTagB; + impl<'p> TypeTag<'p> for CustomTagB { + type Type = usize; + } + + pub(crate) struct MyError { + value: usize, + reference: usize, + custom_tag_a: usize, + custom_tag_b: usize, + option: Option, + result_ok: Result, + result_err: Result, + } + + impl Provider for MyError { + fn provide<'p>(&'p self, mut req: Requisition<'p, '_>) { + req.provide_value(|| self.value) + .provide_ref(&self.reference) + .provide_with::(|| self.custom_tag_a) + .provide::(self.custom_tag_b) + .provide::>>(self.option) + .provide::, tags::Value>>(self.result_ok) + .provide::, tags::Value>>(self.result_err); + } + } + + pub(crate) const ERR: MyError = MyError { + value: 1, + reference: 2, + custom_tag_a: 3, + custom_tag_b: 4, + option: Some(5), + result_ok: Ok(6), + result_err: Err(7), + }; + + #[test] + fn provide_value() { + assert_eq!( + crate::request_by_type_tag::, _>(&ERR), + Some(1) + ); + } + + #[test] + fn provide_ref() { + assert_eq!( + crate::request_by_type_tag::, _>(&ERR), + Some(&2) + ); + } + + #[test] + fn provide_with() { + assert_eq!(crate::request_by_type_tag::(&ERR), Some(3)); + } + + #[test] + fn provide() { + assert_eq!(crate::request_by_type_tag::(&ERR), Some(4)); + } + + #[test] + fn tags() { + assert_eq!( + crate::request_by_type_tag::>, _>(&ERR), + Some(Some(5)) + ); + assert_eq!( + crate::request_by_type_tag::, tags::Value>, _>( + &ERR + ), + Some(Ok(6)) + ); + assert_eq!( + crate::request_by_type_tag::, tags::Value>, _>( + &ERR + ), + Some(Err(7)) + ); + } +} diff --git a/packages/engine/lib/provider/src/requisition.rs b/packages/engine/lib/provider/src/requisition.rs new file mode 100644 index 00000000000..11928b891b2 --- /dev/null +++ b/packages/engine/lib/provider/src/requisition.rs @@ -0,0 +1,59 @@ +//! Internal API + +use crate::{tags, Requisition, TagValue, TypeTag}; + +impl<'p> Requisition<'p, '_> { + /// Provide a value with the given [`TypeTag`]. + pub fn provide(&mut self, value: I::Type) -> &mut Self + where + I: TypeTag<'p>, + { + if let Some(res @ TagValue(Option::None)) = + self.0.tagged.downcast_mut::>() + { + res.0 = Some(value); + } + self + } + + /// Provide a value or other type with only static lifetimes. + pub fn provide_value(&mut self, f: F) -> &mut Self + where + T: 'static, + F: FnOnce() -> T, + { + self.provide_with::, F>(f) + } + + /// Provide a reference, note that `T` must be bounded by `'static`, but may be unsized. + pub fn provide_ref(&mut self, value: &'p T) -> &mut Self { + self.provide::>(value) + } + + /// Provide a value with the given [`TypeTag`], using a closure to prevent unnecessary work. + pub fn provide_with(&mut self, f: F) -> &mut Self + where + I: TypeTag<'p>, + F: FnOnce() -> I::Type, + { + if let Some(res @ TagValue(Option::None)) = + self.0.tagged.downcast_mut::>() + { + res.0 = Some(f()); + } + self + } +} + +/// A concrete request for a tagged value. Can be coerced to [`Requisition`] to be passed to +/// provider methods. +pub(super) type ConcreteRequisition<'p, I> = RequisitionImpl>>; + +/// Implementation detail shared between [`Requisition`] and [`ConcreteRequisition`]. +/// +/// Generally this value is used through the [`Requisition`] type as an `&mut Requisition<'p>` out +/// parameter, or constructed with the `ConcreteRequisition<'p, I>` type alias. +#[repr(transparent)] +pub(super) struct RequisitionImpl { + pub(super) tagged: T, +} diff --git a/packages/engine/lib/provider/src/tags.rs b/packages/engine/lib/provider/src/tags.rs new file mode 100644 index 00000000000..cb27f74021d --- /dev/null +++ b/packages/engine/lib/provider/src/tags.rs @@ -0,0 +1,44 @@ +//! Type tags are used to identify a type using a separate value. This module includes type tags for +//! some very common types. +//! +//! Many users of the provider APIs will not need to use type tags at all. But if you want to use +//! them with more complex types (typically those including lifetime parameters), you will +//! need to write your own tags. + +use core::marker::PhantomData; + +use crate::TypeTag; + +/// Type-based [`TypeTag`] for `&'p T` types. +#[derive(Debug)] +pub struct Ref(PhantomData); + +impl<'p, T: ?Sized + 'static> TypeTag<'p> for Ref { + type Type = &'p T; +} + +/// Type-based [`TypeTag`] for static `T` types. +#[derive(Debug)] +pub struct Value(PhantomData); + +impl<'p, T: 'static> TypeTag<'p> for Value { + type Type = T; +} + +/// Tag combinator to wrap the given tag's value in an [`Option`][Option] +#[derive(Debug)] +pub struct OptionTag(PhantomData); + +impl<'p, I: TypeTag<'p>> TypeTag<'p> for OptionTag { + type Type = Option; +} + +/// Tag combinator to wrap the given tag's value in an [`Result`][Result] +#[derive(Debug)] +pub struct ResultTag(PhantomData, PhantomData); + +impl<'p, I: TypeTag<'p>, E: TypeTag<'p>> TypeTag<'p> for ResultTag { + type Type = Result; +} + +// Tested in `crate::tests`