README.md

  1# Zed's Plugin Runner
  2This is a short guide that aims to answer the following questions:
  3
  4- How do plugins work in Zed?
  5- How can I create a new plugin?
  6- How can I integrate plugins into a part of Zed?
  7
  8### Nomenclature
  9
 10- Host-side: The native Rust runtime managing plugins, e.g. Zed.
 11- Guest-side: The wasm-based runtime that plugins use.
 12
 13## How plugins work
 14Zed's plugins are WebAssembly (Wasm) based, and have access to the WebAssembly System Interface (WASI), which allows for permissions-based access to subsets of system resources, like the filesystem.
 15
 16To execute plugins, Zed's plugin system uses the sandboxed [`wasmtime`](https://wasmtime.dev/) runtime, which is Open Source and developed by the [Bytecode Alliance](https://bytecodealliance.org/). Wasmtime uses the [Cranelift](https://docs.rs/cranelift/latest/cranelift/) codegen library to compile plugins to native code.
 17
 18Zed has three `plugin` crates that implement different things:
 19
 201. `plugin_runtime` is a host-side library that loads and runs compiled `Wasm` plugins, in addition to setting up system bindings. This crate should be used host-side
 21
 222. `plugin` contains a prelude for guest-side plugins to depend on. It re-exports some required crates (e.g. `serde`, `bincode`) and provides some necessary macros for generating bindings that `plugin_runtime` can hook into.
 23
 243. `plugin_macros` implements the proc macros required by `plugin`, like the `#[import]` and `#[export]` attribute macros, and should also be used guest-side.
 25
 26### ABI
 27The interface between the host Rust runtime ('Runtime') and plugins implemented in Wasm ('Plugin') is pretty simple.
 28
 29When calling a guest-side function, all arguments are serialized to bytes and passed through `Buffer`s. We currently use `serde` + [`bincode`](https://docs.rs/bincode/latest/bincode/) to do this serialization. This means that any type that can be serialized using serde can be passed across the ABI boundary. For types that represent resources that cannot pass the ABI boundary (e.g. `Rope`), we are working on an opaque callback-based system.
 30
 31> **Note**: It's important to note that there is a draft ABI standard for Wasm called WebAssembly Interface Types (often abbreviated `WITX`). This standard is currently not stable and only experimentally supported in some runtimes. Once this proposal becomes stable, it would be a good idea to transition towards using WITX as the ABI, rather than the rather rudimentary `bincode` ABI we have now.
 32
 33All `Buffer`s are stored in Wasm linear memory (Wasm memory). A `Buffer` is a pointer, length pair to a byte array somewhere in Wasm memory. A `Buffer` itself is represented as a pair of two 4-byte (`u32`) fields:
 34
 35```rust
 36struct Buffer {
 37    ptr: u32,
 38    len: u32,
 39}
 40```
 41
 42Which we encode as a single `u64` when crossing the ABI boundary:
 43
 44```
 45+-------+-------+
 46| ptr   | len   |
 47+-------+-------+
 48        |
 49~ ~ ~ ~ | ~ ~ ~ ~ spOoky ABI boundary O.o
 50        V
 51+---------------+
 52| u64           |
 53+---------------+
 54```
 55
 56All functions that a plugin exports or imports have the following properties:
 57
 58- A function signature of `fn(u64) -> u64`, where both the argument (input) and return type (output) are a `Buffer`:
 59
 60    - The input `Buffer` will contain the input arguments serialized to `bincode`.
 61    - The output `Buffer` will contain the output arguments serialized to `bincode`.
 62
 63- Have a name starting with two underscores.
 64
 65Luckily for us, we don't have to worry about mangling names or writing serialization code. The `plugin::prelude::*` defines a couple of macros—aptly named `#[import]` and `#[export]`—that generate all serialization code and perform all mangling of names requisite for crossing the ABI boundary.
 66
 67There are also a couple important things every plugin must have:
 68
 69- `__alloc_buffer` function that, given a `u32` length, returns a `u32` pointer to a buffer of that length.
 70- `__free_buffer` function that, given a buffer encoded as a `u64`, frees the buffer at the given location, and does not return anything.
 71
 72Luckily enough for us yet again, the `plugin` prelude defines two ready-made versions of these functions, so you don't have to worry about implementing them yourselves.
 73
 74So, what does importing and exporting functions from a plugin look like in practice? I'm glad you asked...
 75
 76## Creating new plugins
 77Since Zed's plugin system uses Wasm + WASI, in theory any language that compiles to Wasm can be used to write plugins. In practice, and out of practicality, however, we currently only really support plugins written in Rust.
 78
 79A plugin is just a rust crate like any other. All plugins embedded in Zed are located in the `plugins` folder in the root. These plugins will automatically be compiled, optimized, and recompiled on change, so it's recommended that when creating a new plugin you create it there.
 80
 81As plugins are compiled to Wasm + WASI, you need to have the `wasm32-wasi` toolchain installed on your system. If you don't have it already, a little rustup magick will do the trick:
 82
 83```bash
 84rustup target add wasm32-wasi
 85```
 86
 87### Configuring a plugin
 88After you've created a new plugin in `plugins` using `cargo new --lib`, edit your `Cargo.toml` to ensure that it looks something like this:
 89
 90```toml
 91[package]
 92name = "my_very_cool_incredible_plugin_with_a_short_name_of_course"
 93version = "0.1.0"
 94edition = "2021"
 95
 96[lib]
 97crate-type = ["cdylib"]
 98
 99[dependencies]
100plugin = { path = "../../crates/plugin" }
101
102[profile.release]
103opt-level = "z"
104lto = true
105```
106
107Here's a quick explainer of what we're doing:
108
109- `crate-type = ["cdylib"]` is used because a plugin essentially acts a *library*, exposing functions with specific signatures that perform certain tasks. This key ensures that the library is generated in a reproducible manner with a layout `plugin_runtime` knows how to hook into.
110
111- `plugin = { path = "../../crates/plugin" }` is used so we have access to the prelude, which has a few useful functions and can automatically generate serialization glue code for us.
112
113- `[profile.release]` these options wholistically optimize for size, which will become increasingly important as we add more plugins.
114
115### Importing and Exporting functions
116To import or export a function, all you need are two things:
117
1181. Make sure that you've imported `plugin::prelude::*`
1192. Annotate your function or signature with `#[export]` or `#[import]` respectively.
120
121Here's an example plugin that doubles the value of every float in a `Vec<f64>` passed into it:
122
123```rust
124use plugin::prelude::*;
125
126#[export]
127pub fn double(mut x: Vec<f64>) -> Vec<f64> {
128    x.into_iter().map(|x| x * 2.0).collect()
129}
130```
131
132All the serialization code is automatically generated by `#[export]`.
133
134You can specify functions that must be defined host-side by using the `#[import]` attribute. This attribute must be attached to a function signature:
135
136```rust
137use plugin::prelude::*;
138
139#[import]
140fn run(command: String) -> Vec<u8>;
141```
142
143The `#[import]` macro will generate a function body that performs the proper serialization/deserialization needed to call out to the host rust runtime. Note that the same `serde` + `bincode` + `Buffer` ABI is used for both `#[import]` and `#[export]`.
144
145> **Note**: If you'd like to see an example of importing and exporting functions, check out the `test_plugin`, which can be found in the `plugins` directory.
146
147## Integrating plugins into Zed
148Currently, plugins are used to add support for language servers to Zed. Plugins should be fairly simple to integrate for library-like applications. Here's a quick overview of how plugins work:
149
150### Normal vs Precompiled plugins
151Plugins in the `plugins` directory are automatically recompiled and serialized to disk when compiling Zed. The resulting artifacts can be found in the `plugins/bin` directory. For each `plugin`, you should see two files:
152
153- `plugin.wasm` is the plugin compiled to Wasm. As a baseline, this should be about 4MB for debug builds and 2MB for release builds, but it depends on the specific plugin being built.
154
155- `plugin.wasm.pre` is the plugin compiled to Wasm *and additionally* precompiled to host-platform-specific native code, determined by the `TARGET` cargo exposes at compile-time. This should be about 700KB for debug builds and 500KB in release builds. Each plugin takes about 1 or 2 seconds to compile to native code using cranelift, so precompiling plugins drastically reduces the startup time required to begin to run a plugin.
156
157For all intents and purposes, it is *highly recommended* that you use precompiled plugins where possible, as they are much more lightweight and take much less time to instantiate.
158
159### Instantiating a plugin
160So you have something you'd like to add a plugin for. What now? The general pattern for adding support for plugins is as follows:
161
162#### 1. Create a struct to hold the plugin
163To call the functions that a plugin exports host-side, you need to have 'handles' to those functions. Each handle is typed and stored in `WasiFn<A, R>` where `A: Serialize` and `R: DeserializeOwned`.
164
165For example, let's suppose we're creating a plugin that:
166
1671. formats a message 
1682. processes a list of numbers somehow
169
170We could create a struct for this plugin as follows:
171
172```rust
173use plugin_runtime::{WasiFn, Plugin};
174
175pub struct CoolPlugin {
176    format:  WasiFn<String, String>,
177    process: WasiFn<Vec<f64>, f64>,
178    runtime: Plugin,
179}
180```
181
182Note that this plugin also holds an owned reference to the runtime, which is stored in the `Plugin` type. In asynchronous or multithreaded contexts, it may be required to put `Plugin` behind an `Arc<Mutex<Plugin>>`. Although plugins expose an asynchronous interface, the underlying Wasm engine can only execute a single function at a time. 
183
184> **Note**: This is a limitation of the WebAssembly standard itself. In the future, to work around this, we've been considering starting a pool of plugins, or instantiating a new plugin per call (this isn't as bad as it sounds, as instantiating a new plugin only takes about 30µs).
185
186In the following steps, we're going to build a plugin and extract handles to fill this struct we've created.
187
188#### 2. Bind all imported functions
189While a plugin can export functions, it can also import them. We'll refer to the host-side functions that a plugin imports as 'native' functions. Native functions are represented using callbacks, and both synchronous and asynchronous callbacks are supported.
190
191To bind imported functions, the first thing we need to do is create a new plugin using `PluginBuilder`. `PluginBuilder` uses the builder pattern to configure a new plugin, after which calling the `init` method will instantiate the `Plugin`.
192
193You can create a new plugin builder as follows:
194
195```rust
196let builder = PluginBuilder::new_with_default_ctx();
197```
198
199This creates a plugin with a sensible default set of WASI permissions, namely the ability to write to `stdout` and `stderr` (note that, by default, plugins do not have access to `stdin`). For more control, you can use `PluginBuilder::new` and pass in a `WasiCtx` manually.
200
201##### Synchronous Functions
202To add a sync native function to a plugin, use the `.host_function` method:
203
204```rust
205let builder = builder.host_function(
206    "add_f64", 
207    |(a, b): (f64, f64)| a + b,
208).unwrap();
209```
210
211The `.host_function` method takes two arguments: the name of the function, and a sync callback that implements it. Note that this name must match the name of the function declared in the plugin exactly. For example, to use the `add_f64` from a plugin, you must include the following `#[import]` signature:
212
213```rust
214use plugin::prelude::*;
215
216#[import]
217fn add_f64(a: f64, b: f64) -> f64;
218```
219
220Note that the specific names of the arguments do not matter, as long as they are unique. Once a function has been imported, it may be used in the plugin as any other Rust function.
221
222##### Asynchronous Functions
223To add an async native function to a plugin, use the `.host_function_async` method:
224
225```rust
226let builder = builder.host_function_async(
227    "half", 
228    |n: f64| async move { n / 2.0 },
229).unwrap();
230```
231
232This method works exactly the same as the `.host_function` method, but requires a callback that returns an async future. On the plugin side, there is no distinction made between sync and async functions (as Wasm has no built-in notion of sync vs. async), so the required import signature should *not* use the `async` keyword:
233
234```rust
235use plugin::prelude::*;
236
237#[import]
238fn half(n: f64) -> f64;
239```
240
241All functions declared by the builder must be imported by the Wasm plugin, otherwise an error will be raised.
242
243#### 3. Get the compiled plugin
244Once all imports are marked, we can instantiate the plugin. To instantiate the plugin, simply call the `.init` method on a `PluginBuilder`:
245
246```rust
247let plugin = builder
248    .init(
249        PluginBinary::Precompiled(bytes),
250    )
251    .await
252    .unwrap();
253```
254
255The `.init` method takes a single argument containing the plugin binary. 
256
2571. If not precompiled, use `PluginBinary::Wasm(bytes)`. This supports both the WebAssembly Textual format (`.wat`) and the WebAssembly Binary format (`.wasm`). 
258
2592. If precompiled, use `PluginBinary::Precompiled(bytes)`. This supports precompiled plugins ending in `.wasm.pre`. You need to be extra-careful when using precompiled plugins to ensure that the plugin target matches the target of the binary you are compiling.
260
261The `.init` method is asynchronous, and must be `.await`ed upon. If the plugin is malformed or doesn't import the right functions, an error will be raised.
262
263#### 4. Get handles to all exported functions
264Once the plugin has been compiled, it's time to start filling in the plugin struct defined earlier. In the case of `CoolPlugin` from earlier, this can be done as follows:
265
266```rust
267let mut cool_plugin = CoolPlugin {
268    format:  plugin.function("format").unwrap(),
269    process: plugin.function("process").unwrap(),
270    runtime: plugin,
271};
272```
273
274Because the struct definition defines the types of functions we're grabbing handles to, it's not required to specify the types of the functions here.
275
276Note that, yet again, the names of guest-side functions we import must match exactly. Here's an example of what that implementation might look like:
277
278```rust
279use plugin::prelude::*;
280
281#[export]
282pub fn format(message: String) -> String {
283    format!("Cool Plugin says... '{}!'", message)
284}
285
286#[export]
287pub fn process(numbers: Vec<f64>) -> f64 {
288    // Process by calculating the average
289    let mut total = 0.0;
290    for number in numbers.into_iter() {
291        total += number;
292    }
293    total / numbers.len()
294}
295```
296
297That's it! Now you have a struct that holds an instance of a plugin. The last thing you need to know is how to call out the plugin you've defined...
298
299### Using a plugin
300To call a plugin function, use the async `.call` method on `Plugin`:
301
302```rust
303let average = cool_plugin.runtime
304    .call(
305        &cool_plugin.process,
306        vec![1.0, 2.0, 3.0],
307    )
308    .await
309    .unwrap();
310```
311
312The `.call` method takes two arguments:
313
3141. A reference to the handle of the function we want to call.
3152. The input argument to this function.
316
317This method is async, and must be `.await`ed. If something goes wrong (e.g. the plugin panics, or there is a type mismatch between the plugin and `WasiFn`), then this method will return an error.
318
319## Last Notes
320This has been a brief overview of how the plugin system currently works in Zed. We hope to implement higher-level affordances as time goes on, to make writing plugins easier, and providing tooling so that users of Zed may also write plugins to extend their own editors.