iridis_url_scheme/
url_scheme.rs

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