typst_library/foundations/
plugin.rs

1use std::fmt::{self, Debug, Formatter};
2use std::hash::{Hash, Hasher};
3use std::sync::{Arc, Mutex};
4
5use ecow::{eco_format, EcoString};
6use typst_syntax::Spanned;
7use wasmi::Memory;
8
9use crate::diag::{bail, At, SourceResult, StrResult};
10use crate::engine::Engine;
11use crate::foundations::{cast, func, scope, Binding, Bytes, Func, Module, Scope, Value};
12use crate::loading::{DataSource, Load};
13
14/// Loads a WebAssembly module.
15///
16/// The resulting [module] will contain one Typst [function] for each function
17/// export of the loaded WebAssembly module.
18///
19/// Typst WebAssembly plugins need to follow a specific
20/// [protocol]($plugin/#protocol). To run as a plugin, a program needs to be
21/// compiled to a 32-bit shared WebAssembly library. Plugin functions may accept
22/// multiple [byte buffers]($bytes) as arguments and return a single byte
23/// buffer. They should typically be wrapped in idiomatic Typst functions that
24/// perform the necessary conversions between native Typst types and bytes.
25///
26/// For security reasons, plugins run in isolation from your system. This means
27/// that printing, reading files, or similar things are not supported.
28///
29/// # Example
30/// ```example
31/// #let myplugin = plugin("hello.wasm")
32/// #let concat(a, b) = str(
33///   myplugin.concatenate(
34///     bytes(a),
35///     bytes(b),
36///   )
37/// )
38///
39/// #concat("hello", "world")
40/// ```
41///
42/// Since the plugin function returns a module, it can be used with import
43/// syntax:
44/// ```typ
45/// #import plugin("hello.wasm"): concatenate
46/// ```
47///
48/// # Purity
49/// Plugin functions **must be pure:** A plugin function call most not have any
50/// observable side effects on future plugin calls and given the same arguments,
51/// it must always return the same value.
52///
53/// The reason for this is that Typst functions must be pure (which is quite
54/// fundamental to the language design) and, since Typst function can call
55/// plugin functions, this requirement is inherited. In particular, if a plugin
56/// function is called twice with the same arguments, Typst might cache the
57/// results and call your function only once. Moreover, Typst may run multiple
58/// instances of your plugin in multiple threads, with no state shared between
59/// them.
60///
61/// Typst does not enforce plugin function purity (for efficiency reasons), but
62/// calling an impure function will lead to unpredictable and irreproducible
63/// results and must be avoided.
64///
65/// That said, mutable operations _can be_ useful for plugins that require
66/// costly runtime initialization. Due to the purity requirement, such
67/// initialization cannot be performed through a normal function call. Instead,
68/// Typst exposes a [plugin transition API]($plugin.transition), which executes
69/// a function call and then creates a derived module with new functions which
70/// will observe the side effects produced by the transition call. The original
71/// plugin remains unaffected.
72///
73/// # Plugins and Packages
74/// Any Typst code can make use of a plugin simply by including a WebAssembly
75/// file and loading it. However, because the byte-based plugin interface is
76/// quite low-level, plugins are typically exposed through a package containing
77/// the plugin and idiomatic wrapper functions.
78///
79/// # WASI
80/// Many compilers will use the [WASI ABI](https://wasi.dev/) by default or as
81/// their only option (e.g. emscripten), which allows printing, reading files,
82/// etc. This ABI will not directly work with Typst. You will either need to
83/// compile to a different target or [stub all
84/// functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub).
85///
86/// # Protocol
87/// To be used as a plugin, a WebAssembly module must conform to the following
88/// protocol:
89///
90/// ## Exports
91/// A plugin module can export functions to make them callable from Typst. To
92/// conform to the protocol, an exported function should:
93///
94/// - Take `n` 32-bit integer arguments `a_1`, `a_2`, ..., `a_n` (interpreted as
95///   lengths, so `usize/size_t` may be preferable), and return one 32-bit
96///   integer.
97///
98/// - The function should first allocate a buffer `buf` of length `a_1 + a_2 +
99///   ... + a_n`, and then call
100///   `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`.
101///
102/// - The `a_1` first bytes of the buffer now constitute the first argument, the
103///   `a_2` next bytes the second argument, and so on.
104///
105/// - The function can now do its job with the arguments and produce an output
106///   buffer. Before returning, it should call
107///   `wasm_minimal_protocol_send_result_to_host` to send its result back to the
108///   host.
109///
110/// - To signal success, the function should return `0`.
111///
112/// - To signal an error, the function should return `1`. The written buffer is
113///   then interpreted as an UTF-8 encoded error message.
114///
115/// ## Imports
116/// Plugin modules need to import two functions that are provided by the
117/// runtime. (Types and functions are described using WAT syntax.)
118///
119/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func
120///   (param i32)))`
121///
122///   Writes the arguments for the current function into a plugin-allocated
123///   buffer. When a plugin function is called, it [receives the
124///   lengths](#exports) of its input buffers as arguments. It should then
125///   allocate a buffer whose capacity is at least the sum of these lengths. It
126///   should then call this function with a `ptr` to the buffer to fill it with
127///   the arguments, one after another.
128///
129/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func
130///   (param i32 i32)))`
131///
132///   Sends the output of the current function to the host (Typst). The first
133///   parameter shall be a pointer to a buffer (`ptr`), while the second is the
134///   length of that buffer (`len`). The memory pointed at by `ptr` can be freed
135///   immediately after this function returns. If the message should be
136///   interpreted as an error message, it should be encoded as UTF-8.
137///
138/// # Resources
139/// For more resources, check out the [wasm-minimal-protocol
140/// repository](https://github.com/astrale-sharp/wasm-minimal-protocol). It
141/// contains:
142///
143/// - A list of example plugin implementations and a test runner for these
144///   examples
145/// - Wrappers to help you write your plugin in Rust (Zig wrapper in
146///   development)
147/// - A stubber for WASI
148#[func(scope)]
149pub fn plugin(
150    engine: &mut Engine,
151    /// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes.
152    source: Spanned<DataSource>,
153) -> SourceResult<Module> {
154    let data = source.load(engine.world)?;
155    Plugin::module(data).at(source.span)
156}
157
158#[scope]
159impl plugin {
160    /// Calls a plugin function that has side effects and returns a new module
161    /// with plugin functions that are guaranteed to have observed the results
162    /// of the mutable call.
163    ///
164    /// Note that calling an impure function through a normal function call
165    /// (without use of the transition API) is forbidden and leads to
166    /// unpredictable behaviour. Read the [section on purity]($plugin/#purity)
167    /// for more details.
168    ///
169    /// In the example below, we load the plugin `hello-mut.wasm` which exports
170    /// two functions: The `get()` function retrieves a global array as a
171    /// string. The `add(value)` function adds a value to the global array.
172    ///
173    /// We call `add` via the transition API. The call `mutated.get()` on the
174    /// derived module will observe the addition. Meanwhile the original module
175    /// remains untouched as demonstrated by the `base.get()` call.
176    ///
177    /// _Note:_ Due to limitations in the internal WebAssembly implementation,
178    /// the transition API can only guarantee to reflect changes in the plugin's
179    /// memory, not in WebAssembly globals. If your plugin relies on changes to
180    /// globals being visible after transition, you might want to avoid use of
181    /// the transition API for now. We hope to lift this limitation in the
182    /// future.
183    ///
184    /// ```typ
185    /// #let base = plugin("hello-mut.wasm")
186    /// #assert.eq(base.get(), "[]")
187    ///
188    /// #let mutated = plugin.transition(base.add, "hello")
189    /// #assert.eq(base.get(), "[]")
190    /// #assert.eq(mutated.get(), "[hello]")
191    /// ```
192    #[func]
193    pub fn transition(
194        /// The plugin function to call.
195        func: PluginFunc,
196        /// The byte buffers to call the function with.
197        #[variadic]
198        arguments: Vec<Bytes>,
199    ) -> StrResult<Module> {
200        func.transition(arguments)
201    }
202}
203
204/// A function loaded from a WebAssembly plugin.
205#[derive(Debug, Clone, PartialEq, Hash)]
206pub struct PluginFunc {
207    /// The underlying plugin, shared by this and the other functions.
208    plugin: Arc<Plugin>,
209    /// The name of the plugin function.
210    name: EcoString,
211}
212
213impl PluginFunc {
214    /// The name of the plugin function.
215    pub fn name(&self) -> &str {
216        &self.name
217    }
218
219    /// Call the WebAssembly function with the given arguments.
220    #[comemo::memoize]
221    #[typst_macros::time(name = "call plugin")]
222    pub fn call(&self, args: Vec<Bytes>) -> StrResult<Bytes> {
223        self.plugin.call(&self.name, args)
224    }
225
226    /// Transition a plugin and turn the result into a module.
227    #[comemo::memoize]
228    #[typst_macros::time(name = "transition plugin")]
229    pub fn transition(&self, args: Vec<Bytes>) -> StrResult<Module> {
230        self.plugin.transition(&self.name, args).map(Plugin::into_module)
231    }
232}
233
234cast! {
235    PluginFunc,
236    self => Value::Func(self.into()),
237    v: Func => v.to_plugin().ok_or("expected plugin function")?.clone(),
238}
239
240/// A plugin with potentially multiple instances for multi-threaded
241/// execution.
242struct Plugin {
243    /// Shared by all variants of the plugin.
244    base: Arc<PluginBase>,
245    /// A pool of plugin instances.
246    ///
247    /// When multiple plugin calls run concurrently due to multi-threading, we
248    /// create new instances whenever we run out of ones.
249    pool: Mutex<Vec<PluginInstance>>,
250    /// A snapshot that new instances should be restored to.
251    snapshot: Option<Snapshot>,
252    /// A combined hash that incorporates all function names and arguments used
253    /// in transitions of this plugin, such that this plugin has a deterministic
254    /// hash and equality check that can differentiate it from "siblings" (same
255    /// base, different transitions).
256    fingerprint: u128,
257}
258
259impl Plugin {
260    /// Create a plugin and turn it into a module.
261    #[comemo::memoize]
262    #[typst_macros::time(name = "load plugin")]
263    fn module(bytes: Bytes) -> StrResult<Module> {
264        Self::new(bytes).map(Self::into_module)
265    }
266
267    /// Create a new plugin from raw WebAssembly bytes.
268    fn new(bytes: Bytes) -> StrResult<Self> {
269        let engine = wasmi::Engine::default();
270        let module = wasmi::Module::new(&engine, bytes.as_slice())
271            .map_err(|err| format!("failed to load WebAssembly module ({err})"))?;
272
273        // Ensure that the plugin exports its memory.
274        if !matches!(module.get_export("memory"), Some(wasmi::ExternType::Memory(_))) {
275            bail!("plugin does not export its memory");
276        }
277
278        let mut linker = wasmi::Linker::new(&engine);
279        linker
280            .func_wrap(
281                "typst_env",
282                "wasm_minimal_protocol_send_result_to_host",
283                wasm_minimal_protocol_send_result_to_host,
284            )
285            .unwrap();
286        linker
287            .func_wrap(
288                "typst_env",
289                "wasm_minimal_protocol_write_args_to_buffer",
290                wasm_minimal_protocol_write_args_to_buffer,
291            )
292            .unwrap();
293
294        let base = Arc::new(PluginBase { bytes, linker, module });
295        let instance = PluginInstance::new(&base, None)?;
296
297        Ok(Self {
298            base,
299            snapshot: None,
300            fingerprint: 0,
301            pool: Mutex::new(vec![instance]),
302        })
303    }
304
305    /// Execute a function with access to an instsance.
306    fn call(&self, func: &str, args: Vec<Bytes>) -> StrResult<Bytes> {
307        // Acquire an instance from the pool (potentially creating a new one).
308        let mut instance = self.acquire()?;
309
310        // Execute the call on an instance from the pool. If the call fails, we
311        // return early and _don't_ return the instance to the pool as it might
312        // be irrecoverably damaged.
313        let output = instance.call(func, args)?;
314
315        // Return the instance to the pool.
316        self.pool.lock().unwrap().push(instance);
317
318        Ok(output)
319    }
320
321    /// Call a mutable plugin function, producing a new mutable whose functions
322    /// are guaranteed to be able to observe the mutation.
323    fn transition(&self, func: &str, args: Vec<Bytes>) -> StrResult<Plugin> {
324        // Derive a new transition hash from the old one and the function and arguments.
325        let fingerprint = typst_utils::hash128(&(self.fingerprint, func, &args));
326
327        // Execute the mutable call on an instance.
328        let mut instance = self.acquire()?;
329
330        // Call the function. If the call fails, we return early and _don't_
331        // return the instance to the pool as it might be irrecoverably damaged.
332        instance.call(func, args)?;
333
334        // Snapshot the instance after the mutable call.
335        let snapshot = instance.snapshot();
336
337        // Create a new plugin and move (this is important!) the used instance
338        // into it, so that the old plugin won't observe the mutation. Also
339        // save the snapshot so that instances that are initialized for the
340        // transitioned plugin's pool observe the mutation.
341        Ok(Self {
342            base: self.base.clone(),
343            snapshot: Some(snapshot),
344            fingerprint,
345            pool: Mutex::new(vec![instance]),
346        })
347    }
348
349    /// Acquire an instance from the pool (or create a new one).
350    fn acquire(&self) -> StrResult<PluginInstance> {
351        // Don't use match to ensure that the lock is released before we create
352        // a new instance.
353        if let Some(instance) = self.pool.lock().unwrap().pop() {
354            return Ok(instance);
355        }
356
357        PluginInstance::new(&self.base, self.snapshot.as_ref())
358    }
359
360    /// Turn a plugin into a Typst module containing plugin functions.
361    fn into_module(self) -> Module {
362        let shared = Arc::new(self);
363
364        // Build a scope from the collected functions.
365        let mut scope = Scope::new();
366        for export in shared.base.module.exports() {
367            if matches!(export.ty(), wasmi::ExternType::Func(_)) {
368                let name = EcoString::from(export.name());
369                let func = PluginFunc { plugin: shared.clone(), name: name.clone() };
370                scope.bind(name, Binding::detached(Func::from(func)));
371            }
372        }
373
374        Module::anonymous(scope)
375    }
376}
377
378impl Debug for Plugin {
379    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
380        f.pad("Plugin(..)")
381    }
382}
383
384impl PartialEq for Plugin {
385    fn eq(&self, other: &Self) -> bool {
386        self.base.bytes == other.base.bytes && self.fingerprint == other.fingerprint
387    }
388}
389
390impl Hash for Plugin {
391    fn hash<H: Hasher>(&self, state: &mut H) {
392        self.base.bytes.hash(state);
393        self.fingerprint.hash(state);
394    }
395}
396
397/// Shared by all pooled & transitioned variants of the plugin.
398struct PluginBase {
399    /// The raw WebAssembly bytes.
400    bytes: Bytes,
401    /// The compiled WebAssembly module.
402    module: wasmi::Module,
403    /// A linker used to create a `Store` for execution.
404    linker: wasmi::Linker<CallData>,
405}
406
407/// An single plugin instance for single-threaded execution.
408struct PluginInstance {
409    /// The underlying wasmi instance.
410    instance: wasmi::Instance,
411    /// The execution store of this concrete plugin instance.
412    store: wasmi::Store<CallData>,
413}
414
415/// A snapshot of a plugin instance.
416struct Snapshot {
417    /// The number of pages in the main memory.
418    mem_pages: u32,
419    /// The data in the main memory.
420    mem_data: Vec<u8>,
421}
422
423impl PluginInstance {
424    /// Create a new execution instance of a plugin, potentially restoring
425    /// a snapshot.
426    #[typst_macros::time(name = "create plugin instance")]
427    fn new(base: &PluginBase, snapshot: Option<&Snapshot>) -> StrResult<PluginInstance> {
428        let mut store = wasmi::Store::new(base.linker.engine(), CallData::default());
429        let instance = base
430            .linker
431            .instantiate(&mut store, &base.module)
432            .and_then(|pre_instance| pre_instance.start(&mut store))
433            .map_err(|e| eco_format!("{e}"))?;
434
435        let mut instance = PluginInstance { instance, store };
436        if let Some(snapshot) = snapshot {
437            instance.restore(snapshot);
438        }
439        Ok(instance)
440    }
441
442    /// Call a plugin function with byte arguments.
443    fn call(&mut self, func: &str, args: Vec<Bytes>) -> StrResult<Bytes> {
444        let handle = self
445            .instance
446            .get_export(&self.store, func)
447            .unwrap()
448            .into_func()
449            .unwrap();
450        let ty = handle.ty(&self.store);
451
452        // Check function signature. Do this lazily only when a function is called
453        // because there might be exported functions like `_initialize` that don't
454        // match the schema.
455        if ty.params().iter().any(|&v| v != wasmi::core::ValType::I32) {
456            bail!(
457                "plugin function `{func}` has a parameter that is not a 32-bit integer"
458            );
459        }
460        if ty.results() != [wasmi::core::ValType::I32] {
461            bail!("plugin function `{func}` does not return exactly one 32-bit integer");
462        }
463
464        // Check inputs.
465        let expected = ty.params().len();
466        let given = args.len();
467        if expected != given {
468            bail!(
469                "plugin function takes {expected} argument{}, but {given} {} given",
470                if expected == 1 { "" } else { "s" },
471                if given == 1 { "was" } else { "were" },
472            );
473        }
474
475        // Collect the lengths of the argument buffers.
476        let lengths = args
477            .iter()
478            .map(|a| wasmi::Val::I32(a.len() as i32))
479            .collect::<Vec<_>>();
480
481        // Store the input data.
482        self.store.data_mut().args = args;
483
484        // Call the function.
485        let mut code = wasmi::Val::I32(-1);
486        handle
487            .call(&mut self.store, &lengths, std::slice::from_mut(&mut code))
488            .map_err(|err| eco_format!("plugin panicked: {err}"))?;
489
490        if let Some(MemoryError { offset, length, write }) =
491            self.store.data_mut().memory_error.take()
492        {
493            return Err(eco_format!(
494                "plugin tried to {kind} out of bounds: \
495                 pointer {offset:#x} is out of bounds for {kind} of length {length}",
496                kind = if write { "write" } else { "read" }
497            ));
498        }
499
500        // Extract the returned data.
501        let output = std::mem::take(&mut self.store.data_mut().output);
502
503        // Parse the functions return value.
504        match code {
505            wasmi::Val::I32(0) => {}
506            wasmi::Val::I32(1) => match std::str::from_utf8(&output) {
507                Ok(message) => bail!("plugin errored with: {message}"),
508                Err(_) => {
509                    bail!("plugin errored, but did not return a valid error message")
510                }
511            },
512            _ => bail!("plugin did not respect the protocol"),
513        };
514
515        Ok(Bytes::new(output))
516    }
517
518    /// Creates a snapshot of this instance from which another one can be
519    /// initialized.
520    #[typst_macros::time(name = "save snapshot")]
521    fn snapshot(&self) -> Snapshot {
522        let memory = self.memory();
523        let mem_pages = memory.size(&self.store);
524        let mem_data = memory.data(&self.store).to_vec();
525        Snapshot { mem_pages, mem_data }
526    }
527
528    /// Restores the instance to a snapshot.
529    #[typst_macros::time(name = "restore snapshot")]
530    fn restore(&mut self, snapshot: &Snapshot) {
531        let memory = self.memory();
532        let current_size = memory.size(&self.store);
533        if current_size < snapshot.mem_pages {
534            memory
535                .grow(&mut self.store, snapshot.mem_pages - current_size)
536                .unwrap();
537        }
538
539        memory.data_mut(&mut self.store)[..snapshot.mem_data.len()]
540            .copy_from_slice(&snapshot.mem_data);
541    }
542
543    /// Retrieves a handle to the plugin's main memory.
544    fn memory(&self) -> Memory {
545        self.instance
546            .get_export(&self.store, "memory")
547            .unwrap()
548            .into_memory()
549            .unwrap()
550    }
551}
552
553/// The persistent store data used for communication between store and host.
554#[derive(Default)]
555struct CallData {
556    /// Arguments for a current call.
557    args: Vec<Bytes>,
558    /// The results of the current call.
559    output: Vec<u8>,
560    /// A memory error that occured during execution of the current call.
561    memory_error: Option<MemoryError>,
562}
563
564/// If there was an error reading/writing memory, keep the offset + length to
565/// display an error message.
566struct MemoryError {
567    offset: u32,
568    length: u32,
569    write: bool,
570}
571
572/// Write the arguments to the plugin function into the plugin's memory.
573fn wasm_minimal_protocol_write_args_to_buffer(
574    mut caller: wasmi::Caller<CallData>,
575    ptr: u32,
576) {
577    let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
578    let arguments = std::mem::take(&mut caller.data_mut().args);
579    let mut offset = ptr as usize;
580    for arg in arguments {
581        if memory.write(&mut caller, offset, arg.as_slice()).is_err() {
582            caller.data_mut().memory_error = Some(MemoryError {
583                offset: offset as u32,
584                length: arg.len() as u32,
585                write: true,
586            });
587            return;
588        }
589        offset += arg.len();
590    }
591}
592
593/// Extracts the output of the plugin function from the plugin's memory.
594fn wasm_minimal_protocol_send_result_to_host(
595    mut caller: wasmi::Caller<CallData>,
596    ptr: u32,
597    len: u32,
598) {
599    let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
600    let mut buffer = std::mem::take(&mut caller.data_mut().output);
601    buffer.resize(len as usize, 0);
602    if memory.read(&caller, ptr as _, &mut buffer).is_err() {
603        caller.data_mut().memory_error =
604            Some(MemoryError { offset: ptr, length: len, write: false });
605        return;
606    }
607    caller.data_mut().output = buffer;
608}