From 99ddbafc095695d278fba81d835e88ac23e9b990 Mon Sep 17 00:00:00 2001 From: astrale-sharp Date: Mon, 21 Aug 2023 15:17:16 +0200 Subject: [PATCH] Wasm plugin system (#1555) --- Cargo.lock | 60 +++++ assets/files/hello.wasm | Bin 0 -> 30249 bytes crates/typst-library/src/compute/construct.rs | 111 +++++++++- crates/typst-library/src/compute/mod.rs | 1 + crates/typst/Cargo.toml | 1 + crates/typst/src/eval/methods.rs | 10 +- crates/typst/src/eval/mod.rs | 2 + crates/typst/src/eval/plugin.rs | 208 ++++++++++++++++++ crates/typst/src/ide/complete.rs | 14 +- tests/typ/compiler/plugin.typ | 35 +++ 10 files changed, 439 insertions(+), 3 deletions(-) create mode 100755 assets/files/hello.wasm create mode 100644 crates/typst/src/eval/plugin.rs create mode 100644 tests/typ/compiler/plugin.typ diff --git a/Cargo.lock b/Cargo.lock index 4a7f1fa43..21c076514 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -494,6 +494,12 @@ dependencies = [ "syn 2.0.16", ] +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + [[package]] name = "ecow" version = "0.1.1" @@ -1014,6 +1020,12 @@ dependencies = [ "hashbrown 0.14.0", ] +[[package]] +name = "indexmap-nostd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" + [[package]] name = "inferno" version = "0.11.15" @@ -1066,6 +1078,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "intx" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f38a50a899dc47a6d0ed5508e7f601a2e34c3a85303514b5d137f3c10a0c75" + [[package]] name = "io-lifetimes" version = "1.0.10" @@ -2590,6 +2608,7 @@ dependencies = [ "unicode-segmentation", "unscanny", "usvg", + "wasmi", "xmlparser", "xmlwriter", "xmp-writer", @@ -3037,6 +3056,47 @@ version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +[[package]] +name = "wasmi" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51fb5c61993e71158abf5bb863df2674ca3ec39ed6471c64f07aeaf751d67b4" +dependencies = [ + "intx", + "smallvec", + "spin 0.9.8", + "wasmi_arena", + "wasmi_core", + "wasmparser-nostd", +] + +[[package]] +name = "wasmi_arena" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "401c1f35e413fac1846d4843745589d9ec678977ab35a384db8ae7830525d468" + +[[package]] +name = "wasmi_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624e6333e861ef49095d2d678b76ebf30b06bf37effca845be7e5b87c90071b7" +dependencies = [ + "downcast-rs", + "libm", + "num-traits", + "paste", +] + +[[package]] +name = "wasmparser-nostd" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9157cab83003221bfd385833ab587a039f5d6fa7304854042ba358a3b09e0724" +dependencies = [ + "indexmap-nostd", +] + [[package]] name = "web-sys" version = "0.3.63" diff --git a/assets/files/hello.wasm b/assets/files/hello.wasm new file mode 100755 index 0000000000000000000000000000000000000000..eddd738760ec237a64c81ac7be9dabe22b455b95 GIT binary patch literal 30249 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 TF{q-pOCh#mwUou**, + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult { + let Spanned { v: path, span } = path; + let id = vm.location().join(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + Plugin::new(data).at(span) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs index 757377f03..5e7e8d46b 100644 --- a/crates/typst-library/src/compute/mod.rs +++ b/crates/typst-library/src/compute/mod.rs @@ -39,4 +39,5 @@ pub(super) fn define(global: &mut Scope) { global.define("yaml", yaml_func()); global.define("xml", xml_func()); global.define("calc", calc::module()); + global.define("plugin", plugin_func()); } diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index dc1dcc773..8fdd51e01 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -55,6 +55,7 @@ usvg = { version = "0.32", default-features = false, features = ["text"] } xmlwriter = "0.1.0" xmp-writer = "0.1" time = { version = "0.3.20", features = ["std", "formatting"] } +wasmi = "0.30.0" xmlparser = "0.13.5" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/typst/src/eval/methods.rs b/crates/typst/src/eval/methods.rs index 0247a4a73..a47d945b1 100644 --- a/crates/typst/src/eval/methods.rs +++ b/crates/typst/src/eval/methods.rs @@ -2,7 +2,7 @@ use ecow::{eco_format, EcoString}; -use super::{Args, IntoValue, Str, Value, Vm}; +use super::{Args, Bytes, IntoValue, Plugin, Str, Value, Vm}; use crate::diag::{At, Hint, SourceResult}; use crate::eval::{bail, Datetime}; use crate::geom::{Align, Axes, Color, Dir, Em, GenAlign}; @@ -280,6 +280,14 @@ pub fn call( "inv" => align2d.map(GenAlign::inv).into_value(), _ => return missing(), } + } else if let Some(plugin) = dynamic.downcast::() { + if plugin.iter().any(|func_name| func_name == method) { + let bytes = args.all::()?; + args.take().finish()?; + plugin.call(method, bytes).at(span)?.into_value() + } else { + return missing(); + } } else { return (vm.items.library_method)(vm, &dynamic, method, args, span); } diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index 9141544e1..88302c4a5 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -23,6 +23,7 @@ mod methods; mod module; mod none; pub mod ops; +mod plugin; mod scope; mod symbol; mod tracer; @@ -53,6 +54,7 @@ pub use self::library::{set_lang_items, LangItems, Library}; pub use self::methods::methods_on; pub use self::module::Module; pub use self::none::NoneValue; +pub use self::plugin::Plugin; pub use self::scope::{Scope, Scopes}; pub use self::str::{format_str, Regex, Str}; pub use self::symbol::Symbol; diff --git a/crates/typst/src/eval/plugin.rs b/crates/typst/src/eval/plugin.rs new file mode 100644 index 000000000..82bb5b6ae --- /dev/null +++ b/crates/typst/src/eval/plugin.rs @@ -0,0 +1,208 @@ +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; + +use ecow::{eco_format, EcoString}; +use std::sync::{Arc, Mutex}; +use wasmi::{AsContext, AsContextMut, Caller, Engine, Linker, Module}; + +use super::{cast, Bytes}; +use crate::diag::{bail, StrResult}; + +/// A plugin loaded from WebAssembly code. +/// +/// It can run external code conforming to its protocol. +/// +/// This type is cheap to clone and hash. +#[derive(Clone)] +pub struct Plugin(Arc); + +/// The internal representation of a plugin. +struct Repr { + /// The raw WebAssembly bytes. + bytes: Bytes, + /// The function defined by the WebAssembly module. + functions: Vec<(EcoString, wasmi::Func)>, + /// Owns all data associated with the WebAssembly module. + store: Mutex, +} + +/// Owns all data associated with the WebAssembly module. +type Store = wasmi::Store; + +/// The persistent store data used for communication between store and host. +#[derive(Default)] +struct StoreData { + args: Vec, + output: Vec, +} + +impl Plugin { + /// Create a new plugin from raw WebAssembly bytes. + #[comemo::memoize] + pub fn new(bytes: Bytes) -> StrResult { + let engine = Engine::default(); + let module = Module::new(&engine, bytes.as_slice()) + .map_err(|err| format!("failed to load WebAssembly module: {err}"))?; + + let mut linker = Linker::new(&engine); + linker + .func_wrap( + "typst_env", + "wasm_minimal_protocol_send_result_to_host", + wasm_minimal_protocol_send_result_to_host, + ) + .unwrap(); + linker + .func_wrap( + "typst_env", + "wasm_minimal_protocol_write_args_to_buffer", + wasm_minimal_protocol_write_args_to_buffer, + ) + .unwrap(); + + let mut store = Store::new(&engine, StoreData::default()); + let instance = linker + .instantiate(&mut store, &module) + .and_then(|pre_instance| pre_instance.start(&mut store)) + .map_err(|e| eco_format!("{e}"))?; + + // Ensure that the plugin exports its memory. + if !matches!( + instance.get_export(&store, "memory"), + Some(wasmi::Extern::Memory(_)) + ) { + bail!("plugin does not export its memory"); + } + + // Collect exported functions. + let functions = instance + .exports(&store) + .filter_map(|export| { + let name = export.name().into(); + export.into_func().map(|func| (name, func)) + }) + .collect(); + + Ok(Plugin(Arc::new(Repr { bytes, functions, store: Mutex::new(store) }))) + } + + /// Call the plugin function with the given `name`. + pub fn call(&self, name: &str, args: Vec) -> StrResult { + // Find the function with the given name. + let func = self + .0 + .functions + .iter() + .find(|(v, _)| v == name) + .map(|&(_, func)| func) + .ok_or_else(|| { + eco_format!("plugin does not contain a function called {name}") + })?; + + let mut store = self.0.store.lock().unwrap(); + let ty = func.ty(store.as_context()); + + // Check function signature. + if ty.params().iter().any(|&v| v != wasmi::core::ValueType::I32) { + bail!( + "plugin function `{name}` has a parameter that is not a 32-bit integer" + ); + } + if ty.results() != [wasmi::core::ValueType::I32] { + bail!("plugin function `{name}` does not return exactly one 32-bit integer"); + } + + // Check inputs. + let expected = ty.params().len(); + let given = args.len(); + if expected != given { + bail!( + "plugin function takes {expected} argument{}, but {given} {} given", + if expected == 1 { "" } else { "s" }, + if given == 1 { "was" } else { "were" }, + ); + } + + // Collect the lengths of the argument buffers. + let lengths = args + .iter() + .map(|a| wasmi::Value::I32(a.len() as i32)) + .collect::>(); + + // Store the input data. + store.data_mut().args = args; + + // Call the function. + let mut code = wasmi::Value::I32(-1); + func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code)) + .map_err(|err| eco_format!("plugin panicked: {err}"))?; + + // Extract the returned data. + let output = std::mem::take(&mut store.data_mut().output); + + // Parse the functions return value. + match code { + wasmi::Value::I32(0) => {} + wasmi::Value::I32(1) => match std::str::from_utf8(&output) { + Ok(message) => bail!("plugin errored with: {message}"), + Err(_) => { + bail!("plugin errored, but did not return a valid error message") + } + }, + _ => bail!("plugin did not respect the protocol"), + }; + + Ok(output.into()) + } + + /// An iterator over all the function names defined by the plugin. + pub fn iter(&self) -> impl Iterator { + self.0.functions.as_slice().iter().map(|(func_name, _)| func_name) + } +} + +impl Debug for Plugin { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("plugin(..)") + } +} + +impl PartialEq for Plugin { + fn eq(&self, other: &Self) -> bool { + self.0.bytes == other.0.bytes + } +} + +impl Hash for Plugin { + fn hash(&self, state: &mut H) { + self.0.bytes.hash(state); + } +} + +cast! { + type Plugin: "plugin", +} + +/// Write the arguments to the plugin function into the plugin's memory. +fn wasm_minimal_protocol_write_args_to_buffer(mut caller: Caller, ptr: u32) { + let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); + let arguments = std::mem::take(&mut caller.data_mut().args); + let mut offset = ptr as usize; + for arg in arguments { + memory.write(&mut caller, offset, arg.as_slice()).unwrap(); + offset += arg.len(); + } +} + +/// Extracts the output of the plugin function from the plugin's memory. +fn wasm_minimal_protocol_send_result_to_host( + mut caller: Caller, + ptr: u32, + len: u32, +) { + let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); + let mut buffer = std::mem::take(&mut caller.data_mut().output); + buffer.resize(len as usize, 0); + memory.read(&caller, ptr as _, &mut buffer).unwrap(); + caller.data_mut().output = buffer; +} diff --git a/crates/typst/src/ide/complete.rs b/crates/typst/src/ide/complete.rs index 36d27654f..ded6205ea 100644 --- a/crates/typst/src/ide/complete.rs +++ b/crates/typst/src/ide/complete.rs @@ -10,7 +10,7 @@ use super::analyze::analyze_labels; use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; use crate::doc::Frame; use crate::eval::{ - fields_on, format_str, methods_on, CastInfo, Func, Library, Scope, Value, + fields_on, format_str, methods_on, CastInfo, Func, Library, Plugin, Scope, Value, }; use crate::syntax::{ ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, @@ -415,6 +415,18 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { } } } + Value::Dyn(val) => { + if let Some(plugin) = val.downcast::() { + for name in plugin.iter() { + ctx.completions.push(Completion { + kind: CompletionKind::Func, + label: name.clone(), + apply: None, + detail: None, + }) + } + } + } _ => {} } } diff --git a/tests/typ/compiler/plugin.typ b/tests/typ/compiler/plugin.typ new file mode 100644 index 000000000..aafbdaa1c --- /dev/null +++ b/tests/typ/compiler/plugin.typ @@ -0,0 +1,35 @@ +// Test WebAssembly plugins. +// Ref: false + +--- +#let p = plugin("/files/hello.wasm") +#test(p.hello(), bytes("Hello from wasm!!!")) +#test(p.double_it(bytes("hey!")), bytes("hey!.hey!")) +#test( + p.shuffle(bytes("value1"), bytes("value2"), bytes("value3")), + bytes("value3-value1-value2"), +) + +--- +#let p = plugin("/files/hello.wasm") + +// Error: 2-20 plugin function takes 0 arguments, but 1 was given +#p.hello(bytes("")) + +--- +#let p = plugin("/files/hello.wasm") + +// Error: 10-14 unexpected argument +#p.hello(true) + +--- +#let p = plugin("/files/hello.wasm") + +// Error: 2-17 plugin errored with: This is an `Err` +#p.returns_err() + +--- +#let p = plugin("/files/hello.wasm") + +// Error: 2-16 plugin panicked: wasm `unreachable` instruction executed +#p.will_panic()