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