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}