Skip to main content

typst_library/foundations/content/
vtable.rs

1//! A custom [vtable] implementation for content.
2//!
3//! This is similar to what is generated by the Rust compiler under the hood
4//! when using trait objects. However, ours has two key advantages:
5//!
6//! - It can store a _slice_ of sub-vtables for field-specific operations.
7//! - It can store not only methods, but also plain data, allowing us to access
8//!   that data without going through dynamic dispatch.
9//!
10//! Because our vtable pointers are backed by `static` variables, we can also
11//! perform checks for element types by comparing raw vtable pointers giving us
12//! `RawContent::is` without dynamic dispatch.
13//!
14//! Overall, the custom vtable gives us just a little more flexibility and
15//! optimizability than using built-in trait objects.
16//!
17//! Note that all vtable methods receive elements of type `Packed<E>`, but some
18//! only perform actions on the `E` itself, with the shared part kept outside of
19//! the vtable (e.g. `hash`), while some perform the full action (e.g. `clone`
20//! as it needs to return new, fully populated raw content). Which one it is, is
21//! documented for each.
22//!
23//! # Safety
24//! This module contains a lot of `unsafe` keywords, but almost all of it is the
25//! same and quite straightforward. All function pointers that operate on a
26//! specific element type are marked as unsafe. In combination with `repr(C)`,
27//! this grants us the ability to safely transmute a `ContentVtable<Packed<E>>`
28//! into a `ContentVtable<RawContent>` (or just short `ContentVtable`). Callers
29//! of functions marked as unsafe have to guarantee that the `ContentVtable` was
30//! transmuted from the same `E` as the RawContent was constructed from. The
31//! `Handle` struct provides a safe access layer, moving the guarantee that the
32//! vtable is matching into a single spot.
33//!
34//! [vtable]: https://en.wikipedia.org/wiki/Virtual_method_table
35
36use std::any::TypeId;
37use std::fmt::{self, Debug, Formatter};
38use std::ops::Deref;
39use std::ptr::NonNull;
40
41use ecow::EcoString;
42use typst_utils::DefSite;
43
44use super::raw::RawContent;
45use crate::diag::SourceResult;
46use crate::engine::Engine;
47use crate::foundations::{
48    Args, CastInfo, Construct, Content, LazyElementStore, NativeElement, NativeScope,
49    Packed, Repr, Scope, Set, StyleChain, Styles, Value,
50};
51use crate::text::{Lang, LocalName, Region};
52
53/// Encapsulates content and a vtable, granting safe access to vtable operations.
54pub(super) struct Handle<T, V: 'static>(T, &'static V);
55
56impl<T, V> Handle<T, V> {
57    /// Produces a new handle from content and a vtable.
58    ///
59    /// # Safety
60    /// The content and vtable must be matching, i.e. `vtable` must be derived
61    /// from the content's vtable.
62    pub(super) unsafe fn new(content: T, vtable: &'static V) -> Self {
63        Self(content, vtable)
64    }
65}
66
67impl<T, V> Deref for Handle<T, V> {
68    type Target = V;
69
70    fn deref(&self) -> &Self::Target {
71        self.1
72    }
73}
74
75pub(super) type ContentHandle<T> = Handle<T, ContentVtable>;
76pub(super) type FieldHandle<T> = Handle<T, FieldVtable>;
77
78/// A vtable for performing element-specific actions on type-erased content.
79/// Also contains general metadata for the specific element.
80#[repr(C)]
81pub struct ContentVtable<T: 'static = RawContent> {
82    /// The element's normal name, as in code.
83    pub(super) name: &'static str,
84    /// The element's title-cased name.
85    pub(super) title: &'static str,
86    /// The element's documentation (as Markdown).
87    pub(super) docs: &'static str,
88    /// Whether the element is defined in the source code.
89    pub(super) def_site: DefSite,
90    /// Search keywords for the documentation.
91    pub(super) keywords: &'static [&'static str],
92
93    /// Subvtables for all fields of the element.
94    pub(super) fields: &'static [FieldVtable<T>],
95    /// Determines the ID for a field name. This is a separate function instead
96    /// of searching through `fields` so that Rust can generate optimized code
97    /// for the string matching.
98    pub(super) field_id: fn(name: &str) -> Option<u8>,
99
100    /// The constructor of the element.
101    pub(super) construct: fn(&mut Engine, &mut Args) -> SourceResult<Content>,
102    /// The set rule of the element.
103    pub(super) set: fn(&mut Engine, &mut Args) -> SourceResult<Styles>,
104    /// The element's local name in a specific lang-region pairing.
105    pub(super) local_name: Option<fn(Lang, Option<Region>) -> &'static str>,
106    /// Produces the associated [`Scope`] of the element.
107    pub(super) scope: fn() -> Scope,
108    /// If the `capability` function returns `Some(p)`, then `p` must be a valid
109    /// pointer to a native Rust vtable of `Packed<Self>` w.r.t to the trait `C`
110    /// where `capability` is `TypeId::of::<dyn C>()`.
111    pub(super) capability: fn(capability: TypeId) -> Option<NonNull<()>>,
112
113    /// The `Drop` impl (for the whole raw content). The content must have a
114    /// reference count of zero and may not be used anymore after `drop` was
115    /// called.
116    pub(super) drop: unsafe fn(&mut RawContent),
117    /// The `Clone` impl (for the whole raw content).
118    pub(super) clone: unsafe fn(&T) -> RawContent,
119    /// The `Hash` impl (for just the element).
120    pub(super) hash: unsafe fn(&T) -> u128,
121    /// The `Debug` impl (for just the element).
122    pub(super) debug: unsafe fn(&T, &mut Formatter) -> fmt::Result,
123    /// The `PartialEq` impl (for just the element). If this is `None`,
124    /// field-wise equality checks (via `FieldVtable`) should be performed.
125    pub(super) eq: Option<unsafe fn(&T, &T) -> bool>,
126    /// The `Repr` impl (for just the element). If this is `None`, a generic
127    /// name + fields representation should be produced.
128    pub(super) repr: Option<unsafe fn(&T) -> EcoString>,
129
130    /// Produces a reference to a `static` variable holding a `LazyElementStore`
131    /// that is unique for this element and can be populated with data that is
132    /// somewhat costly to initialize at runtime and shouldn't be initialized
133    /// over and over again. Must be a function rather than a direct reference
134    /// so that we can store the vtable in a `const` without Rust complaining
135    /// about the presence of interior mutability.
136    pub(super) store: fn() -> &'static LazyElementStore,
137}
138
139impl ContentVtable {
140    /// Creates the vtable for an element.
141    #[allow(clippy::too_many_arguments)]
142    pub const fn new<E: NativeElement>(
143        name: &'static str,
144        title: &'static str,
145        docs: &'static str,
146        def_site: DefSite,
147        fields: &'static [FieldVtable<Packed<E>>],
148        field_id: fn(name: &str) -> Option<u8>,
149        capability: fn(TypeId) -> Option<NonNull<()>>,
150        store: fn() -> &'static LazyElementStore,
151    ) -> ContentVtable<Packed<E>> {
152        ContentVtable {
153            name,
154            title,
155            docs,
156            def_site,
157            keywords: &[],
158            fields,
159            field_id,
160            construct: <E as Construct>::construct,
161            set: <E as Set>::set,
162            local_name: None,
163            scope: || Scope::new(),
164            capability,
165            drop: RawContent::drop_impl::<E>,
166            clone: RawContent::clone_impl::<E>,
167            hash: |elem| typst_utils::hash128(elem.as_ref()),
168            debug: |elem, f| Debug::fmt(elem.as_ref(), f),
169            eq: None,
170            repr: None,
171            store,
172        }
173    }
174
175    /// Retrieves the vtable of the element with the given ID.
176    pub fn field(&self, id: u8) -> Option<&'static FieldVtable> {
177        self.fields.get(usize::from(id))
178    }
179}
180
181impl<E: NativeElement> ContentVtable<Packed<E>> {
182    /// Attaches search keywords for the documentation.
183    pub const fn with_keywords(mut self, keywords: &'static [&'static str]) -> Self {
184        self.keywords = keywords;
185        self
186    }
187
188    /// Takes a [`Repr`] impl into account.
189    pub const fn with_repr(mut self) -> Self
190    where
191        E: Repr,
192    {
193        self.repr = Some(|e| E::repr(&**e));
194        self
195    }
196
197    /// Takes a [`PartialEq`] impl into account.
198    pub const fn with_partial_eq(mut self) -> Self
199    where
200        E: PartialEq,
201    {
202        self.eq = Some(|a, b| E::eq(&**a, &**b));
203        self
204    }
205
206    /// Takes a [`LocalName`] impl into account.
207    pub const fn with_local_name(mut self) -> Self
208    where
209        Packed<E>: LocalName,
210    {
211        self.local_name = Some(<Packed<E> as LocalName>::local_name);
212        self
213    }
214
215    /// Takes a [`NativeScope`] impl into account.
216    pub const fn with_scope(mut self) -> Self
217    where
218        E: NativeScope,
219    {
220        self.scope = || E::scope();
221        self
222    }
223
224    /// Type-erases the data.
225    pub const fn erase(self) -> ContentVtable {
226        // Safety:
227        // - `ContentVtable` is `repr(C)`.
228        // - `ContentVtable` does not hold any `E`-specific data except for
229        //   function pointers.
230        // - All functions pointers have the same memory layout.
231        // - All functions containing `E` are marked as unsafe and callers need
232        //   to uphold the guarantee that they only call them with raw content
233        //   that is of type `E`.
234        // - `Packed<E>` and `RawContent` have the exact same memory layout
235        //   because of `repr(transparent)`.
236        unsafe {
237            std::mem::transmute::<ContentVtable<Packed<E>>, ContentVtable<RawContent>>(
238                self,
239            )
240        }
241    }
242}
243
244impl<T> ContentHandle<T> {
245    /// Provides safe access to operations for the field with the given `id`.
246    pub(super) fn field(self, id: u8) -> Option<FieldHandle<T>> {
247        self.fields.get(usize::from(id)).map(|vtable| {
248            // Safety: Field vtables are of same type as the content vtable.
249            unsafe { Handle::new(self.0, vtable) }
250        })
251    }
252
253    /// Provides safe access to all field operations.
254    pub(super) fn fields(self) -> impl Iterator<Item = FieldHandle<T>>
255    where
256        T: Copy,
257    {
258        self.fields.iter().map(move |vtable| {
259            // Safety: Field vtables are of same type as the content vtable.
260            unsafe { Handle::new(self.0, vtable) }
261        })
262    }
263}
264
265impl ContentHandle<&RawContent> {
266    /// See [`ContentVtable::debug`].
267    pub fn debug(&self, f: &mut Formatter) -> fmt::Result {
268        // Safety: `Handle` has the invariant that the vtable is matching.
269        unsafe { (self.1.debug)(self.0, f) }
270    }
271
272    /// See [`ContentVtable::repr`].
273    pub fn repr(&self) -> Option<EcoString> {
274        // Safety: `Handle` has the invariant that the vtable is matching.
275        unsafe { self.1.repr.map(|f| f(self.0)) }
276    }
277
278    /// See [`ContentVtable::clone`].
279    pub fn clone(&self) -> RawContent {
280        // Safety: `Handle` has the invariant that the vtable is matching.
281        unsafe { (self.1.clone)(self.0) }
282    }
283
284    /// See [`ContentVtable::hash`].
285    pub fn hash(&self) -> u128 {
286        // Safety: `Handle` has the invariant that the vtable is matching.
287        unsafe { (self.1.hash)(self.0) }
288    }
289}
290
291impl ContentHandle<&mut RawContent> {
292    /// See [`ContentVtable::drop`].
293    pub unsafe fn drop(&mut self) {
294        // Safety:
295        // - `Handle` has the invariant that the vtable is matching.
296        // - The caller satisfies the requirements of `drop`
297        unsafe { (self.1.drop)(self.0) }
298    }
299}
300
301impl ContentHandle<(&RawContent, &RawContent)> {
302    /// See [`ContentVtable::eq`].
303    pub fn eq(&self) -> Option<bool> {
304        // Safety: `Handle` has the invariant that the vtable is matching.
305        let (a, b) = self.0;
306        unsafe { self.1.eq.map(|f| f(a, b)) }
307    }
308}
309
310/// A vtable for performing field-specific actions on type-erased
311/// content. Also contains general metadata for the specific field.
312#[repr(C)]
313pub struct FieldVtable<T: 'static = RawContent> {
314    /// The field's name, as in code.
315    pub(super) name: &'static str,
316    /// The fields's documentation (as Markdown).
317    pub(super) docs: &'static str,
318    /// Where the field is defined in the source code.
319    pub(super) def_site: DefSite,
320
321    /// Whether the field's parameter is positional.
322    pub(super) positional: bool,
323    /// Whether the field's parameter is variadic.
324    pub(super) variadic: bool,
325    /// Whether the field's parameter is required.
326    pub(super) required: bool,
327    /// Whether the field can be set via a set rule.
328    pub(super) settable: bool,
329    /// Whether the field is synthesized (i.e. initially not present).
330    pub(super) synthesized: bool,
331    /// Reflects what types the field's parameter accepts.
332    pub(super) input: fn() -> CastInfo,
333    /// Produces the default value of the field, if any. This would e.g. be
334    /// `None` for a required parameter.
335    pub(super) default: Option<fn() -> Value>,
336
337    /// Whether the field is set on the given element. Always true for required
338    /// fields, but can be false for settable or synthesized fields.
339    pub(super) has: unsafe fn(elem: &T) -> bool,
340    /// Retrieves the field and [turns it into a
341    /// value](crate::foundations::IntoValue).
342    pub(super) get: unsafe fn(elem: &T) -> Option<Value>,
343    /// Retrieves the field given styles. The resulting value may come from the
344    /// element, the style chain, or a mix (if it's a
345    /// [`Fold`](crate::foundations::Fold) field).
346    pub(super) get_with_styles: unsafe fn(elem: &T, StyleChain) -> Option<Value>,
347    /// Retrieves the field just from the styles.
348    pub(super) get_from_styles: fn(StyleChain) -> Option<Value>,
349    /// Sets the field from the styles if it is currently unset. (Or merges
350    /// with the style data in case of a `Fold` field).
351    pub(super) materialize: unsafe fn(elem: &mut T, styles: StyleChain),
352    /// Compares the field for equality.
353    pub(super) eq: unsafe fn(a: &T, b: &T) -> bool,
354}
355
356impl FieldHandle<&RawContent> {
357    /// See [`FieldVtable::has`].
358    pub fn has(&self) -> bool {
359        // Safety: `Handle` has the invariant that the vtable is matching.
360        unsafe { (self.1.has)(self.0) }
361    }
362
363    /// See [`FieldVtable::get`].
364    pub fn get(&self) -> Option<Value> {
365        // Safety: `Handle` has the invariant that the vtable is matching.
366        unsafe { (self.1.get)(self.0) }
367    }
368
369    /// See [`FieldVtable::get_with_styles`].
370    pub fn get_with_styles(&self, styles: StyleChain) -> Option<Value> {
371        // Safety: `Handle` has the invariant that the vtable is matching.
372        unsafe { (self.1.get_with_styles)(self.0, styles) }
373    }
374}
375
376impl FieldHandle<&mut RawContent> {
377    /// See [`FieldVtable::materialize`].
378    pub fn materialize(&mut self, styles: StyleChain) {
379        // Safety: `Handle` has the invariant that the vtable is matching.
380        unsafe { (self.1.materialize)(self.0, styles) }
381    }
382}
383
384impl FieldHandle<(&RawContent, &RawContent)> {
385    /// See [`FieldVtable::eq`].
386    pub fn eq(&self) -> bool {
387        // Safety: `Handle` has the invariant that the vtable is matching.
388        let (a, b) = self.0;
389        unsafe { (self.1.eq)(a, b) }
390    }
391}