zenoh_flow/runtime/dataflow/
loader.rs

1//
2// Copyright (c) 2021 - 2023 ZettaScale Technology
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7// which is available at https://www.apache.org/licenses/LICENSE-2.0.
8//
9// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10//
11// Contributors:
12//   ZettaScale Zenoh Team, <zenoh@zettascale.tech>
13//
14
15use super::instance::builtin::zenoh::{get_zenoh_sink_declaration, get_zenoh_source_declaration};
16use super::node::{
17    ConstructorFn, OperatorConstructor, OperatorFn, SinkConstructor, SinkFn, SourceConstructor,
18    SourceFn,
19};
20use crate::model::record::{OperatorRecord, SinkRecord, SourceRecord};
21use crate::model::{Middleware, ZFUri};
22use crate::types::Configuration;
23use crate::utils::parse_uri;
24use crate::zfresult::ErrorKind;
25use crate::Result;
26use crate::{bail, zferror};
27use serde::{Deserialize, Serialize};
28use std::path::{Path, PathBuf};
29use std::sync::Arc;
30
31#[cfg(target_family = "unix")]
32use libloading::os::unix::Library;
33#[cfg(target_family = "windows")]
34use libloading::Library;
35
36#[cfg(target_family = "unix")]
37static LOAD_FLAGS: std::os::raw::c_int =
38    libloading::os::unix::RTLD_NOW | libloading::os::unix::RTLD_LOCAL;
39
40/// Constant used to check if a node is compatible with the currently running Zenoh Flow daemon.
41/// As nodes are dynamically loaded, this is to prevent (possibly cryptic) runtime error due to
42/// incompatible API.
43pub static CORE_VERSION: &str = env!("CARGO_PKG_VERSION");
44/// Constant used to check if a node was compiled with the same version of the Rust compiler than
45/// the currently running Zenoh Flow daemon.
46/// As Rust is not ABI stable, this is to prevent (possibly cryptic) runtime errors.
47pub static RUSTC_VERSION: &str = env!("RUSTC_VERSION");
48
49pub static EXT_FILE_EXTENSION: &str = "zfext";
50
51/// NodeSymbol groups the symbol we must find in the shared library we load.
52pub(crate) enum NodeSymbol {
53    Source,
54    Operator,
55    Sink,
56}
57
58impl NodeSymbol {
59    /// Returns the bytes representation of the symbol.
60    ///
61    /// They are of the form:
62    ///
63    /// `b"_zf_export_<node_kind>\0"`
64    ///
65    /// Where `<node_kind>` is either `operator`, `source`, or `sink`.
66    pub(crate) fn to_bytes(&self) -> &[u8] {
67        match self {
68            NodeSymbol::Source => b"_zf_export_source\0",
69            NodeSymbol::Operator => b"_zf_export_operator\0",
70            NodeSymbol::Sink => b"_zf_export_sink\0",
71        }
72    }
73}
74
75/// Declaration expected in the library that will be loaded.
76pub struct NodeDeclaration<C> {
77    pub rustc_version: &'static str,
78    pub core_version: &'static str,
79    pub constructor: C,
80}
81
82pub type SourceDeclaration = NodeDeclaration<SourceFn>;
83pub type OperatorDeclaration = NodeDeclaration<OperatorFn>;
84pub type SinkDeclaration = NodeDeclaration<SinkFn>;
85
86/// Extensible support for different implementations
87/// This represents the configuration for an extension.
88///
89///
90/// Example:
91///
92/// ```yaml
93/// name: python
94/// file_extension: py
95/// source_lib: ./target/release/libpy_source.so
96/// sink_lib: ./target/release/libpy_sink.so
97/// operator_lib: ./target/release/libpy_op.so
98/// config_lib_key: python-script
99/// ```
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ExtensibleImplementation {
102    pub(crate) name: String,
103    pub(crate) file_extension: String,
104    pub(crate) source_lib: String,
105    pub(crate) sink_lib: String,
106    pub(crate) operator_lib: String,
107    pub(crate) config_lib_key: String,
108}
109
110/// Loader configuration files, it includes the extensions.
111///
112/// Example:
113///
114/// ```yaml
115/// extensions:
116///   - name: python
117///     file_extension: py
118///     source_lib: ./target/release/libpy_source.so
119///     sink_lib: ./target/release/libpy_sink.so
120///     operator_lib: ./target/release/libpy_op.so
121///     config_lib_key: python-script
122/// ```
123///
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct LoaderConfig {
126    extensions: Vec<ExtensibleImplementation>,
127}
128
129impl LoaderConfig {
130    /// Creates an empty `LoaderConfig`.
131    pub fn new() -> Self {
132        Self { extensions: vec![] }
133    }
134
135    /// Adds the given extension.
136    ///
137    /// # Errors
138    /// It returns an error variant if the extension is already present.
139    pub fn try_add_extension(&mut self, ext: ExtensibleImplementation) -> Result<()> {
140        if self.extensions.iter().any(|e| e.name == ext.name) {
141            return Err(zferror!(ErrorKind::Duplicate).into());
142        }
143        self.extensions.push(ext);
144        Ok(())
145    }
146
147    /// Removes the given extension.
148    pub fn remove_extension(&mut self, name: &str) -> Option<ExtensibleImplementation> {
149        if let Some(index) = self.extensions.iter().position(|e| e.name == name) {
150            let ext = self.extensions.remove(index);
151            return Some(ext);
152        }
153        None
154    }
155
156    /// Gets the extension that matches the given `file_extension`.
157    pub fn get_extension_by_file_extension(
158        &self,
159        file_extension: &str,
160    ) -> Option<&ExtensibleImplementation> {
161        if let Some(ext) = self
162            .extensions
163            .iter()
164            .find(|e| e.file_extension == file_extension)
165        {
166            return Some(ext);
167        }
168        None
169    }
170
171    /// Gets the extension that matches the given `name`.
172    pub fn get_extension_by_name(&self, name: &str) -> Option<&ExtensibleImplementation> {
173        if let Some(ext) = self.extensions.iter().find(|e| e.name == name) {
174            return Some(ext);
175        }
176        None
177    }
178}
179
180impl Default for LoaderConfig {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186/// The dynamic library loader.
187/// Before loading it verifies if the versions are compatible
188/// and if the symbols are presents.
189/// It loads the files in different way depending on the operating system.
190/// In particular the scope of the symbols is different between Unix and
191/// Windows.
192/// In Unix system the symbols are loaded with the flags:
193///
194/// - `RTLD_NOW` load all the symbols when loading the library.
195/// - `RTLD_LOCAL` keep all the symbols local.
196pub struct Loader {
197    pub(crate) config: LoaderConfig,
198}
199
200impl Loader {
201    /// Creates a new `Loader` with the given `config`.
202    pub fn new(config: LoaderConfig) -> Self {
203        Self { config }
204    }
205
206    /// Loads a node library from a file, using one of the extension configured within the loader.
207    ///
208    /// # Errors
209    ///
210    /// It can fail because of:
211    /// - different version of Zenoh-Flow used to build the node
212    /// - different version of the rust compiler used to build the node
213    /// - the library does not contain the symbols
214    /// - the extension is not known
215    /// - the node does not match the extension interface
216    unsafe fn load_node_from_file<T: ConstructorFn>(
217        &self,
218        node_symbol: NodeSymbol,
219        file_path: PathBuf,
220        configuration: &mut Option<Configuration>,
221    ) -> Result<(Library, T)> {
222        let file_extension = crate::utils::get_file_extension(&file_path).ok_or_else(|| {
223            zferror!(
224                ErrorKind::LoadingError,
225                "Missing file extension for < {:?} >",
226                file_path,
227            )
228        })?;
229
230        let library_path = if crate::utils::is_dynamic_library(&file_extension) {
231            file_path
232        } else {
233            match self.config.get_extension_by_file_extension(&file_extension) {
234                Some(e) => {
235                    Self::wrap_configuration(configuration, e.config_lib_key.clone(), &file_path)?;
236                    let lib = match node_symbol {
237                        NodeSymbol::Source => &e.source_lib,
238                        NodeSymbol::Operator => &e.operator_lib,
239                        NodeSymbol::Sink => &e.sink_lib,
240                    };
241                    std::fs::canonicalize(lib)?
242                }
243                _ => bail!(ErrorKind::Unimplemented),
244            }
245        };
246
247        log::trace!("[Loader] loading library {:?}", library_path);
248
249        #[cfg(target_family = "unix")]
250        let library = Library::open(Some(library_path.clone()), LOAD_FLAGS)?;
251
252        #[cfg(target_family = "windows")]
253        let library = Library::new(library_path)?;
254
255        let decl = library
256            .get::<*mut NodeDeclaration<T>>(node_symbol.to_bytes())?
257            .read();
258
259        // version checks to prevent accidental ABI incompatibilities
260        if decl.rustc_version != RUSTC_VERSION || decl.core_version != CORE_VERSION {
261            return Err(zferror!(
262                ErrorKind::VersionMismatch,
263                "Library {} rustc expected {} rustc found {} - Zenoh-Flow expected {} Zenoh-Flow found {}",
264                library_path.display(),
265                RUSTC_VERSION,
266                decl.rustc_version,
267                CORE_VERSION,
268                decl.core_version
269            )
270            .into());
271        }
272
273        Ok((library, decl.constructor))
274    }
275
276    /// Loads a source from the builtin ones.
277    ///
278    /// # Errors
279    ///
280    /// It can fail because of:
281    /// - the buitin middleware is not supported (so far only Zenoh is supported)
282    fn load_source_from_builtin(&self, middleware: Middleware) -> Result<SourceFn> {
283        match middleware {
284            Middleware::Zenoh => {
285                let declaration = get_zenoh_source_declaration();
286                Ok(declaration.constructor)
287            }
288        }
289    }
290
291    /// Loads a sink from the builtin ones
292    ///
293    /// # Errors
294    ///
295    /// It can fail because of:
296    /// - the buitin middleware is not supported (so far only Zenoh is supported)
297    fn load_sink_from_builtin(&self, middleware: Middleware) -> Result<SinkFn> {
298        match middleware {
299            Middleware::Zenoh => {
300                let declaration = get_zenoh_sink_declaration();
301                Ok(declaration.constructor)
302            }
303        }
304    }
305
306    /// Tries to load a Source from the information passed within the
307    /// [`SourceRecord`](`SourceRecord`).
308    ///
309    /// # Errors
310    ///
311    /// It can fail because of:
312    /// - different version of Zenoh-Flow used to build the source
313    /// - different version of the rust compiler used to build the source
314    /// - the library does not contain the symbols
315    /// - the URI is missing
316    /// - the URI scheme is not known (so far only `file://` is supported).
317    pub(crate) fn load_source_constructor(
318        &self,
319        mut record: SourceRecord,
320    ) -> Result<SourceConstructor> {
321        if let Some(uri) = &record.uri {
322            match parse_uri(uri)? {
323                ZFUri::File(file_path) => {
324                    let (library, constructor) = unsafe {
325                        self.load_node_from_file::<SourceFn>(
326                            NodeSymbol::Source,
327                            file_path,
328                            &mut record.configuration,
329                        )?
330                    };
331
332                    Ok(SourceConstructor::new_dynamic(
333                        record,
334                        constructor,
335                        Arc::new(library),
336                    ))
337                }
338                ZFUri::Builtin(mw) => {
339                    let constructor = self.load_source_from_builtin(mw)?;
340                    Ok(SourceConstructor::new_static(record, constructor))
341                }
342            }
343        } else {
344            bail!(
345                ErrorKind::LoadingError,
346                "Missing URI for dynamically loaded Source < {} >.",
347                record.id.clone()
348            )
349        }
350    }
351
352    /// Tries to load an Operator from the information passed within the
353    /// [`OperatorRecord`](`OperatorRecord`).
354    ///
355    ///
356    /// # Errors
357    ///
358    /// This method can fail if:
359    /// - different versions of Zenoh-Flow used to build the operator
360    /// - different versions of the rust compiler used to build the operator
361    /// - the library does not contain the symbols
362    /// - the URI is missing
363    /// - the URI scheme is not known (so far only `file://` is known).
364    pub(crate) fn load_operator_constructor(
365        &self,
366        mut record: OperatorRecord,
367    ) -> Result<OperatorConstructor> {
368        if let Some(uri) = &record.uri {
369            match parse_uri(uri)? {
370                ZFUri::File(file_path) => {
371                    let (library, constructor) = unsafe {
372                        self.load_node_from_file::<OperatorFn>(
373                            NodeSymbol::Operator,
374                            file_path,
375                            &mut record.configuration,
376                        )?
377                    };
378
379                    Ok(OperatorConstructor::new_dynamic(
380                        record,
381                        constructor,
382                        Arc::new(library),
383                    ))
384                }
385                ZFUri::Builtin(_mw) => {
386                    bail!(
387                        ErrorKind::Unimplemented,
388                        "Loading builtin operators is not supported < {} >.",
389                        record.id.clone()
390                    )
391                }
392            }
393        } else {
394            bail!(
395                ErrorKind::LoadingError,
396                "Missing URI for dynamically loaded Operator < {} >.",
397                record.id.clone()
398            )
399        }
400    }
401
402    /// Tries to load a Sink from the information passed within the
403    /// [`SinkRecord`](`SinkRecord`).
404    ///
405    /// # Errors
406    ///
407    /// It can fail because of:
408    /// - different versions of Zenoh-Flow used to build the sink
409    /// - different versions of the rust compiler used to build the sink
410    /// - the library does not contain the symbols
411    /// - the URI is missing
412    /// - the URI scheme is not known (so far only `file://` is known).
413    pub(crate) fn load_sink_constructor(&self, mut record: SinkRecord) -> Result<SinkConstructor> {
414        if let Some(uri) = &record.uri {
415            match parse_uri(uri)? {
416                ZFUri::File(file_path) => {
417                    let (library, constructor) = unsafe {
418                        self.load_node_from_file::<SinkFn>(
419                            NodeSymbol::Sink,
420                            file_path,
421                            &mut record.configuration,
422                        )?
423                    };
424
425                    Ok(SinkConstructor::new_dynamic(
426                        record,
427                        constructor,
428                        Arc::new(library),
429                    ))
430                }
431                ZFUri::Builtin(mw) => {
432                    let constructor = self.load_sink_from_builtin(mw)?;
433                    Ok(SinkConstructor::new_static(record, constructor))
434                }
435            }
436        } else {
437            bail!(
438                ErrorKind::LoadingError,
439                "Missing URI for dynamically loaded Sink < {} >.",
440                record.id.clone()
441            )
442        }
443    }
444
445    /// Wraps the configuration in case of an extension.
446    ///
447    /// # Errors
448    ///
449    /// An error variant is returned in case of:
450    /// - unable to parse the file path
451    fn wrap_configuration(
452        configuration: &mut Option<Configuration>,
453        config_key: String,
454        file_path: &Path,
455    ) -> Result<()> {
456        let mut new_config: serde_json::map::Map<String, Configuration> =
457            serde_json::map::Map::new();
458        let config = configuration.take();
459        new_config.insert(
460            config_key,
461            file_path
462                .to_str()
463                .ok_or_else(|| {
464                    zferror!(
465                        ErrorKind::LoadingError,
466                        "Unable parse file path < {:?} >.",
467                        file_path,
468                    )
469                })?
470                .into(),
471        );
472
473        if let Some(config) = config {
474            new_config.insert(String::from("configuration"), config);
475        }
476
477        *configuration = Some(new_config.into());
478        Ok(())
479    }
480}