Compare commits
1 Commits
master
...
plugins-sy
Author | SHA1 | Date | |
---|---|---|---|
|
ec8755d726 |
1280
Cargo.lock
generated
1280
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@ members = [
|
|||||||
"helix-tui",
|
"helix-tui",
|
||||||
"helix-syntax",
|
"helix-syntax",
|
||||||
"helix-lsp",
|
"helix-lsp",
|
||||||
|
"helix-plugin",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Build helix-syntax in release mode to make the code path faster in development.
|
# Build helix-syntax in release mode to make the code path faster in development.
|
||||||
|
14
helix-plugin/Cargo.toml
Normal file
14
helix-plugin/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "helix-plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wasmtime = "0.28"
|
||||||
|
wasmtime-wasi = { version = "0.28", features = ["tokio"] }
|
||||||
|
anyhow = "1"
|
||||||
|
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio-macros = "1.3"
|
||||||
|
tokio = "1.8"
|
16
helix-plugin/README.md
Normal file
16
helix-plugin/README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Helix plugins system MVP
|
||||||
|
|
||||||
|
This is still an early alpha, you should expect frequent breaking changes.
|
||||||
|
The intend is to take inspiration from [CodeMirror 6](https://codemirror.net/6/docs/ref/).
|
||||||
|
|
||||||
|
Plugins for Helix are written in any language that can be compiled to WASM (Web Assembly).
|
||||||
|
This implies:
|
||||||
|
- No language language lock-in, user can use what they're most comfortable with to extend and customize their experience.
|
||||||
|
- Sandboxing, a plugin can do only what it has been allowed to.
|
||||||
|
- Portability, .wasm and .wat files are platform agnostic and can be packaged as is for any platform.
|
||||||
|
|
||||||
|
API is described by .witx files and bindings for some languages can be generated by [`witx-bindgen`](https://github.com/bytecodealliance/witx-bindgen).
|
||||||
|
As of now, `witx-bindgen` is still in prototype stage.
|
||||||
|
|
||||||
|
Host side code is generated with [BLABLA TODO]
|
||||||
|
|
253
helix-plugin/src/lib.rs
Normal file
253
helix-plugin/src/lib.rs
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use wasmtime_wasi::tokio::WasiCtxBuilder;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct PluginName(String);
|
||||||
|
|
||||||
|
impl From<&str> for PluginName {
|
||||||
|
fn from(name: &str) -> Self {
|
||||||
|
Self(name.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for PluginName {
|
||||||
|
fn from(name: String) -> Self {
|
||||||
|
Self(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PluginName {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a path accessible from sandbox
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DirDef {
|
||||||
|
Mirrored {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Mapped {
|
||||||
|
host_path: PathBuf,
|
||||||
|
guest_path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirDef {
|
||||||
|
/// Preopens the directory definition for the given WASI context
|
||||||
|
pub fn preopen(&self, wasi_builder: WasiCtxBuilder) -> Result<WasiCtxBuilder> {
|
||||||
|
use std::fs::File;
|
||||||
|
use wasmtime_wasi::Dir;
|
||||||
|
|
||||||
|
let (host_path, guest_path) = match self {
|
||||||
|
DirDef::Mirrored { path } => (path.as_path(), path.as_path()),
|
||||||
|
DirDef::Mapped {
|
||||||
|
host_path,
|
||||||
|
guest_path,
|
||||||
|
} => (host_path.as_path(), guest_path.as_path()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let host_dir = unsafe {
|
||||||
|
// SAFETY: user is deciding for himself folders that should be accessibles
|
||||||
|
Dir::from_std_file(File::open(host_path)?)
|
||||||
|
};
|
||||||
|
|
||||||
|
wasi_builder.preopened_dir(host_dir, guest_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HelixCtx {
|
||||||
|
wasi: wasmtime_wasi::WasiCtx,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type HelixStore = wasmtime::Store<HelixCtx>;
|
||||||
|
|
||||||
|
pub struct PluginDef {
|
||||||
|
pub name: PluginName,
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub dependencies: Vec<PluginName>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Plugin {
|
||||||
|
instance: wasmtime::Instance,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin {
|
||||||
|
pub fn get_typed_func<Params, Results>(
|
||||||
|
&self,
|
||||||
|
store: &mut HelixStore,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<wasmtime::TypedFunc<Params, Results>>
|
||||||
|
where
|
||||||
|
Params: wasmtime::WasmParams,
|
||||||
|
Results: wasmtime::WasmResults,
|
||||||
|
{
|
||||||
|
let func = self
|
||||||
|
.instance
|
||||||
|
.get_typed_func::<Params, Results, _>(store, name)?;
|
||||||
|
Ok(func)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_func(&self, store: &mut HelixStore, name: &str) -> Option<wasmtime::Func> {
|
||||||
|
self.instance.get_func(store, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PluginsSystem {
|
||||||
|
pub store: HelixStore,
|
||||||
|
pub plugins: HashMap<PluginName, Plugin>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginsSystem {
|
||||||
|
pub fn builder() -> PluginsSystemBuilder {
|
||||||
|
PluginsSystemBuilder::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PluginsSystemBuilder {
|
||||||
|
debug_info: bool,
|
||||||
|
definitions: Vec<PluginDef>,
|
||||||
|
preopened_dirs: Vec<DirDef>,
|
||||||
|
linker_fn: Box<dyn Fn(&mut wasmtime::Linker<HelixCtx>) -> Result<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PluginsSystemBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
debug_info: false,
|
||||||
|
definitions: vec![],
|
||||||
|
preopened_dirs: vec![],
|
||||||
|
linker_fn: Box::new(|_| Ok(())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginsSystemBuilder {
|
||||||
|
pub fn plugin(&mut self, def: PluginDef) -> &mut Self {
|
||||||
|
self.definitions.push(def);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plugins(&mut self, mut defs: Vec<PluginDef>) -> &mut Self {
|
||||||
|
self.definitions.append(&mut defs);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dir(&mut self, dir: DirDef) -> &mut Self {
|
||||||
|
self.preopened_dirs.push(dir);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dirs(&mut self, mut dirs: Vec<DirDef>) -> &mut Self {
|
||||||
|
self.preopened_dirs.append(&mut dirs);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn debug_info(&mut self, debug: bool) -> &mut Self {
|
||||||
|
self.debug_info = debug;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn linker<F>(&mut self, linker_fn: F) -> &mut Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut wasmtime::Linker<HelixCtx>) -> Result<()> + 'static,
|
||||||
|
{
|
||||||
|
self.linker_fn = Box::new(linker_fn);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Instanciate the plugins system, compiling and linking WASM modules as appropriate.
|
||||||
|
pub async fn build(&self) -> Result<PluginsSystem> {
|
||||||
|
use wasmtime::{Config, Engine, Linker, Module, Store};
|
||||||
|
|
||||||
|
let mut config = Config::new();
|
||||||
|
config.debug_info(self.debug_info);
|
||||||
|
config.async_support(true);
|
||||||
|
|
||||||
|
let engine = Engine::new(&config)?;
|
||||||
|
|
||||||
|
// Compile plugins
|
||||||
|
let modules: HashMap<PluginName, Module> = self
|
||||||
|
.definitions
|
||||||
|
.iter()
|
||||||
|
.map(|def| {
|
||||||
|
println!("Compile {}", def.name);
|
||||||
|
Module::from_file(&engine, &def.path)
|
||||||
|
.with_context(|| format!("module creation failed for `{}`", def.name))
|
||||||
|
.map(|module| (def.name.clone(), module))
|
||||||
|
})
|
||||||
|
.collect::<Result<HashMap<PluginName, Module>>>()?;
|
||||||
|
|
||||||
|
// Dumb link order resolution: a good one would detect cycles to give a better error, in
|
||||||
|
// our case a link error will arise at link time
|
||||||
|
let mut link_order = Vec::new();
|
||||||
|
for def in &self.definitions {
|
||||||
|
let insert_pos = if let Some(pos) = link_order.iter().position(|name| name == &def.name)
|
||||||
|
{
|
||||||
|
pos
|
||||||
|
} else {
|
||||||
|
link_order.push(def.name.clone());
|
||||||
|
link_order.len() - 1
|
||||||
|
};
|
||||||
|
|
||||||
|
for dep in &def.dependencies {
|
||||||
|
if let Some(pos) = link_order.iter().position(|name| name == dep) {
|
||||||
|
if pos > insert_pos {
|
||||||
|
link_order.remove(pos);
|
||||||
|
link_order.insert(insert_pos, dep.clone());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
link_order.insert(insert_pos, dep.clone());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link and create instances
|
||||||
|
let mut wasi_builder = WasiCtxBuilder::new();
|
||||||
|
for dir in &self.preopened_dirs {
|
||||||
|
wasi_builder = dir
|
||||||
|
.preopen(wasi_builder)
|
||||||
|
.with_context(|| format!("couldn't preopen directory `{:?}`", dir))?;
|
||||||
|
}
|
||||||
|
let wasi = wasi_builder.build();
|
||||||
|
|
||||||
|
let ctx = HelixCtx { wasi };
|
||||||
|
|
||||||
|
let mut store = Store::new(&engine, ctx);
|
||||||
|
|
||||||
|
let mut linker = Linker::new(&engine);
|
||||||
|
wasmtime_wasi::add_to_linker(&mut linker, |s: &mut HelixCtx| &mut s.wasi)?;
|
||||||
|
|
||||||
|
(self.linker_fn)(&mut linker).context("couldn't add host-provided modules to linker")?;
|
||||||
|
|
||||||
|
let mut plugins: HashMap<PluginName, Plugin> = HashMap::new();
|
||||||
|
|
||||||
|
for name in link_order {
|
||||||
|
let module = modules.get(&name).expect("this module was compiled above");
|
||||||
|
|
||||||
|
let instance = linker
|
||||||
|
.instantiate_async(&mut store, module)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("couldn't instanciate `{}`", name))?;
|
||||||
|
|
||||||
|
// Register the instance with the linker for the next linking
|
||||||
|
linker.instance(&mut store, &name.0, instance)?;
|
||||||
|
|
||||||
|
plugins.insert(name, Plugin { instance });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call `init` function on all loaded plugins
|
||||||
|
for plugin in plugins.values_mut() {
|
||||||
|
// Call this plugin's `init` function if one is defined
|
||||||
|
if let Ok(func) = plugin.get_typed_func::<(), ()>(&mut store, "init") {
|
||||||
|
func.call_async(&mut store, ()).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PluginsSystem { store, plugins })
|
||||||
|
}
|
||||||
|
}
|
7
helix-plugin/tests/call_host.wat
Normal file
7
helix-plugin/tests/call_host.wat
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
(module
|
||||||
|
(import "host" "callback" (func $callback))
|
||||||
|
|
||||||
|
(func (export "init")
|
||||||
|
call $callback
|
||||||
|
)
|
||||||
|
)
|
88
helix-plugin/tests/linking.rs
Normal file
88
helix-plugin/tests/linking.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use helix_plugin::{DirDef, PluginDef, PluginName, PluginsSystem};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Based on https://docs.wasmtime.dev/examples-rust-linking.html
|
||||||
|
#[tokio_macros::test]
|
||||||
|
async fn two_plugins() {
|
||||||
|
let linking1 = PluginName::from("linking1");
|
||||||
|
let linking2 = PluginName::from("linking2");
|
||||||
|
|
||||||
|
let mut system = PluginsSystem::builder()
|
||||||
|
.plugin(PluginDef {
|
||||||
|
name: linking1.clone(),
|
||||||
|
path: PathBuf::from("tests/linking1.wat"),
|
||||||
|
dependencies: vec![linking2.clone()],
|
||||||
|
})
|
||||||
|
.plugin(PluginDef {
|
||||||
|
name: linking2.clone(),
|
||||||
|
path: PathBuf::from("tests/linking2.wat"),
|
||||||
|
dependencies: vec![],
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let run = system
|
||||||
|
.plugins
|
||||||
|
.get(&linking1)
|
||||||
|
.unwrap()
|
||||||
|
.get_typed_func::<(), ()>(&mut system.store, "run")
|
||||||
|
.unwrap();
|
||||||
|
run.call_async(&mut system.store, ()).await.unwrap();
|
||||||
|
|
||||||
|
let double = system
|
||||||
|
.plugins
|
||||||
|
.get(&linking2)
|
||||||
|
.unwrap()
|
||||||
|
.get_typed_func::<i32, i32>(&mut system.store, "double")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(double.call_async(&mut system.store, 5).await.unwrap(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio_macros::test(flavor = "multi_thread")]
|
||||||
|
async fn wasi_preopen_dir() {
|
||||||
|
let name = PluginName::from("read_file");
|
||||||
|
|
||||||
|
// read_file.wasm is a program reading `./Cargo.toml` file and does nothing with it
|
||||||
|
|
||||||
|
PluginsSystem::builder()
|
||||||
|
.dir(DirDef::Mirrored {
|
||||||
|
path: PathBuf::from("./"),
|
||||||
|
})
|
||||||
|
.plugin(PluginDef {
|
||||||
|
name: name.clone(),
|
||||||
|
path: PathBuf::from("tests/read_file.wasm"),
|
||||||
|
dependencies: vec![],
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio_macros::test]
|
||||||
|
async fn callback_to_host() {
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
static CANARY: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
PluginsSystem::builder()
|
||||||
|
.plugin(PluginDef {
|
||||||
|
name: PluginName::from("call_host"),
|
||||||
|
path: PathBuf::from("tests/call_host.wat"),
|
||||||
|
dependencies: vec![],
|
||||||
|
})
|
||||||
|
.linker(|l| {
|
||||||
|
l.func_wrap("host", "callback", || CANARY.store(true, Ordering::Relaxed))?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(CANARY.load(Ordering::Relaxed), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio_macros::test]
|
||||||
|
async fn callback_to_content() {
|
||||||
|
todo!()
|
||||||
|
}
|
24
helix-plugin/tests/linking1.wat
Normal file
24
helix-plugin/tests/linking1.wat
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
;; from https://docs.wasmtime.dev/examples-rust-linking.html
|
||||||
|
|
||||||
|
(module
|
||||||
|
(import "linking2" "double" (func $double (param i32) (result i32)))
|
||||||
|
(import "linking2" "log" (func $log (param i32 i32)))
|
||||||
|
(import "linking2" "memory" (memory 1))
|
||||||
|
(import "linking2" "memory_offset" (global $offset i32))
|
||||||
|
|
||||||
|
(func (export "run")
|
||||||
|
;; Call into the other module to double our number, and we could print it
|
||||||
|
;; here but for now we just drop it
|
||||||
|
i32.const 2
|
||||||
|
call $double
|
||||||
|
drop
|
||||||
|
|
||||||
|
;; Our `data` segment initialized our imported memory, so let's print the
|
||||||
|
;; string there now.
|
||||||
|
global.get $offset
|
||||||
|
i32.const 14
|
||||||
|
call $log
|
||||||
|
)
|
||||||
|
|
||||||
|
(data (global.get $offset) "Hello, world!\n")
|
||||||
|
)
|
36
helix-plugin/tests/linking2.wat
Normal file
36
helix-plugin/tests/linking2.wat
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
;; from https://docs.wasmtime.dev/examples-rust-linking.html
|
||||||
|
|
||||||
|
(module
|
||||||
|
(type $fd_write_ty (func (param i32 i32 i32 i32) (result i32)))
|
||||||
|
(import "wasi_snapshot_preview1" "fd_write" (func $fd_write (type $fd_write_ty)))
|
||||||
|
|
||||||
|
(func (export "double") (param i32) (result i32)
|
||||||
|
local.get 0
|
||||||
|
i32.const 2
|
||||||
|
i32.mul
|
||||||
|
)
|
||||||
|
|
||||||
|
(func (export "log") (param i32 i32)
|
||||||
|
;; store the pointer in the first iovec field
|
||||||
|
i32.const 4
|
||||||
|
local.get 0
|
||||||
|
i32.store
|
||||||
|
|
||||||
|
;; store the length in the first iovec field
|
||||||
|
i32.const 4
|
||||||
|
local.get 1
|
||||||
|
i32.store offset=4
|
||||||
|
|
||||||
|
;; call the `fd_write` import
|
||||||
|
i32.const 1 ;; stdout fd
|
||||||
|
i32.const 4 ;; iovs start
|
||||||
|
i32.const 1 ;; number of iovs
|
||||||
|
i32.const 0 ;; where to write nwritten bytes
|
||||||
|
call $fd_write
|
||||||
|
drop
|
||||||
|
)
|
||||||
|
|
||||||
|
(memory (export "memory") 2)
|
||||||
|
(global (export "memory_offset") i32 (i32.const 65536))
|
||||||
|
)
|
||||||
|
|
BIN
helix-plugin/tests/read_file.wasm
Normal file
BIN
helix-plugin/tests/read_file.wasm
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user