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) -> Option<String> {
57 Some(format!("lib{}.so", s))
58}
59
60#[cfg(target_os = "windows")]
61fn make_system_specific_name(s: &str) -> Option<String> {
62 Some(format!("{}.dll", s))
63}
64
65#[cfg(target_os = "macos")]
66fn make_system_specific_name(s: &str) -> Option<String> {
67 Some(format!("lib{}.dylib", s))
68}
69
70#[cfg(any(
71 target_os = "ios",
72 target_os = "tvos",
73 target_os = "watchos",
74 target_os = "visionos"
75))]
76fn make_system_specific_name(_s: &str) -> Option<String> {
81 None
82}
83
84fn system_specific_name(s: &str) -> Option<String> {
85 if s.contains('.') {
86 None
87 } else {
88 let p = std::path::Path::new(s);
89 let fname = p
90 .file_name()
91 .and_then(|np| np.to_str())
92 .and_then(make_system_specific_name);
93 let parent = p.parent().and_then(|np| np.to_str());
94 match (parent, fname) {
95 (Some(p), Some(c)) => Some(format!("{}/{}", p, c)),
96 _ => None,
97 }
98 }
99}
100
101impl<'a, 'b, T: PluginCategory + ?Sized> PluginLoader<'a, 'b, T> {
102 pub fn new(grammar: &'a mut Grammar<'b>, config: &'a Config) -> PluginLoader<'a, 'b, T> {
103 PluginLoader {
104 cfg: config,
105 grammar,
106 libraries: Vec::new(),
107 plugins: Vec::new(),
108 }
109 }
110
111 pub fn load(&mut self) -> SudachiResult<()> {
112 let configs = <T as PluginCategory>::configurations(self.cfg);
113 for cfg in configs {
114 let name = extract_plugin_class(cfg)?;
115 self.load_plugin(name, cfg)?;
116 }
117 Ok(())
118 }
119
120 pub fn freeze(self) -> PluginContainer<T> {
121 PluginContainer {
122 libraries: self.libraries,
123 plugins: self.plugins,
124 }
125 }
126
127 fn load_plugin(&mut self, name: &str, plugin_cfg: &Value) -> SudachiResult<()> {
128 let mut plugin =
129 if let Some(stripped_name) = name.strip_prefix("com.worksap.nlp.sudachi.") {
131 if let Some(p) = <T as PluginCategory>::bundled_impl(stripped_name) {
132 p
133 } else {
134 return Err(SudachiError::ConfigError(ConfigError::InvalidFormat(
135 format!("Failed to lookup bundled plugin: {}", name)
136 )))
137 }
138 } else {
140 let candidates = self.resolve_dso_names(name);
141 self.load_plugin_from_dso(&candidates)?
142 };
143
144 <T as PluginCategory>::do_setup(&mut plugin, plugin_cfg, self.cfg, self.grammar)
145 .map_err(|e| e.with_context(format!("plugin {} setup", name)))?;
146 self.plugins.push(plugin);
147 Ok(())
148 }
149
150 fn resolve_dso_names(&self, name: &str) -> Vec<String> {
151 let mut resolved = self.cfg.resolve_paths(name.to_owned());
152
153 if let Some(sysname) = system_specific_name(name) {
154 let resolved_sys = self.cfg.resolve_paths(sysname);
155 resolved.extend(resolved_sys);
156 }
157
158 resolved
159 }
160
161 fn try_load_library_from(candidates: &[String]) -> SudachiResult<(Library, &str)> {
162 if candidates.is_empty() {
163 return Err(SudachiError::PluginError(PluginError::InvalidDataFormat(
164 "No candidates to load library".to_owned(),
165 )));
166 }
167
168 let mut last_error = libloading::Error::IncompatibleSize;
169 for p in candidates.iter() {
170 match unsafe { Library::new(p.as_str()) } {
171 Ok(lib) => return Ok((lib, p.as_str())),
172 Err(e) => last_error = e,
173 }
174 }
175 Err(SudachiError::PluginError(PluginError::Libloading {
176 source: last_error,
177 message: format!("failed to load library from: {:?}", candidates),
178 }))
179 }
180
181 fn load_plugin_from_dso(
182 &mut self,
183 candidates: &[String],
184 ) -> SudachiResult<<T as PluginCategory>::BoxType> {
185 let (lib, path) = Self::try_load_library_from(candidates)?;
186 let load_fn: Symbol<fn() -> SudachiResult<<T as PluginCategory>::BoxType>> =
187 unsafe { lib.get(b"load_plugin") }.map_err(|e| PluginError::Libloading {
188 source: e,
189 message: format!("no load_plugin symbol in {}", path),
190 })?;
191 let plugin = load_fn();
192 self.libraries.push(lib);
193 plugin
194 }
195}
196
197fn extract_plugin_class(val: &Value) -> SudachiResult<&str> {
198 let obj = match val {
199 Value::Object(v) => v,
200 o => {
201 return Err(SudachiError::ConfigError(ConfigError::InvalidFormat(
202 format!("plugin config must be an object, was {}", o),
203 )));
204 }
205 };
206 match obj.get("class") {
207 Some(Value::String(v)) => Ok(v),
208 _ => Err(SudachiError::ConfigError(ConfigError::InvalidFormat(
209 "plugin config must have 'class' key to indicate plugin SO file".to_owned(),
210 ))),
211 }
212}
213
214pub trait PluginCategory {
216 type BoxType;
218
219 type InitFnType;
222
223 fn configurations(cfg: &Config) -> &[Value];
225
226 fn bundled_impl(name: &str) -> Option<Self::BoxType>;
233
234 fn do_setup(
238 ptr: &mut Self::BoxType,
239 settings: &Value,
240 config: &Config,
241 grammar: &mut Grammar,
242 ) -> SudachiResult<()>;
243}
244
245pub fn load_plugins_of<'a, T: PluginCategory + ?Sized>(
249 cfg: &'a Config,
250 grammar: &'a mut Grammar<'_>,
251) -> SudachiResult<PluginContainer<T>> {
252 let mut loader: PluginLoader<T> = PluginLoader::new(grammar, cfg);
253 loader.load()?;
254 Ok(loader.freeze())
255}