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) -> 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            // Try to load bundled plugin first, if its name looks like it
116            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            // Otherwise treat name as DSO
125            } 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
200/// A category of Plugins
201pub trait PluginCategory {
202    /// Boxed type of the plugin. Should be Box<dyn XXXX>.
203    type BoxType;
204
205    /// Type of the initialization function.
206    /// It must take 0 arguments and return `SudachiResult<Self::BoxType>`.
207    type InitFnType;
208
209    /// Extract plugin configurations from the config
210    fn configurations(cfg: &Config) -> &[Value];
211
212    /// Create bundled plugin for plugin name
213    /// Instead of full name like com.worksap.nlp.sudachi.ProlongedSoundMarkPlugin
214    /// should handle only the short one: ProlongedSoundMarkPlugin
215    ///
216    /// com.worksap.nlp.sudachi. (last dot included) will be stripped automatically
217    /// by the loader code
218    fn bundled_impl(name: &str) -> Option<Self::BoxType>;
219
220    /// Perform initial setup.
221    /// We can't call set_up of the plugin directly in the default implementation
222    /// of this method because we do not know the specific type yet
223    fn do_setup(
224        ptr: &mut Self::BoxType,
225        settings: &Value,
226        config: &Config,
227        grammar: &mut Grammar,
228    ) -> SudachiResult<()>;
229}
230
231/// Helper function to load the plugins of a single category
232/// Should be called with turbofish syntax and trait object type:
233/// `let plugins = load_plugins_of::<dyn InputText>(...)`.
234pub 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}