Skip to main content

libsurfer/translation/
wasm_translator.rs

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