use libloading::{Library, Symbol};
use serde_json::Value;
use crate::config::{Config, ConfigError};
use crate::dic::grammar::Grammar;
use crate::error::{SudachiError, SudachiResult};
use crate::plugin::PluginError;
pub struct PluginContainer<T: PluginCategory + ?Sized> {
libraries: Vec<Library>,
plugins: Vec<<T as PluginCategory>::BoxType>,
}
impl<T: PluginCategory + ?Sized> PluginContainer<T> {
pub fn plugins(&self) -> &[<T as PluginCategory>::BoxType] {
&self.plugins
}
pub fn is_empty(&self) -> bool {
self.plugins.is_empty()
}
}
impl<T: PluginCategory + ?Sized> Drop for PluginContainer<T> {
fn drop(&mut self) {
self.plugins.clear();
self.libraries.clear();
}
}
struct PluginLoader<'a, 'b, T: PluginCategory + ?Sized> {
cfg: &'a Config,
grammar: &'a mut Grammar<'b>,
libraries: Vec<Library>,
plugins: Vec<<T as PluginCategory>::BoxType>,
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn make_system_specific_name(s: &str) -> String {
format!("lib{}.so", s)
}
#[cfg(target_os = "windows")]
fn make_system_specific_name(s: &str) -> String {
format!("{}.dll", s)
}
#[cfg(target_os = "macos")]
fn make_system_specific_name(s: &str) -> String {
format!("lib{}.dylib", s)
}
fn system_specific_name(s: &str) -> Option<String> {
if s.contains('.') {
None
} else {
let p = std::path::Path::new(s);
let fname = p
.file_name()
.and_then(|np| np.to_str())
.map(make_system_specific_name);
let parent = p.parent().and_then(|np| np.to_str());
match (parent, fname) {
(Some(p), Some(c)) => Some(format!("{}/{}", p, c)),
_ => None,
}
}
}
impl<'a, 'b, T: PluginCategory + ?Sized> PluginLoader<'a, 'b, T> {
pub fn new(grammar: &'a mut Grammar<'b>, config: &'a Config) -> PluginLoader<'a, 'b, T> {
PluginLoader {
cfg: config,
grammar,
libraries: Vec::new(),
plugins: Vec::new(),
}
}
pub fn load(&mut self) -> SudachiResult<()> {
let configs = <T as PluginCategory>::configurations(self.cfg);
for cfg in configs {
let name = extract_plugin_class(cfg)?;
self.load_plugin(name, cfg)?;
}
Ok(())
}
pub fn freeze(self) -> PluginContainer<T> {
PluginContainer {
libraries: self.libraries,
plugins: self.plugins,
}
}
fn load_plugin(&mut self, name: &str, plugin_cfg: &Value) -> SudachiResult<()> {
let mut plugin =
if let Some(stripped_name) = name.strip_prefix("com.worksap.nlp.sudachi.") {
if let Some(p) = <T as PluginCategory>::bundled_impl(stripped_name) {
p
} else {
return Err(SudachiError::ConfigError(ConfigError::InvalidFormat(
format!("Failed to lookup bundled plugin: {}", name)
)))
}
} else {
let candidates = self.resolve_dso_names(name);
self.load_plugin_from_dso(&candidates)?
};
<T as PluginCategory>::do_setup(&mut plugin, plugin_cfg, self.cfg, self.grammar)
.map_err(|e| e.with_context(format!("plugin {} setup", name)))?;
self.plugins.push(plugin);
Ok(())
}
fn resolve_dso_names(&self, name: &str) -> Vec<String> {
let mut resolved = self.cfg.resolve_paths(name.to_owned());
if let Some(sysname) = system_specific_name(name) {
let resolved_sys = self.cfg.resolve_paths(sysname);
resolved.extend(resolved_sys);
}
resolved
}
fn try_load_library_from(candidates: &[String]) -> SudachiResult<(Library, &str)> {
if candidates.is_empty() {
return Err(SudachiError::PluginError(PluginError::InvalidDataFormat(
"No candidates to load library".to_owned(),
)));
}
let mut last_error = libloading::Error::IncompatibleSize;
for p in candidates.iter() {
match unsafe { Library::new(p.as_str()) } {
Ok(lib) => return Ok((lib, p.as_str())),
Err(e) => last_error = e,
}
}
Err(SudachiError::PluginError(PluginError::Libloading {
source: last_error,
message: format!("failed to load library from: {:?}", candidates),
}))
}
fn load_plugin_from_dso(
&mut self,
candidates: &[String],
) -> SudachiResult<<T as PluginCategory>::BoxType> {
let (lib, path) = Self::try_load_library_from(candidates)?;
let load_fn: Symbol<fn() -> SudachiResult<<T as PluginCategory>::BoxType>> =
unsafe { lib.get(b"load_plugin") }.map_err(|e| PluginError::Libloading {
source: e,
message: format!("no load_plugin symbol in {}", path),
})?;
let plugin = load_fn();
self.libraries.push(lib);
plugin
}
}
fn extract_plugin_class(val: &Value) -> SudachiResult<&str> {
let obj = match val {
Value::Object(v) => v,
o => {
return Err(SudachiError::ConfigError(ConfigError::InvalidFormat(
format!("plugin config must be an object, was {}", o),
)));
}
};
match obj.get("class") {
Some(Value::String(v)) => Ok(v),
_ => Err(SudachiError::ConfigError(ConfigError::InvalidFormat(
"plugin config must have 'class' key to indicate plugin SO file".to_owned(),
))),
}
}
pub trait PluginCategory {
type BoxType;
type InitFnType;
fn configurations(cfg: &Config) -> &[Value];
fn bundled_impl(name: &str) -> Option<Self::BoxType>;
fn do_setup(
ptr: &mut Self::BoxType,
settings: &Value,
config: &Config,
grammar: &mut Grammar,
) -> SudachiResult<()>;
}
pub fn load_plugins_of<'a, T: PluginCategory + ?Sized>(
cfg: &'a Config,
grammar: &'a mut Grammar<'_>,
) -> SudachiResult<PluginContainer<T>> {
let mut loader: PluginLoader<T> = PluginLoader::new(grammar, cfg);
loader.load()?;
Ok(loader.freeze())
}