arvalez_plugin_runtime/
lib.rs1use std::{
2 fs,
3 path::{Path, PathBuf},
4};
5
6use anyhow::{Context, Result};
7use arvalez_ir::{
8 CoreIr, PluginContextEnvelope, PluginRequest, PluginResponse, Target, validate_ir,
9};
10use serde_json::Value;
11use tempfile::tempdir;
12use wasmtime::{Engine, Linker, Module, Store};
13use wasmtime_wasi::{DirPerms, FilePerms, WasiCtx};
14
15#[derive(Debug, Clone)]
16pub struct WasmPluginDefinition {
17 pub path: PathBuf,
18 pub options: Value,
19}
20
21pub struct WasmPluginRunner {
22 engine: Engine,
23}
24
25impl WasmPluginRunner {
26 pub fn new() -> Result<Self> {
27 Ok(Self {
28 engine: Engine::default(),
29 })
30 }
31
32 pub fn run(
33 &self,
34 plugin_name: &str,
35 definition: &WasmPluginDefinition,
36 target: Option<Target>,
37 ir: &CoreIr,
38 ) -> Result<PluginResponse> {
39 validate_ir(ir).context("refusing to send invalid IR to plugin")?;
40
41 let plugin_path = canonicalize(&definition.path)?;
42 let workspace = tempdir().context("failed to create plugin workspace")?;
43 let request_path = workspace.path().join("request.json");
44 let response_path = workspace.path().join("response.json");
45
46 let request = PluginRequest {
47 context: PluginContextEnvelope {
48 plugin_name: plugin_name.to_owned(),
49 target,
50 options: definition.options.clone(),
51 },
52 ir: ir.clone(),
53 };
54
55 let request_bytes =
56 serde_json::to_vec_pretty(&request).context("failed to serialize plugin request")?;
57 fs::write(&request_path, request_bytes).with_context(|| {
58 format!("failed to write request file `{}`", request_path.display())
59 })?;
60
61 let module = Module::from_file(&self.engine, &plugin_path)
62 .with_context(|| format!("failed to load plugin module `{}`", plugin_path.display()))?;
63
64 let mut linker = Linker::new(&self.engine);
65 wasmtime_wasi::p1::add_to_linker_sync(&mut linker, |wasi| wasi)
66 .context("failed to add WASI functions to linker")?;
67
68 let mut wasi = WasiCtx::builder();
69 wasi.arg(plugin_name);
70 wasi.env("ARVALEZ_REQUEST_PATH", "request.json");
71 wasi.env("ARVALEZ_RESPONSE_PATH", "response.json");
72 wasi.preopened_dir(workspace.path(), ".", DirPerms::all(), FilePerms::all())
73 .context("failed to preopen plugin workspace")?;
74
75 let mut store = Store::new(&self.engine, wasi.build_p1());
76 linker
77 .module(&mut store, "", &module)
78 .context("failed to instantiate plugin module")?;
79 let start = linker
80 .get_default(&mut store, "")
81 .context("failed to locate plugin entrypoint")?
82 .typed::<(), ()>(&store)
83 .context("failed to type-check plugin entrypoint")?;
84 start
85 .call(&mut store, ())
86 .context("plugin execution trapped")?;
87
88 let response_bytes = fs::read(&response_path).with_context(|| {
89 format!(
90 "plugin did not write a response file at `{}`",
91 response_path.display()
92 )
93 })?;
94 let response: PluginResponse = serde_json::from_slice(&response_bytes)
95 .context("failed to deserialize plugin response")?;
96 validate_ir(&response.ir).context("plugin returned invalid IR")?;
97
98 Ok(response)
99 }
100}
101
102fn canonicalize(path: &Path) -> Result<PathBuf> {
103 fs::canonicalize(path)
104 .with_context(|| format!("failed to resolve plugin path `{}`", path.display()))
105}