iridis_file_ext/
file_ext.rs

1//! This module defines the `Manager` and `Loader` associated with the `FileExtPlugin` trait.
2//! It lets you load `FileExtPlugin`, store them and then load files according to their extension.
3
4use std::{collections::HashMap, mem::ManuallyDrop, path::PathBuf, sync::Arc};
5
6use crate::prelude::{
7    thirdparty::{libloading, tokio::task::JoinSet},
8    *,
9};
10
11/// Use this struct to load files according to their extension.
12pub struct FileExtManager {
13    pub plugins: HashMap<String, Arc<RuntimeFileExt>>,
14}
15
16/// Use this struct to load and store the plugins.
17pub struct FileExtLoader {
18    pub plugins: JoinSet<Result<(Vec<String>, RuntimeFileExt)>>,
19}
20
21impl FileExtManager {
22    /// Create a new `FileExtManager` with the given plugins.
23    pub fn new(plugins: HashMap<String, Arc<RuntimeFileExt>>) -> Self {
24        Self { plugins }
25    }
26
27    /// Load a file according to its extension. It's instantiating the `Node`, and so needs all
28    /// the primitives and configuration. This will `await` for the `Node` to be instantiated.
29    pub async fn load(
30        &self,
31        path: PathBuf,
32        inputs: Inputs,
33        outputs: Outputs,
34        queries: Queries,
35        queryables: Queryables,
36        configuration: serde_yml::Value,
37    ) -> Result<RuntimeNode> {
38        let ext = path
39            .extension()
40            .ok_or_eyre(format!("No extension found for path '{:?}'", path))?
41            .to_str()
42            .ok_or_eyre("Invalid extension")?;
43
44        let plugin = self
45            .plugins
46            .get(ext)
47            .ok_or_eyre(format!("Plugin not found for extension '{}'", ext))?;
48
49        plugin
50            .load(path, inputs, outputs, queries, queryables, configuration)
51            .await
52    }
53}
54
55impl FileExtLoader {
56    /// Create a new `FileExtLoader` with an empty set of plugins.
57    pub async fn new() -> Result<Self> {
58        Ok(FileExtLoader {
59            plugins: JoinSet::new(),
60        })
61    }
62
63    /// Load a statically linked plugin by calling the `new` method of the plugin. This function
64    /// is not `async`, so it will not `await` for the plugin to be loaded. It will spawn a new
65    /// task to load the plugin and return immediately.
66    ///
67    /// Before using any loaded plugin, the `FileExtLoader::finish` method must be called to
68    /// `await` for all the plugins to be loaded and return them.
69    pub fn load_statically_linked_plugin<T: FileExtPlugin + 'static>(&mut self) {
70        self.plugins.spawn(async move {
71            let plugin = T::new().await?.wrap_err(format!(
72                "Failed to load static plugin '{}'",
73                std::any::type_name::<T>(),
74            ))?;
75
76            let plugin = RuntimeFileExt::StaticallyLinked(plugin);
77
78            tracing::debug!(
79                "Loaded statically linked plugin: {}",
80                std::any::type_name::<T>()
81            );
82
83            Ok((plugin.target(), plugin))
84        });
85    }
86
87    /// Load a dynamically linked plugin by calling the `new` method of the plugin. This function
88    /// is not `async`, so it will not `await` for the plugin to be loaded. It will spawn a new
89    /// task to load the plugin and return immediately.
90    ///
91    /// Before using any loaded plugin, the `FileExtLoader::finish` method must be called to
92    /// `await` for all the plugins to be loaded and return them.
93    pub fn load_dynamically_linked_plugin(&mut self, path: PathBuf) {
94        self.plugins.spawn(async move {
95            match path.extension() {
96                Some(ext) => {
97                    if ext == std::env::consts::DLL_EXTENSION {
98                        let path_buf = path.clone();
99                        let (library, constructor) = tokio::task::spawn_blocking(move || {
100                            let library = unsafe {
101                                #[cfg(target_family = "unix")]
102                                let library = libloading::os::unix::Library::open(
103                                    Some(path_buf.clone()),
104                                    libloading::os::unix::RTLD_NOW | libloading::os::unix::RTLD_GLOBAL,
105                                )
106                                .wrap_err(format!("Failed to load path {:?}", path_buf))?;
107
108
109                                #[cfg(not(target_family = "unix"))]
110                                let library = Library::new(path_buf.clone())
111                                    .wrap_err(format!("Failed to load path {:?}", path_buf))?;
112
113                                library
114                            };
115
116                            let constructor = unsafe {
117                                library
118                                    .get::<*mut DynamicallyLinkedFileExtPluginInstance>(
119                                        b"IRIDIS_FILE_EXT_PLUGIN",
120                                    )
121                                    .wrap_err(format!(
122                                        "Failed to load symbol 'IRIDIS_FILE_EXT_PLUGIN' from cdylib {:?}",
123                                        path_buf
124                                    ))?
125                                    .read()
126                            };
127
128                            Ok::<_, eyre::Report>((library, constructor))
129                        })
130                        .await??;
131
132                        let plugin = RuntimeFileExt::DynamicallyLinked(
133                            DynamicallyLinkedFileExtPlugin::new(
134                                (constructor)().await?.wrap_err(format!(
135                                    "Failed to load dynamically linked plugin '{:?}'",
136                                    path,
137                                ))?,
138                                library,
139                            ),
140                        );
141
142                        tracing::debug!(
143                            "Loaded dynamically linked plugin from path: {}",
144                            path.display()
145                        );
146
147                        Ok((plugin.target(), plugin))
148                    } else {
149                        Err(eyre::eyre!("Extension '{:?}' is not supported", ext))
150                    }
151                }
152                _ => Err(eyre::eyre!("Unsupported path '{:?}'", path)),
153            }
154        });
155    }
156
157    /// Finish loading all the plugins. This function will `await` for all the plugins to be loaded
158    /// and return them.
159    pub async fn finish(mut self) -> Result<HashMap<String, Arc<RuntimeFileExt>>> {
160        let mut plugins = HashMap::new();
161
162        while let Some(result) = self.plugins.join_next().await {
163            let (targets, plugin) = result??;
164
165            let plugin = Arc::new(plugin);
166
167            for target in targets {
168                plugins.insert(target, plugin.clone());
169            }
170        }
171
172        Ok(plugins)
173    }
174}
175
176/// This struct represents a dynamically linked Plugin.
177/// It loads the plugin from a shared library at runtime, storing the handle as a `Box<dyn FileExtPlugin>`.
178/// It's really important to store the library as well, because once the library is dropped the handle will be invalid.
179///
180/// While for the `Node` struct we don't care about the order of the library and the handle, because by design the node will be dropped
181/// before the library, it's not the case here. And so we need either to use `ManuallyDrop` or to order the fields in a way that the library is dropped last.
182pub struct DynamicallyLinkedFileExtPlugin {
183    pub handle: ManuallyDrop<Box<dyn FileExtPlugin>>,
184
185    #[cfg(not(target_family = "unix"))]
186    pub library: ManuallyDrop<libloading::Library>,
187    #[cfg(target_family = "unix")]
188    pub library: ManuallyDrop<libloading::os::unix::Library>,
189}
190
191impl DynamicallyLinkedFileExtPlugin {
192    /// Create a new `DynamicallyLinkedFileExtPlugin` with the given handle and library.
193    /// Use this function to make it easier to create a new `DynamicallyLinkedFileExtPlugin`.
194    pub fn new(
195        handle: Box<dyn FileExtPlugin>,
196        #[cfg(not(target_family = "unix"))] library: libloading::Library,
197        #[cfg(target_family = "unix")] library: libloading::os::unix::Library,
198    ) -> Self {
199        Self {
200            handle: ManuallyDrop::new(handle),
201            library: ManuallyDrop::new(library),
202        }
203    }
204}
205
206impl Drop for DynamicallyLinkedFileExtPlugin {
207    fn drop(&mut self) {
208        unsafe {
209            ManuallyDrop::drop(&mut self.handle);
210            ManuallyDrop::drop(&mut self.library);
211        }
212    }
213}
214
215/// This is the main enum of this module. It represents a plugin that can be either statically linked or dynamically linked,
216/// allowing the runtime to use either type of plugin interchangeably.
217pub enum RuntimeFileExt {
218    StaticallyLinked(Box<dyn FileExtPlugin>),
219    DynamicallyLinked(DynamicallyLinkedFileExtPlugin),
220}
221
222impl RuntimeFileExt {
223    /// Returns the target of the plugin. This is used to determine which plugin to use for a given file.
224    pub fn target(&self) -> Vec<String> {
225        match self {
226            RuntimeFileExt::StaticallyLinked(plugin) => plugin.target(),
227            RuntimeFileExt::DynamicallyLinked(plugin) => plugin.handle.target(),
228        }
229    }
230
231    /// Load a `Node` based on the `PathBuf` to the correct file. This will `await` for the `Node` to be loaded.
232    pub async fn load(
233        &self,
234        path: PathBuf,
235        inputs: Inputs,
236        outputs: Outputs,
237        queries: Queries,
238        queryables: Queryables,
239        configuration: serde_yml::Value,
240    ) -> Result<RuntimeNode> {
241        match self {
242            RuntimeFileExt::StaticallyLinked(plugin) => {
243                plugin
244                    .load(path, inputs, outputs, queries, queryables, configuration)
245                    .await?
246            }
247            RuntimeFileExt::DynamicallyLinked(plugin) => {
248                plugin
249                    .handle
250                    .load(path, inputs, outputs, queries, queryables, configuration)
251                    .await?
252            }
253        }
254    }
255}