diff --git a/crates/plugin_runtime/build.rs b/crates/plugin_runtime/build.rs index 6d792e6ed7..d1b1b58411 100644 --- a/crates/plugin_runtime/build.rs +++ b/crates/plugin_runtime/build.rs @@ -67,13 +67,9 @@ fn main() { } /// Creates a default engine for compiling Wasm. -/// N.B.: this must create the same `Engine` as -/// the `create_default_engine` function -/// in `plugin_runtime/src/plugin.rs`. fn create_default_engine() -> Engine { let mut config = Config::default(); config.async_support(true); - // config.epoch_interruption(true); Engine::new(&config).expect("Could not create engine") } diff --git a/crates/plugin_runtime/src/lib.rs b/crates/plugin_runtime/src/lib.rs index 555d925874..b008a98c28 100644 --- a/crates/plugin_runtime/src/lib.rs +++ b/crates/plugin_runtime/src/lib.rs @@ -23,7 +23,7 @@ mod tests { } async { - let mut runtime = PluginBuilder::new_with_default_ctx() + let mut runtime = PluginBuilder::new_fuel_with_default_ctx(PluginYield::default_fuel()) .unwrap() .host_function("mystery_number", |input: u32| input + 7) .unwrap() @@ -46,10 +46,9 @@ mod tests { .map(|output| output.stdout) }) .unwrap() - .init( - false, - include_bytes!("../../../plugins/bin/test_plugin.wasm"), - ) + .init(PluginBinary::Wasm( + include_bytes!("../../../plugins/bin/test_plugin.wasm").as_ref(), + )) .await .unwrap(); diff --git a/crates/plugin_runtime/src/plugin.rs b/crates/plugin_runtime/src/plugin.rs index d2570410f9..2767d9c4b8 100644 --- a/crates/plugin_runtime/src/plugin.rs +++ b/crates/plugin_runtime/src/plugin.rs @@ -1,5 +1,6 @@ use std::future::Future; +use std::time::Duration; use std::{fs::File, marker::PhantomData, path::Path}; use anyhow::{anyhow, Error}; @@ -54,6 +55,40 @@ impl Clone for WasiFn { } } +pub struct PluginYieldEpoch { + delta: u64, + epoch: std::time::Duration, +} + +pub struct PluginYieldFuel { + initial: u64, + refill: u64, +} + +pub enum PluginYield { + Epoch { + yield_epoch: PluginYieldEpoch, + initialize_incrementer: Box () + Send>, + }, + Fuel(PluginYieldFuel), +} + +impl PluginYield { + pub fn default_epoch() -> PluginYieldEpoch { + PluginYieldEpoch { + delta: 1, + epoch: Duration::from_millis(1), + } + } + + pub fn default_fuel() -> PluginYieldFuel { + PluginYieldFuel { + initial: 1000, + refill: 1000, + } + } +} + /// This struct is used to build a new [`Plugin`], using the builder pattern. /// Create a new default plugin with `PluginBuilder::new_with_default_ctx`, /// and add host-side exported functions using `host_function` and `host_function_async`. @@ -62,42 +97,110 @@ pub struct PluginBuilder { wasi_ctx: WasiCtx, engine: Engine, linker: Linker, -} - -/// Creates a default engine for compiling Wasm. -/// N.B.: this must create the same `Engine` as -/// the `create_default_engine` function -/// in `plugin_runtime/build.rs`. -pub fn create_default_engine() -> Result { - let mut config = Config::default(); - config.async_support(true); - // config.epoch_interruption(true); - Engine::new(&config) + yield_when: PluginYield, } impl PluginBuilder { + /// Creates an engine with the proper configuration given the yield mechanism in use + fn create_engine(yield_when: &PluginYield) -> Result<(Engine, Linker), Error> { + let mut config = Config::default(); + config.async_support(true); + + match yield_when { + PluginYield::Epoch { .. } => { + config.epoch_interruption(true); + } + PluginYield::Fuel(_) => { + config.consume_fuel(true); + } + } + + let engine = Engine::new(&config)?; + let linker = Linker::new(&engine); + Ok((engine, linker)) + } + /// Create a new [`PluginBuilder`] with the given WASI context. /// Using the default context is a safe bet, see [`new_with_default_context`]. - pub fn new(wasi_ctx: WasiCtx) -> Result { - let engine = create_default_engine()?; - let linker = Linker::new(&engine); + /// This plugin will yield after each fixed configurable epoch. + pub fn new_epoch( + wasi_ctx: WasiCtx, + yield_epoch: PluginYieldEpoch, + spawn_detached_future: C, + ) -> Result + where + C: FnOnce(std::pin::Pin + Send + 'static>>) -> () + + Send + + 'static, + { + // we can't create the future until after initializing + // because we need the engine to load the plugin + let epoch = yield_epoch.epoch; + let initialize_incrementer = Box::new(move |engine: Engine| { + spawn_detached_future(Box::pin(async move { + loop { + smol::Timer::after(epoch).await; + engine.increment_epoch(); + } + })) + }); + + let yield_when = PluginYield::Epoch { + yield_epoch, + initialize_incrementer, + }; + let (engine, linker) = Self::create_engine(&yield_when)?; Ok(PluginBuilder { - // host_functions: HashMap::new(), wasi_ctx, engine, linker, + yield_when, }) } - /// Create a new `PluginBuilder` that inherits the + /// Create a new [`PluginBuilder`] with the given WASI context. + /// Using the default context is a safe bet, see [`new_with_default_context`]. + /// This plugin will yield after a configurable amount of fuel is consumed. + pub fn new_fuel(wasi_ctx: WasiCtx, yield_fuel: PluginYieldFuel) -> Result { + let yield_when = PluginYield::Fuel(yield_fuel); + let (engine, linker) = Self::create_engine(&yield_when)?; + + Ok(PluginBuilder { + wasi_ctx, + engine, + linker, + yield_when, + }) + } + + /// Create a new `WasiCtx` that inherits the /// host processes' access to `stdout` and `stderr`. - pub fn new_with_default_ctx() -> Result { - let wasi_ctx = WasiCtxBuilder::new() + fn default_ctx() -> WasiCtx { + WasiCtxBuilder::new() .inherit_stdout() .inherit_stderr() - .build(); - Self::new(wasi_ctx) + .build() + } + + /// Create a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]). + /// This plugin will yield after each fixed configurable epoch. + pub fn new_epoch_with_default_ctx( + yield_epoch: PluginYieldEpoch, + spawn_detached_future: C, + ) -> Result + where + C: FnOnce(std::pin::Pin + Send + 'static>>) -> () + + Send + + 'static, + { + Self::new_epoch(Self::default_ctx(), yield_epoch, spawn_detached_future) + } + + /// Create a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]). + /// This plugin will yield after a configurable amount of fuel is consumed. + pub fn new_fuel_with_default_ctx(yield_fuel: PluginYieldFuel) -> Result { + Self::new_fuel(Self::default_ctx(), yield_fuel) } /// Add an `async` host function. See [`host_function`] for details. @@ -242,8 +345,8 @@ impl PluginBuilder { /// Initializes a [`Plugin`] from a given compiled Wasm module. /// Both binary (`.wasm`) and text (`.wat`) module formats are supported. - pub async fn init>(self, precompiled: bool, module: T) -> Result { - Plugin::init(precompiled, module.as_ref().to_vec(), self).await + pub async fn init<'a>(self, binary: PluginBinary<'a>) -> Result { + Plugin::init(binary, self).await } } @@ -276,6 +379,11 @@ impl WasiCtxAlloc { } } +pub enum PluginBinary<'a> { + Wasm(&'a [u8]), + Precompiled(&'a [u8]), +} + /// Represents a WebAssembly plugin, with access to the WebAssembly System Inferface. /// Build a new plugin using [`PluginBuilder`]. pub struct Plugin { @@ -303,11 +411,7 @@ impl Plugin { println!(); } - async fn init( - precompiled: bool, - module: Vec, - plugin: PluginBuilder, - ) -> Result { + async fn init<'a>(binary: PluginBinary<'a>, plugin: PluginBuilder) -> Result { // initialize the WebAssembly System Interface context let engine = plugin.engine; let mut linker = plugin.linker; @@ -322,13 +426,27 @@ impl Plugin { alloc: None, }, ); - // store.epoch_deadline_async_yield_and_update(todo!()); - let module = if precompiled { - unsafe { Module::deserialize(&engine, module)? } - } else { - Module::new(&engine, module)? + + let module = match binary { + PluginBinary::Precompiled(bytes) => unsafe { Module::deserialize(&engine, bytes)? }, + PluginBinary::Wasm(bytes) => Module::new(&engine, bytes)?, }; + // set up automatic yielding based on configuration + match plugin.yield_when { + PluginYield::Epoch { + yield_epoch: PluginYieldEpoch { delta, .. }, + initialize_incrementer, + } => { + store.epoch_deadline_async_yield_and_update(delta); + initialize_incrementer(engine); + } + PluginYield::Fuel(PluginYieldFuel { initial, refill }) => { + store.add_fuel(initial).unwrap(); + store.out_of_fuel_async_yield(u64::MAX, refill); + } + } + // load the provided module into the asynchronous runtime linker.module_async(&mut store, "", &module).await?; let instance = linker.instantiate_async(&mut store, &module).await?; diff --git a/crates/zed/src/languages/language_plugin.rs b/crates/zed/src/languages/language_plugin.rs index 773cc16b97..6f4bdab78e 100644 --- a/crates/zed/src/languages/language_plugin.rs +++ b/crates/zed/src/languages/language_plugin.rs @@ -5,12 +5,16 @@ use collections::HashMap; use futures::lock::Mutex; use gpui::executor::Background; use language::{LanguageServerName, LspAdapter}; -use plugin_runtime::{Plugin, PluginBuilder, WasiFn}; +use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, PluginYield, WasiFn}; use std::{any::Any, path::PathBuf, sync::Arc}; use util::ResultExt; pub async fn new_json(executor: Arc) -> Result { - let plugin = PluginBuilder::new_with_default_ctx()? + let executor_ref = executor.clone(); + let plugin = + PluginBuilder::new_epoch_with_default_ctx(PluginYield::default_epoch(), move |future| { + executor_ref.spawn(future).detach() + })? .host_function_async("command", |command: String| async move { let mut args = command.split(' '); let command = args.next().unwrap(); @@ -21,11 +25,11 @@ pub async fn new_json(executor: Arc) -> Result { .log_err() .map(|output| output.stdout) })? - .init( - true, - include_bytes!("../../../../plugins/bin/json_language.wasm.pre"), - ) + .init(PluginBinary::Precompiled(include_bytes!( + "../../../../plugins/bin/json_language.wasm.pre" + ))) .await?; + PluginLspAdapter::new(plugin, executor).await }