xref: /wasmtime-44.0.1/docs/wasip2-plugins.md (revision 63f094df)
1*63f094dfSTim Chevalier# Calculator with WebAssembly Plugins
2*63f094dfSTim Chevalier
3*63f094dfSTim ChevalierThis example demonstrates how to embed Wasmtime
4*63f094dfSTim Chevalierto create an application that uses plugins.
5*63f094dfSTim ChevalierThe plugins are WebAssembly components.
6*63f094dfSTim Chevalier
7*63f094dfSTim ChevalierYou can [browse this source code online](https://github.com/bytecodealliance/wasmtime/blob/main/examples/wasip2-plugins)
8*63f094dfSTim Chevalierand clone the wasmtime repository to run this example locally.
9*63f094dfSTim Chevalier
10*63f094dfSTim ChevalierThis application is a simplified version of the application presented
11*63f094dfSTim Chevalierin Sy Brand's blog post
12*63f094dfSTim Chevalier["Building Native Plugin Systems with WebAssembly Components "](https://tartanllama.xyz/posts/wasm-plugins/).
13*63f094dfSTim ChevalierConsult that blog post for a more complex example of embedding Wasmtime
14*63f094dfSTim Chevalierand using plugins.
15*63f094dfSTim Chevalier
16*63f094dfSTim Chevalier## The calculator
17*63f094dfSTim Chevalier
18*63f094dfSTim ChevalierThe calculator being implemented is very simple;
19*63f094dfSTim Chevalierit takes an expression represented in prefix form, without parentheses,
20*63f094dfSTim Chevalieron the command line. For example:
21*63f094dfSTim Chevalier
22*63f094dfSTim Chevalier```
23*63f094dfSTim Chevaliertarget/release/calculator --plugins plugins/ add 1 2
24*63f094dfSTim Chevalier```
25*63f094dfSTim Chevalier
26*63f094dfSTim Chevalieror
27*63f094dfSTim Chevalier
28*63f094dfSTim Chevalier```
29*63f094dfSTim Chevaliertarget/release/calculator --plugins plugins/ subtract -1 -2
30*63f094dfSTim Chevalier```
31*63f094dfSTim Chevalier
32*63f094dfSTim ChevalierThe set of operations available is defined by the set of plugins present in
33*63f094dfSTim Chevalierthe `plugins/` directory.
34*63f094dfSTim Chevalier
35*63f094dfSTim ChevalierEach plugin is a component that supports two operations:
36*63f094dfSTim Chevalier* `get_plugin_name`: Returns the name of the arithmetic operation
37*63f094dfSTim Chevalier  that this plugin implements.
38*63f094dfSTim Chevalier* `evaluate`: Takes two signed integer arguments and returns the result
39*63f094dfSTim Chevalier  of evaluating the operation on the arguments.
40*63f094dfSTim Chevalier
41*63f094dfSTim ChevalierTwo example plugins are included: an `add` plugin implemented in C,
42*63f094dfSTim Chevalierand a `subtract` plugin implemented in JavaScript.
43*63f094dfSTim ChevalierRunning `cargo build --release` will generate the plugins:
44*63f094dfSTim Chevalier`c-plugin/add.wasm` and `js-plugin/subtract.wasm`.
45*63f094dfSTim ChevalierTo run the code, you should copy both of these files into the
46*63f094dfSTim Chevalier`plugins/` directory that you provide with the `--plugins` option.
47*63f094dfSTim Chevalier
48*63f094dfSTim Chevalier> To build the plugins, you must install `wit-bindgen` and the WASI SDK
49*63f094dfSTim Chevalier> (for building the C plugin) and `jco` (for building the JavaScript
50*63f094dfSTim Chevalier> plugin). For instructions, see [the C/C++ section](https://component-model.bytecodealliance.org/language-support/c.html)
51*63f094dfSTim Chevalier> and [the JavaScript section](https://component-model.bytecodealliance.org/language-support/javascript.html)
52*63f094dfSTim Chevalier> of the [Component Model documentation](https://component-model.bytecodealliance.org/).
53*63f094dfSTim Chevalier
54*63f094dfSTim ChevalierThere are no nested expressions.
55*63f094dfSTim Chevalier
56*63f094dfSTim Chevalier## WIT bindings
57*63f094dfSTim Chevalier
58*63f094dfSTim ChevalierTo define the interface for the plugins, we have to create a `.wit` file.
59*63f094dfSTim ChevalierThe contents of this file are:
60*63f094dfSTim Chevalier
61*63f094dfSTim Chevalier```wit
62*63f094dfSTim Chevalierpackage docs:calculator;
63*63f094dfSTim Chevalier
64*63f094dfSTim Chevalierinterface host {}
65*63f094dfSTim Chevalier
66*63f094dfSTim Chevalierworld plugin {
67*63f094dfSTim Chevalier    import host;
68*63f094dfSTim Chevalier
69*63f094dfSTim Chevalier    export get-plugin-name: func() -> string;
70*63f094dfSTim Chevalier    export evaluate: func(x: s32, y: s32) -> s32;
71*63f094dfSTim Chevalier}
72*63f094dfSTim Chevalier```
73*63f094dfSTim Chevalier
74*63f094dfSTim ChevalierThe WIT file defines a world that the plugin must implement,
75*63f094dfSTim Chevalieras well as any imports it can expect its host to provide.
76*63f094dfSTim ChevalierIn this case, the `host` interface is empty, indicating that
77*63f094dfSTim Chevalierthere is no functionality in the host that a plugin can call.
78*63f094dfSTim Chevalier
79*63f094dfSTim ChevalierThe world has two exports, indicating that the plugin must implement
80*63f094dfSTim Chevaliertwo functions: a `get-plugin-name` function that returns the name
81*63f094dfSTim Chevalierof the operation that this plugin implements, and an `evaluate` function
82*63f094dfSTim Chevalierthat does computation specific to this plugin.
83*63f094dfSTim Chevalier
84*63f094dfSTim ChevalierIn the calculator code, we write:
85*63f094dfSTim Chevalier
86*63f094dfSTim Chevalier```wit
87*63f094dfSTim Chevalierbindgen!("plugin");
88*63f094dfSTim Chevalier```
89*63f094dfSTim Chevalier
90*63f094dfSTim Chevalierwhich uses a macro provided by Wasmtime that automatically runs
91*63f094dfSTim Chevalierthe `wit-bindgen` tool to generate bindings for the world.
92*63f094dfSTim Chevalier
93*63f094dfSTim Chevalier## `CalculatorState`
94*63f094dfSTim Chevalier
95*63f094dfSTim ChevalierWe use the `CalculatorState` type to represent the global state of the program,
96*63f094dfSTim Chevalierwhich in this case is just a mapping from strings that represent operation names
97*63f094dfSTim Chevalierto `PluginDesc`s. A `PluginDesc` represents the information needed in order
98*63f094dfSTim Chevalierto execute a plugin given arguments.
99*63f094dfSTim Chevalier
100*63f094dfSTim Chevalier## Loading plugins
101*63f094dfSTim Chevalier
102*63f094dfSTim ChevalierThe application takes a directory as a command-line argument,
103*63f094dfSTim Chevalierwhich is expected to contain plugins (as .wasm files).
104*63f094dfSTim ChevalierAll plugins in the directory are loaded eagerly.
105*63f094dfSTim Chevalier
106*63f094dfSTim ChevalierThe `load_plugins()` function starts by calling the Wasmtime library's
107*63f094dfSTim Chevalier`Engine::default()` function to create an `Engine`:
108*63f094dfSTim Chevalier
109*63f094dfSTim Chevalier```rust
110*63f094dfSTim Chevalierlet engine = Engine::default();
111*63f094dfSTim Chevalier```
112*63f094dfSTim Chevalier
113*63f094dfSTim ChevalierAn `Engine` is an environment for executing WebAssembly programs.
114*63f094dfSTim ChevalierOnly one `Engine` is needed regardless of how many plugins
115*63f094dfSTim Chevaliermay be executed. For more details, see the [Wasmtime crate documentation](https://docs.rs/wasmtime/latest/wasmtime/#core-concepts).
116*63f094dfSTim Chevalier
117*63f094dfSTim ChevalierNext, it passes the engine to the Wasmtime library's `Linker::new()`
118*63f094dfSTim Chevalierfunction to create a `Linker`. A `Linker` is parameterized over a state
119*63f094dfSTim Chevaliertype.
120*63f094dfSTim ChevalierIn this application, we don't need per-plugin state
121*63f094dfSTim Chevalier(plugins implement pure functions), so the state type is `()` (the unit type).
122*63f094dfSTim Chevalier
123*63f094dfSTim Chevalier```rust
124*63f094dfSTim Chevalierlet linker: Linker<()> = Linker::new(&engine);
125*63f094dfSTim Chevalier```
126*63f094dfSTim Chevalier
127*63f094dfSTim ChevalierAs with the `Engine`, only one `Linker` is needed for the whole application.
128*63f094dfSTim ChevalierA `Linker` can be used to define functions in the host (in this case, the
129*63f094dfSTim Chevaliercalculator application) that can be called by guests (in this case, plugins
130*63f094dfSTim Chevalierloaded by the calculator). We don't define any such functions in our host,
131*63f094dfSTim Chevalierso the linker is only used as an argument to the `instantiate()` function
132*63f094dfSTim Chevalier(which we'll see a little bit later).
133*63f094dfSTim Chevalier
134*63f094dfSTim ChevalierThe remaining code checks that the provided path for the plugins directory
135*63f094dfSTim Chevalierexists and is really a directory, and if so, calls `load_plugin()` on
136*63f094dfSTim Chevaliereach file in the directory that has the `.wasm` extension:
137*63f094dfSTim Chevalier
138*63f094dfSTim Chevalier```rust
139*63f094dfSTim Chevalier    if !plugins_dir.is_dir() {
140*63f094dfSTim Chevalier        anyhow::bail!("plugins directory does not exist");
141*63f094dfSTim Chevalier    }
142*63f094dfSTim Chevalier
143*63f094dfSTim Chevalier    for entry in fs::read_dir(plugins_dir)? {
144*63f094dfSTim Chevalier        let path = entry?.path();
145*63f094dfSTim Chevalier        if path.is_file() && path.extension().and_then(OsStr::to_str) == Some("wasm") {
146*63f094dfSTim Chevalier            load_plugin(state, &engine, &linker, path)?;
147*63f094dfSTim Chevalier        }
148*63f094dfSTim Chevalier    }
149*63f094dfSTim Chevalier```
150*63f094dfSTim Chevalier
151*63f094dfSTim ChevalierNext let's look at the `load_plugin()` function, which loads a single plugin.
152*63f094dfSTim ChevalierThe function begins by calling the Wasmtime library's `Component::from_file()`
153*63f094dfSTim Chevalierfunction, which takes an `Engine` and the name of a binary WebAssembly file.
154*63f094dfSTim Chevalier
155*63f094dfSTim Chevalier```rust
156*63f094dfSTim Chevalierlet component = Component::from_file(engine, &path)?;
157*63f094dfSTim Chevalier```
158*63f094dfSTim Chevalier
159*63f094dfSTim Chevalier`from_file()` loads and compiles WebAssembly code and creates
160*63f094dfSTim Chevalierthe in-memory representation of a component, which we assign to
161*63f094dfSTim Chevalierthe variable `component`.
162*63f094dfSTim Chevalier
163*63f094dfSTim ChevalierThe next block of code creates the dynamic representation
164*63f094dfSTim Chevalierof the component, which has all the resources it needs and can have its
165*63f094dfSTim Chevalierfunctions called:
166*63f094dfSTim Chevalier
167*63f094dfSTim Chevalier```rust
168*63f094dfSTim Chevalier    let (plugin_name, plugin, store) = {
169*63f094dfSTim Chevalier        let mut store = Store::new(engine, ());
170*63f094dfSTim Chevalier        let plugin = Plugin::instantiate(&mut store, &component, linker)?;
171*63f094dfSTim Chevalier        (plugin.call_get_plugin_name(&mut store)?, plugin, store)
172*63f094dfSTim Chevalier    };
173*63f094dfSTim Chevalier```
174*63f094dfSTim Chevalier
175*63f094dfSTim ChevalierFirst, it calls the Wasmtime library's `Store::new` function to create a `Store`.
176*63f094dfSTim ChevalierA [`Store`](https://docs.rs/wasmtime/latest/wasmtime/#core-concepts)
177*63f094dfSTim Chevalierrepresents the state of a particular component. Unlike an `Engine`, it's specific
178*63f094dfSTim Chevalierto each unit of code and so it can't be re-used across different plugins.
179*63f094dfSTim ChevalierThe `Store` type is parameterized with a state type `T`, and the third argument
180*63f094dfSTim Chevalierto `Store::new` must have type `T`.
181*63f094dfSTim ChevalierSince we don't use host state in this example,
182*63f094dfSTim Chevalierwe pass in `()`, which has type `()`; so we get a `Store<()>` back.
183*63f094dfSTim Chevalier
184*63f094dfSTim ChevalierNext, it calls the `Plugin::instantiate()` function, which was generated
185*63f094dfSTim Chevalierautomatically by the `bindgen!("plugin")` macro.
186*63f094dfSTim ChevalierThe function takes the `Store` we just created,
187*63f094dfSTim Chevalierthe `Component` that represents the code for the plugin,
188*63f094dfSTim Chevalierand the `Linker` that was passed in to `load_plugin()`.
189*63f094dfSTim Chevalier`instantiate()` returns a `Plugin`.
190*63f094dfSTim ChevalierThe `Plugin` type corresponds to the `plugin` world from our `.wit` file,
191*63f094dfSTim Chevalierand was also automatically generated by the `bindgen!` macro.
192*63f094dfSTim Chevalier
193*63f094dfSTim ChevalierNow that we have a fully instantiated plugin, we can call its
194*63f094dfSTim Chevalier`get-plugin-name` function. The `call_get_plugin_name` method was
195*63f094dfSTim Chevaliergenerated by the `bindgen!` macro; notice that the name
196*63f094dfSTim Chevalier`call_get_plugin_name` is the same as `get-plugin-name` from the `.wit` file,
197*63f094dfSTim Chevalierbut with underscores in place of hyphens, and is prefixed by `call_`.
198*63f094dfSTim ChevalierThis method takes a `Store`, which in general allows use of host state
199*63f094dfSTim Chevalierby the implementation of the method in the plugin, though not in this case
200*63f094dfSTim Chevalier(since we have a `Store<()>`).
201*63f094dfSTim Chevalier
202*63f094dfSTim ChevalierFinally, we update the calculator's state by associating the plugin name
203*63f094dfSTim Chevalierwith a structure containing the plugin and store:
204*63f094dfSTim Chevalier
205*63f094dfSTim Chevalier```rust
206*63f094dfSTim Chevalier    state
207*63f094dfSTim Chevalier        .plugin_descs
208*63f094dfSTim Chevalier        .insert(plugin_name, PluginDesc { plugin, store });
209*63f094dfSTim Chevalier```
210*63f094dfSTim Chevalier
211*63f094dfSTim Chevalier## Running the code
212*63f094dfSTim Chevalier
213*63f094dfSTim ChevalierFinally, this line of code is responsible for actually evaluating the
214*63f094dfSTim Chevalierexpression that was provided on the command line:
215*63f094dfSTim Chevalier
216*63f094dfSTim Chevalier```rust
217*63f094dfSTim Chevalierargs.op.run(&mut state)
218*63f094dfSTim Chevalier```
219*63f094dfSTim Chevalier
220*63f094dfSTim ChevalierThe `BinaryOperation` struct has a `run` method that looks like this:
221*63f094dfSTim Chevalier
222*63f094dfSTim Chevalier```rust
223*63f094dfSTim Chevalier    fn run(self, state: &mut CalculatorState) -> anyhow::Result<()> {
224*63f094dfSTim Chevalier        let desc = lookup_plugin(state, self.op.as_ref())?;
225*63f094dfSTim Chevalier        let result = desc.plugin.call_evaluate(&mut desc.store, self.x, self.y)?;
226*63f094dfSTim Chevalier        println!("{}({}, {}) = {}", self.op, self.x, self.y, result);
227*63f094dfSTim Chevalier        Ok(())
228*63f094dfSTim Chevalier    }
229*63f094dfSTim Chevalier```
230*63f094dfSTim Chevalier
231*63f094dfSTim ChevalierFirst, it calls `lookup_plugin`, which simply looks up the operation
232*63f094dfSTim Chevaliername (`self.op`, which is just the name that was given on the command line)
233*63f094dfSTim Chevalierin the hash table that was created by `load_plugins()`.
234*63f094dfSTim ChevalierThis returns a `PluginDesc`, which is defined as:
235*63f094dfSTim Chevalier
236*63f094dfSTim Chevalier```rust
237*63f094dfSTim Chevalierpub struct PluginDesc {
238*63f094dfSTim Chevalier    pub plugin: Plugin,
239*63f094dfSTim Chevalier    pub store: wasmtime::Store<()>,
240*63f094dfSTim Chevalier}
241*63f094dfSTim Chevalier```
242*63f094dfSTim Chevalier
243*63f094dfSTim ChevalierRemember, the type `Plugin` corresponds to the `plugin` world and was
244*63f094dfSTim Chevaliergenerated automatically by the `bindgen!` macro.
245*63f094dfSTim Chevalier
246*63f094dfSTim ChevalierThe next line of code:
247*63f094dfSTim Chevalier
248*63f094dfSTim Chevalier```rust
249*63f094dfSTim Chevalierlet result = desc.plugin.call_evaluate(&mut desc.store, self.x, self.y)?;
250*63f094dfSTim Chevalier```
251*63f094dfSTim Chevalier
252*63f094dfSTim Chevalieris the one that actually calls into the plugin to do the computation.
253*63f094dfSTim ChevalierThe bindings generator guarantees that a `Plugin` has an `evaluate`
254*63f094dfSTim Chevaliermethod, which we call using `call_evaluate` (the name it was given
255*63f094dfSTim Chevalierby the bindings generator).
256*63f094dfSTim ChevalierLike `call_get_plugin_name`, it takes a `Store` as the first argument.
257*63f094dfSTim ChevalierThe other two arguments are the ones given on the command line.
258*63f094dfSTim Chevalier`result` is a 32-bit integer (`i32`) because that's the return type
259*63f094dfSTim Chevalierof `call_evaluate()`.
260*63f094dfSTim Chevalier
261*63f094dfSTim ChevalierFinally, we print the result to `stdout`.
262*63f094dfSTim Chevalier
263*63f094dfSTim Chevalier## Writing the plugins
264*63f094dfSTim Chevalier
265*63f094dfSTim ChevalierSo far we've assumed that the `plugins` directory is populated
266*63f094dfSTim Chevalierwith plugins for all the arithmetic operations we want.
267*63f094dfSTim ChevalierHow do we actually write the plugins?
268*63f094dfSTim ChevalierThe [component model documentation](https://component-model.bytecodealliance.org)
269*63f094dfSTim Chevalierdocuments how to generate WebAssembly components from various programming languages.
270*63f094dfSTim Chevalier
271*63f094dfSTim ChevalierAs part of this sample application, two plugins are provided,
272*63f094dfSTim Chevalierone in `c-plugin/` (implementing the `add` operation), and one in `js-plugin/`
273*63f094dfSTim Chevalier(implementing the `subtract`) operation.
274*63f094dfSTim ChevalierThe `build.rs` script shows how the code is built.
275*63f094dfSTim Chevalier
276*63f094dfSTim ChevalierAny number of plugins could be added, compiled from any language that
277*63f094dfSTim Chevalierhas a toolchain with WebAssembly support, implementing any other arithmetic
278*63f094dfSTim Chevalieroperations.
279*63f094dfSTim Chevalier
280*63f094dfSTim Chevalier## Wrapping up
281*63f094dfSTim Chevalier
282*63f094dfSTim ChevalierThis is a minimal example showing how to embed Wasmtime to create an
283*63f094dfSTim Chevalierapplication with dynamically loaded plugins.
284*63f094dfSTim ChevalierThe application could be extended in various ways:
285*63f094dfSTim Chevalier
286*63f094dfSTim Chevalier* Allow nested expressions (like `add(subtract(1, 2), 3)`)
287*63f094dfSTim Chevalier* Add floating-point operations
288*63f094dfSTim Chevalier* Add unary expressions (like `sqrt(2)`)
289*63f094dfSTim Chevalier
290*63f094dfSTim ChevalierThe basic mechanism for loading plugins would still be the same,
291*63f094dfSTim Chevalierwith only the application-specific logic changing.
292*63f094dfSTim Chevalier
293