1use libloading::{Library, Symbol};
18use serde_json::Value;
19
20use crate::config::{Config, ConfigError};
21use crate::dic::grammar::Grammar;
22use crate::error::{SudachiError, SudachiResult};
23use crate::plugin::PluginError;
24
25pub struct PluginContainer<T: PluginCategory + ?Sized> {
28 libraries: Vec<Library>,
29 plugins: Vec<<T as PluginCategory>::BoxType>,
30}
31
32impl<T: PluginCategory + ?Sized> PluginContainer<T> {
33 pub fn plugins(&self) -> &[<T as PluginCategory>::BoxType] {
34 &self.plugins
35 }
36 pub fn is_empty(&self) -> bool {
37 self.plugins.is_empty()
38 }
39}
40
41impl<T: PluginCategory + ?Sized> Drop for PluginContainer<T> {
42 fn drop(&mut self) {
43 self.plugins.clear();
44 self.libraries.clear();
45 }
46}
47
48struct PluginLoader<'a, 'b, T: PluginCategory + ?Sized> {
49 cfg: &'a Config,
50 grammar: &'a mut Grammar<'b>,
51 libraries: Vec<Library>,
52 plugins: Vec<<T as PluginCategory>::BoxType>,
53}
54
55#[cfg(any(target_os = "linux", target_os = "freebsd"))]
56fn make_system_specific_name(s: &str) -> String {
57 format!("lib{}.so", s)
58}
59
60#[cfg(target_os = "windows")]
61fn make_system_specific_name(s: &str) -> String {
62 format!("{}.dll", s)
63}
64
65#[cfg(target_os = "macos")]
66fn make_system_specific_name(s: &str) -> String {
67 format!("lib{}.dylib", s)
68}
69
70fn system_specific_name(s: &str) -> Option<String> {
71 if s.contains('.') {
72 None
73 } else {
74 let p = std::path::Path::new(s);
75 let fname = p
76 .file_name()
77 .and_then(|np| np.to_str())
78 .map(make_system_specific_name);
79 let parent = p.parent().and_then(|np| np.to_str());
80 match (parent, fname) {
81 (Some(p), Some(c)) => Some(format!("{}/{}", p, c)),
82 _ => None,
83 }
84 }
85}
86
87impl<'a, 'b, T: PluginCategory + ?Sized> PluginLoader<'a, 'b, T> {
88 pub fn new(grammar: &'a mut Grammar<'b>, config: &'a Config) -> PluginLoader<'a, 'b, T> {
89 PluginLoader {
90 cfg: config,
91 grammar,
92 libraries: Vec::new(),
93 plugins: Vec::new(),
94 }
95 }
96
97 pub fn load(&mut self) -> SudachiResult<()> {
98 let configs = <T as PluginCategory>::configurations(self.cfg);
99 for cfg in configs {
100 let name = extract_plugin_class(cfg)?;
101 self.load_plugin(name, cfg)?;
102 }
103 Ok(())
104 }
105
106 pub fn freeze(self) -> PluginContainer<T> {
107 PluginContainer {
108 libraries: self.libraries,
109 plugins: self.plugins,
110 }
111 }
112
113 fn load_plugin(&mut self, name: &str, plugin_cfg: &Value) -> SudachiResult<()> {
114 let mut plugin =
115 if let Some(stripped_name) = name.strip_prefix("com.worksap.nlp.sudachi.") {
117 if let Some(p) = <T as PluginCategory>::bundled_impl(stripped_name) {
118 p
119 } else {
120 return Err(SudachiError::ConfigError(ConfigError::InvalidFormat(
121 format!("Failed to lookup bundled plugin: {}", name)
122 )))
123 }
124 } else {
126 let candidates = self.resolve_dso_names(name);
127 self.load_plugin_from_dso(&candidates)?
128 };
129
130 <T as PluginCategory>::do_setup(&mut plugin, plugin_cfg, self.cfg, self.grammar)
131 .map_err(|e| e.with_context(format!("plugin {} setup", name)))?;
132 self.plugins.push(plugin);
133 Ok(())
134 }
135
136 fn resolve_dso_names(&self, name: &str) -> Vec<String> {
137 let mut resolved = self.cfg.resolve_paths(name.to_owned());
138
139 if let Some(sysname) = system_specific_name(name) {
140 let resolved_sys = self.cfg.resolve_paths(sysname);
141 resolved.extend(resolved_sys);
142 }
143
144 resolved
145 }
146
147 fn try_load_library_from(candidates: &[String]) -> SudachiResult<(Library, &str)> {
148 if candidates.is_empty() {
149 return Err(SudachiError::PluginError(PluginError::InvalidDataFormat(
150 "No candidates to load library".to_owned(),
151 )));
152 }
153
154 let mut last_error = libloading::Error::IncompatibleSize;
155 for p in candidates.iter() {
156 match unsafe { Library::new(p.as_str()) } {
157 Ok(lib) => return Ok((lib, p.as_str())),
158 Err(e) => last_error = e,
159 }
160 }
161 Err(SudachiError::PluginError(PluginError::Libloading {
162 source: last_error,
163 message: format!("failed to load library from: {:?}", candidates),
164 }))
165 }
166
167 fn load_plugin_from_dso(
168 &mut self,
169 candidates: &[String],
170 ) -> SudachiResult<<T as PluginCategory>::BoxType> {
171 let (lib, path) = Self::try_load_library_from(candidates)?;
172 let load_fn: Symbol<fn() -> SudachiResult<<T as PluginCategory>::BoxType>> =
173 unsafe { lib.get(b"load_plugin") }.map_err(|e| PluginError::Libloading {
174 source: e,
175 message: format!("no load_plugin symbol in {}", path),
176 })?;
177 let plugin = load_fn();
178 self.libraries.push(lib);
179 plugin
180 }
181}
182
183fn extract_plugin_class(val: &Value) -> SudachiResult<&str> {
184 let obj = match val {
185 Value::Object(v) => v,
186 o => {
187 return Err(SudachiError::ConfigError(ConfigError::InvalidFormat(
188 format!("plugin config must be an object, was {}", o),
189 )));
190 }
191 };
192 match obj.get("class") {
193 Some(Value::String(v)) => Ok(v),
194 _ => Err(SudachiError::ConfigError(ConfigError::InvalidFormat(
195 "plugin config must have 'class' key to indicate plugin SO file".to_owned(),
196 ))),
197 }
198}
199
200pub trait PluginCategory {
202 type BoxType;
204
205 type InitFnType;
208
209 fn configurations(cfg: &Config) -> &[Value];
211
212 fn bundled_impl(name: &str) -> Option<Self::BoxType>;
219
220 fn do_setup(
224 ptr: &mut Self::BoxType,
225 settings: &Value,
226 config: &Config,
227 grammar: &mut Grammar,
228 ) -> SudachiResult<()>;
229}
230
231pub fn load_plugins_of<'a, T: PluginCategory + ?Sized>(
235 cfg: &'a Config,
236 grammar: &'a mut Grammar<'_>,
237) -> SudachiResult<PluginContainer<T>> {
238 let mut loader: PluginLoader<T> = PluginLoader::new(grammar, cfg);
239 loader.load()?;
240 Ok(loader.freeze())
241}