sudachi/plugin/
loader.rs

1/*
2 *  Copyright (c) 2021-2024 Works Applications Co., Ltd.
3 *
4 *  Licensed under the Apache License, Version 2.0 (the "License");
5 *  you may not use this file except in compliance with the License.
6 *  You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 *   Unless required by applicable law or agreed to in writing, software
11 *  distributed under the License is distributed on an "AS IS" BASIS,
12 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 *  See the License for the specific language governing permissions and
14 *  limitations under the License.
15 */
16
17use 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
25/// Holds loaded plugins, whether they are bundled
26/// or loaded from DSOs
27pub 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))]
76// Apple embedded platforms do not support dynamic library loading (DSO plugins)
77// due to platform security restrictions. Returning None here
78// effectively disables loading plugins from DSOs, while still allowing
79// plugins bundled with the application/binary to work as expected.
80fn 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            // Try to load bundled plugin first, if its name looks like it
130            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            // Otherwise treat name as DSO
139            } 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
214/// A category of Plugins
215pub trait PluginCategory {
216    /// Boxed type of the plugin. Should be Box<dyn XXXX>.
217    type BoxType;
218
219    /// Type of the initialization function.
220    /// It must take 0 arguments and return `SudachiResult<Self::BoxType>`.
221    type InitFnType;
222
223    /// Extract plugin configurations from the config
224    fn configurations(cfg: &Config) -> &[Value];
225
226    /// Create bundled plugin for plugin name
227    /// Instead of full name like com.worksap.nlp.sudachi.ProlongedSoundMarkPlugin
228    /// should handle only the short one: ProlongedSoundMarkPlugin
229    ///
230    /// com.worksap.nlp.sudachi. (last dot included) will be stripped automatically
231    /// by the loader code
232    fn bundled_impl(name: &str) -> Option<Self::BoxType>;
233
234    /// Perform initial setup.
235    /// We can't call set_up of the plugin directly in the default implementation
236    /// of this method because we do not know the specific type yet
237    fn do_setup(
238        ptr: &mut Self::BoxType,
239        settings: &Value,
240        config: &Config,
241        grammar: &mut Grammar,
242    ) -> SudachiResult<()>;
243}
244
245/// Helper function to load the plugins of a single category
246/// Should be called with turbofish syntax and trait object type:
247/// `let plugins = load_plugins_of::<dyn InputText>(...)`.
248pub 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}