Skip to main content

arvalez_plugin_runtime/
lib.rs

1use 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}