libsurfer/translation/
wasm_translator.rs1use std::ffi::OsString;
2use std::fs::read_dir;
3use std::path::PathBuf;
4use std::sync::{Arc, Mutex};
5
6use camino::Utf8PathBuf;
7use color_eyre::eyre::{anyhow, Context};
8use directories::ProjectDirs;
9use extism::{host_fn, Manifest, Plugin, PluginBuilder, Wasm, PTR};
10use extism_manifest::MemoryOptions;
11use log::{error, warn};
12use surfer_translation_types::plugin_types::TranslateParams;
13use surfer_translation_types::{
14 TranslationPreference, TranslationResult, Translator, VariableInfo, VariableMeta, VariableValue,
15};
16
17use crate::message::Message;
18use crate::wave_container::{ScopeId, VarId};
19
20pub fn discover_wasm_translators() -> Vec<Message> {
21 let search_dirs = [
22 std::env::current_dir()
23 .ok()
24 .map(|dir| dir.join(".surfer").join("translators")),
25 ProjectDirs::from("org", "surfer-project", "surfer")
26 .map(|dirs| dirs.data_dir().join("translators")),
27 ]
28 .into_iter()
29 .filter_map(|x| x)
30 .collect::<Vec<_>>();
31
32 let plugin_files = search_dirs
33 .into_iter()
34 .map(|dir| {
35 if !dir.exists() {
36 return vec![];
37 }
38 read_dir(&dir)
39 .map(|readdir| {
40 readdir
41 .filter_map(|entry| match entry {
42 Ok(entry) => {
43 let path = entry.path();
44 if path.extension() == Some(&OsString::from("wasm")) {
45 Some(path)
46 } else {
47 None
48 }
49 }
50 Err(e) => {
51 warn!("Failed to read entry in {:?}. {e}", dir.to_string_lossy());
52 None
53 }
54 })
55 .collect::<Vec<_>>()
56 })
57 .map_err(|e| {
58 warn!(
59 "Failed to read dir entries in {}. {e}",
60 dir.to_string_lossy()
61 )
62 })
63 .unwrap_or_else(|_| vec![])
64 })
65 .flatten()
66 .filter_map(|file| {
67 file.clone()
68 .try_into()
69 .map_err(|_| {
70 format!(
71 "{} is not a valid UTF8 path, ignoring this translator",
72 file.to_string_lossy()
73 )
74 })
75 .ok()
76 });
77
78 plugin_files
79 .map(|file| Message::LoadWasmTranslator(file))
80 .collect()
81}
82
83pub struct PluginTranslator {
84 plugin: Arc<Mutex<Plugin>>,
85 file: PathBuf,
86}
87
88impl PluginTranslator {
89 pub fn new(file: PathBuf) -> color_eyre::Result<Self> {
90 let data = std::fs::read(&file)
91 .with_context(|| format!("Failed to read {}", file.to_string_lossy()))?;
92
93 let manifest = Manifest::new([Wasm::data(data)])
94 .with_memory_options(MemoryOptions::new().with_max_var_bytes(1024 * 1024 * 10));
95 let mut plugin = PluginBuilder::new(manifest)
96 .with_function(
97 "read_file",
98 [PTR],
99 [PTR],
100 extism::UserData::new(()),
101 read_file,
102 )
103 .with_function(
104 "file_exists",
105 [PTR],
106 [PTR],
107 extism::UserData::new(()),
108 file_exists,
109 )
110 .build()
111 .map_err(|e| anyhow!("Failed to load plugin from {} {e}", file.to_string_lossy()))?;
112
113 plugin.call::<_, ()>("new", ()).map_err(|e| {
114 anyhow!(
115 "Failed to call `new` on plugin from {}. {e}",
116 file.to_string_lossy()
117 )
118 })?;
119
120 Ok(Self {
121 plugin: Arc::new(Mutex::new(plugin)),
122 file,
123 })
124 }
125}
126
127impl Translator<VarId, ScopeId, Message> for PluginTranslator {
128 fn name(&self) -> String {
129 self.plugin
130 .lock()
131 .unwrap()
132 .call::<_, &str>("name", ())
133 .map_err(|e| {
134 error!(
135 "Failed to get translator name from {}. {e}",
136 self.file.to_string_lossy()
137 );
138 })
139 .map(|s| s.to_string())
140 .unwrap_or_default()
141 }
142
143 fn set_wave_source(&self, wave_source: Option<surfer_translation_types::WaveSource>) {
144 let mut plugin = self.plugin.lock().unwrap();
145 if plugin.function_exists("set_wave_source") {
146 plugin
147 .call::<_, ()>("set_wave_source", wave_source)
148 .map_err(|e| {
149 error!(
150 "Failed to set_wave_source on {}. {e}",
151 self.file.to_string_lossy()
152 )
153 })
154 .ok();
155 }
156 }
157
158 fn translate(
159 &self,
160 variable: &VariableMeta<VarId, ScopeId>,
161 value: &VariableValue,
162 ) -> color_eyre::Result<TranslationResult> {
163 let result = self
164 .plugin
165 .lock()
166 .unwrap()
167 .call(
168 "translate",
169 TranslateParams {
170 variable: variable.clone().map_ids(|_| (), |_| ()),
171 value: value.clone(),
172 },
173 )
174 .map_err(|e| {
175 anyhow!(
176 "Failed to translate {} with {}. {e}",
177 variable.var.name,
178 self.file.to_string_lossy()
179 )
180 })?;
181 Ok(result)
182 }
183
184 fn variable_info(
185 &self,
186 variable: &VariableMeta<VarId, ScopeId>,
187 ) -> color_eyre::Result<VariableInfo> {
188 let result = self
189 .plugin
190 .lock()
191 .unwrap()
192 .call("variable_info", variable.clone().map_ids(|_| (), |_| ()))
193 .map_err(|e| {
194 anyhow!(
195 "Failed to get variable info for {} with {}. {e}",
196 variable.var.name,
197 self.file.to_string_lossy()
198 )
199 })?;
200 Ok(result)
201 }
202
203 fn translates(
204 &self,
205 variable: &VariableMeta<VarId, ScopeId>,
206 ) -> color_eyre::Result<TranslationPreference> {
207 match self
208 .plugin
209 .lock()
210 .unwrap()
211 .call("translates", variable.clone().map_ids(|_| (), |_| ()))
212 {
213 Ok(r) => Ok(r),
214 Err(e) => Err(anyhow!(e)),
215 }
216 }
217
218 fn reload(&self, _sender: std::sync::mpsc::Sender<Message>) {
219 let mut plugin = self.plugin.lock().unwrap();
220 if plugin.function_exists("reload") {
221 match plugin.call("reload", ()) {
222 Ok(()) => (),
223 Err(e) => error!("{e:#}"),
224 }
225 }
226 }
227}
228
229host_fn!(current_dir() -> String {
230 std::env::current_dir()
231 .with_context(|| format!("Failed to get current dir"))
232 .and_then(|dir| {
233 dir.to_str().ok_or_else(|| {
234 anyhow!("{} is not valid utf8", dir.to_string_lossy())
235 }).map(|s| s.to_string())
236 })
237 .map_err(|e| extism::Error::msg(format!("{e:#}")))
238});
239
240host_fn!(read_file(filename: String) -> Vec<u8> {
241 std::fs::read(Utf8PathBuf::from(&filename))
242 .with_context(|| format!("Failed to read {filename}"))
243 .map_err(|e| extism::Error::msg(format!("{e:#}")))
244});
245
246host_fn!(file_exists(filename: String) -> bool {
247 Ok(Utf8PathBuf::from(&filename).exists())
248});