1pub(crate) mod wit;
2
3use crate::ExtensionManifest;
4use anyhow::{anyhow, bail, Context as _, Result};
5use fs::{normalize_path, Fs};
6use futures::{
7 channel::{
8 mpsc::{self, UnboundedSender},
9 oneshot,
10 },
11 future::BoxFuture,
12 Future, FutureExt, StreamExt as _,
13};
14use gpui::BackgroundExecutor;
15use language::LanguageRegistry;
16use node_runtime::NodeRuntime;
17use semantic_version::SemanticVersion;
18use std::{
19 path::{Path, PathBuf},
20 sync::{Arc, OnceLock},
21};
22use util::http::HttpClient;
23use wasmtime::{
24 component::{Component, ResourceTable},
25 Engine, Store,
26};
27use wasmtime_wasi as wasi;
28use wit::Extension;
29
30pub(crate) struct WasmHost {
31 engine: Engine,
32 http_client: Arc<dyn HttpClient>,
33 node_runtime: Arc<dyn NodeRuntime>,
34 pub(crate) language_registry: Arc<LanguageRegistry>,
35 fs: Arc<dyn Fs>,
36 pub(crate) work_dir: PathBuf,
37}
38
39#[derive(Clone)]
40pub struct WasmExtension {
41 tx: UnboundedSender<ExtensionCall>,
42 pub(crate) manifest: Arc<ExtensionManifest>,
43 #[allow(unused)]
44 pub zed_api_version: SemanticVersion,
45}
46
47pub(crate) struct WasmState {
48 manifest: Arc<ExtensionManifest>,
49 pub(crate) table: ResourceTable,
50 ctx: wasi::WasiCtx,
51 pub(crate) host: Arc<WasmHost>,
52}
53
54type ExtensionCall = Box<
55 dyn Send + for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, ()>,
56>;
57
58fn wasm_engine() -> wasmtime::Engine {
59 static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
60
61 WASM_ENGINE
62 .get_or_init(|| {
63 let mut config = wasmtime::Config::new();
64 config.wasm_component_model(true);
65 config.async_support(true);
66 wasmtime::Engine::new(&config).unwrap()
67 })
68 .clone()
69}
70
71impl WasmHost {
72 pub fn new(
73 fs: Arc<dyn Fs>,
74 http_client: Arc<dyn HttpClient>,
75 node_runtime: Arc<dyn NodeRuntime>,
76 language_registry: Arc<LanguageRegistry>,
77 work_dir: PathBuf,
78 ) -> Arc<Self> {
79 Arc::new(Self {
80 engine: wasm_engine(),
81 fs,
82 work_dir,
83 http_client,
84 node_runtime,
85 language_registry,
86 })
87 }
88
89 pub fn load_extension(
90 self: &Arc<Self>,
91 wasm_bytes: Vec<u8>,
92 manifest: Arc<ExtensionManifest>,
93 executor: BackgroundExecutor,
94 ) -> impl 'static + Future<Output = Result<WasmExtension>> {
95 let this = self.clone();
96 async move {
97 let zed_api_version = parse_wasm_extension_version(&manifest.id, &wasm_bytes)?;
98
99 let component = Component::from_binary(&this.engine, &wasm_bytes)
100 .context("failed to compile wasm component")?;
101
102 let mut store = wasmtime::Store::new(
103 &this.engine,
104 WasmState {
105 ctx: this.build_wasi_ctx(&manifest).await?,
106 manifest: manifest.clone(),
107 table: ResourceTable::new(),
108 host: this.clone(),
109 },
110 );
111
112 let (mut extension, instance) =
113 Extension::instantiate_async(&mut store, zed_api_version, &component).await?;
114
115 extension
116 .call_init_extension(&mut store)
117 .await
118 .context("failed to initialize wasm extension")?;
119
120 let (tx, mut rx) = mpsc::unbounded::<ExtensionCall>();
121 executor
122 .spawn(async move {
123 let _instance = instance;
124 while let Some(call) = rx.next().await {
125 (call)(&mut extension, &mut store).await;
126 }
127 })
128 .detach();
129
130 Ok(WasmExtension {
131 manifest,
132 tx,
133 zed_api_version,
134 })
135 }
136 }
137
138 async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<wasi::WasiCtx> {
139 use cap_std::{ambient_authority, fs::Dir};
140
141 let extension_work_dir = self.work_dir.join(manifest.id.as_ref());
142 self.fs
143 .create_dir(&extension_work_dir)
144 .await
145 .context("failed to create extension work dir")?;
146
147 let work_dir_preopen = Dir::open_ambient_dir(&extension_work_dir, ambient_authority())
148 .context("failed to preopen extension work directory")?;
149 let current_dir_preopen = work_dir_preopen
150 .try_clone()
151 .context("failed to preopen extension current directory")?;
152 let extension_work_dir = extension_work_dir.to_string_lossy();
153
154 let perms = wasi::FilePerms::all();
155 let dir_perms = wasi::DirPerms::all();
156
157 Ok(wasi::WasiCtxBuilder::new()
158 .inherit_stdio()
159 .preopened_dir(current_dir_preopen, dir_perms, perms, ".")
160 .preopened_dir(work_dir_preopen, dir_perms, perms, &extension_work_dir)
161 .env("PWD", &extension_work_dir)
162 .env("RUST_BACKTRACE", "full")
163 .build())
164 }
165
166 pub fn path_from_extension(&self, id: &Arc<str>, path: &Path) -> PathBuf {
167 let extension_work_dir = self.work_dir.join(id.as_ref());
168 normalize_path(&extension_work_dir.join(path))
169 }
170
171 pub fn writeable_path_from_extension(&self, id: &Arc<str>, path: &Path) -> Result<PathBuf> {
172 let extension_work_dir = self.work_dir.join(id.as_ref());
173 let path = normalize_path(&extension_work_dir.join(path));
174 if path.starts_with(&extension_work_dir) {
175 Ok(path)
176 } else {
177 Err(anyhow!("cannot write to path {}", path.display()))
178 }
179 }
180}
181
182pub fn parse_wasm_extension_version(
183 extension_id: &str,
184 wasm_bytes: &[u8],
185) -> Result<SemanticVersion> {
186 let mut version = None;
187
188 for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
189 if let wasmparser::Payload::CustomSection(s) =
190 part.context("error parsing wasm extension")?
191 {
192 if s.name() == "zed:api-version" {
193 version = parse_wasm_extension_version_custom_section(s.data());
194 if version.is_none() {
195 bail!(
196 "extension {} has invalid zed:api-version section: {:?}",
197 extension_id,
198 s.data()
199 );
200 }
201 }
202 }
203 }
204
205 // The reason we wait until we're done parsing all of the Wasm bytes to return the version
206 // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid.
207 //
208 // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem
209 // earlier as an `Err` rather than as a panic.
210 version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id))
211}
212
213fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVersion> {
214 if data.len() == 6 {
215 Some(SemanticVersion::new(
216 u16::from_be_bytes([data[0], data[1]]) as _,
217 u16::from_be_bytes([data[2], data[3]]) as _,
218 u16::from_be_bytes([data[4], data[5]]) as _,
219 ))
220 } else {
221 None
222 }
223}
224
225impl WasmExtension {
226 pub async fn call<T, Fn>(&self, f: Fn) -> T
227 where
228 T: 'static + Send,
229 Fn: 'static
230 + Send
231 + for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, T>,
232 {
233 let (return_tx, return_rx) = oneshot::channel();
234 self.tx
235 .clone()
236 .unbounded_send(Box::new(move |extension, store| {
237 async {
238 let result = f(extension, store).await;
239 return_tx.send(result).ok();
240 }
241 .boxed()
242 }))
243 .expect("wasm extension channel should not be closed yet");
244 return_rx.await.expect("wasm extension channel")
245 }
246}
247
248impl WasmState {
249 fn work_dir(&self) -> PathBuf {
250 self.host.work_dir.join(self.manifest.id.as_ref())
251 }
252}
253
254impl wasi::WasiView for WasmState {
255 fn table(&mut self) -> &mut ResourceTable {
256 &mut self.table
257 }
258
259 fn ctx(&mut self) -> &mut wasi::WasiCtx {
260 &mut self.ctx
261 }
262}