1 use clap::Parser;
2 use std::collections::HashMap;
3 use std::ffi::OsStr;
4 use std::fs;
5 use std::path::{Path, PathBuf};
6 use wasmtime::component::{Component, Linker, bindgen};
7 use wasmtime::{Engine, Store, format_err};
8 
9 // Generates bindings for the plugin world defined in the wit/calculator.wit file.
10 bindgen!("plugin");
11 
12 /// A CLI that implements a plugin-based calculator whose
13 /// operations are implemented by independent WebAssembly components.
14 
15 #[derive(Parser)]
16 struct BinaryOperation {
17     /// The name of the operation
18     op: String,
19     /// The first operand
20     x: i32,
21     /// The second operand
22     y: i32,
23 }
24 
lookup_plugin<'a>( state: &'a mut CalculatorState, op: &str, ) -> wasmtime::Result<&'a mut PluginDesc>25 fn lookup_plugin<'a>(
26     state: &'a mut CalculatorState,
27     op: &str,
28 ) -> wasmtime::Result<&'a mut PluginDesc> {
29     state
30         .plugin_descs
31         .get_mut(op)
32         .ok_or(format_err!("unknown operation {}", op))
33 }
34 
35 impl BinaryOperation {
run(self, state: &mut CalculatorState) -> wasmtime::Result<()>36     fn run(self, state: &mut CalculatorState) -> wasmtime::Result<()> {
37         let desc = lookup_plugin(state, self.op.as_ref())?;
38         let result = desc.plugin.call_evaluate(&mut desc.store, self.x, self.y)?;
39         println!("{}({}, {}) = {}", self.op, self.x, self.y, result);
40         Ok(())
41     }
42 }
43 
44 pub struct CalculatorState {
45     // Mapping from operation names to descriptors for the plugins
46     pub plugin_descs: HashMap<String, PluginDesc>,
47 }
48 
49 impl Default for CalculatorState {
default() -> Self50     fn default() -> Self {
51         Self::new()
52     }
53 }
54 
55 impl CalculatorState {
new() -> Self56     pub fn new() -> Self {
57         CalculatorState {
58             plugin_descs: HashMap::new(),
59         }
60     }
61 }
62 
63 pub struct PluginDesc {
64     pub plugin: Plugin,
65     pub store: wasmtime::Store<()>,
66 }
67 
load_plugin( state: &mut CalculatorState, engine: &Engine, linker: &Linker<()>, path: PathBuf, ) -> wasmtime::Result<()>68 fn load_plugin(
69     state: &mut CalculatorState,
70     engine: &Engine,
71     linker: &Linker<()>,
72     path: PathBuf,
73 ) -> wasmtime::Result<()> {
74     println!("Loading plugin from file {:?}", path);
75 
76     // Creates a component from a .wasm file
77     let component = Component::from_file(engine, &path)?;
78 
79     let (plugin_name, plugin, store) = {
80         // Creates a Store for each plugin.
81         // A Store represents dynamic state, so each plugin
82         // has its own Store.
83         let mut store = Store::new(engine, ());
84         // Instantiate the plugin to the store we just created.
85         let plugin = Plugin::instantiate(&mut store, &component, linker)?;
86         (plugin.call_get_plugin_name(&mut store)?, plugin, store)
87     };
88 
89     state
90         .plugin_descs
91         .insert(plugin_name, PluginDesc { plugin, store });
92 
93     Ok(())
94 }
95 
load_plugins(state: &mut CalculatorState, plugins_dir: &Path) -> wasmtime::Result<()>96 fn load_plugins(state: &mut CalculatorState, plugins_dir: &Path) -> wasmtime::Result<()> {
97     let engine = Engine::default();
98     let linker: Linker<()> = Linker::new(&engine);
99 
100     if !plugins_dir.is_dir() {
101         wasmtime::bail!("plugins directory does not exist");
102     }
103 
104     for entry in fs::read_dir(plugins_dir)? {
105         let path = entry?.path();
106         if path.is_file() && path.extension().and_then(OsStr::to_str) == Some("wasm") {
107             load_plugin(state, &engine, &linker, path)?;
108         }
109     }
110     Ok(())
111 }
112 
113 #[derive(Parser)]
114 #[command(name = "calculator-host", version = env!("CARGO_PKG_VERSION"))]
115 #[command(about = "A calculator with plugin support")]
116 struct Args {
117     #[command(flatten)]
118     op: BinaryOperation,
119 
120     #[arg(long, help = "Plugin directory")]
121     plugins: PathBuf,
122 }
123 
main() -> wasmtime::Result<()>124 fn main() -> wasmtime::Result<()> {
125     // Get plugins directory
126     let args = Args::parse();
127 
128     // Initialize mapping from plugin names to plugins
129     let mut state = CalculatorState::new();
130 
131     // Load plugins from plugins directory
132     load_plugins(&mut state, args.plugins.as_path())?;
133 
134     // Evaluate the expression given on the command line
135     args.op.run(&mut state)
136 }
137