Skip to main content

edgefirst_tflite/
delegate.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 Au-Zone Technologies. All Rights Reserved.
3
4//! Delegate loading with configuration options.
5//!
6//! Delegates provide hardware acceleration for `TFLite` inference. The most
7//! common delegate for i.MX platforms is the `VxDelegate`, which offloads
8//! operations to the NPU.
9
10use std::ffi::CString;
11use std::path::Path;
12use std::ptr::{self, NonNull};
13
14use edgefirst_tflite_sys::TfLiteDelegate;
15
16use crate::error::{Error, Result};
17
18#[cfg(feature = "dmabuf")]
19use edgefirst_tflite_sys::vx_ffi::VxDmaBufFunctions;
20
21#[cfg(feature = "camera_adaptor")]
22use edgefirst_tflite_sys::vx_ffi::VxCameraAdaptorFunctions;
23
24// ---------------------------------------------------------------------------
25// DelegateOptions
26// ---------------------------------------------------------------------------
27
28/// Key-value options for configuring an external delegate.
29///
30/// # Examples
31///
32/// ```no_run
33/// use edgefirst_tflite::DelegateOptions;
34///
35/// let opts = DelegateOptions::new()
36///     .option("cache_file_path", "/tmp/vx_cache")
37///     .option("device_id", "0");
38/// ```
39#[derive(Debug, Default, Clone)]
40pub struct DelegateOptions {
41    options: Vec<(String, String)>,
42}
43
44impl DelegateOptions {
45    /// Create an empty set of delegate options.
46    #[must_use]
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Add a key-value option pair.
52    #[must_use]
53    pub fn option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
54        self.options.push((key.into(), value.into()));
55        self
56    }
57}
58
59// ---------------------------------------------------------------------------
60// Delegate
61// ---------------------------------------------------------------------------
62
63/// An external `TFLite` delegate for hardware acceleration.
64///
65/// Delegates are loaded from shared libraries that export the standard
66/// `tflite_plugin_create_delegate` / `tflite_plugin_destroy_delegate`
67/// entry points.
68///
69/// # Examples
70///
71/// ```no_run
72/// use edgefirst_tflite::{Delegate, DelegateOptions};
73///
74/// // Load delegate with default options
75/// let delegate = Delegate::load("libvx_delegate.so")?;
76///
77/// // Load delegate with options
78/// let delegate = Delegate::load_with_options(
79///     "libvx_delegate.so",
80///     &DelegateOptions::new()
81///         .option("cache_file_path", "/tmp/vx_cache")
82///         .option("device_id", "0"),
83/// )?;
84/// # Ok::<(), edgefirst_tflite::Error>(())
85/// ```
86#[allow(clippy::struct_field_names)]
87pub struct Delegate {
88    delegate: NonNull<TfLiteDelegate>,
89    free: unsafe extern "C" fn(*mut TfLiteDelegate),
90    // Keeps the delegate .so loaded for the delegate's lifetime.
91    _lib: libloading::Library,
92
93    #[cfg(feature = "dmabuf")]
94    dmabuf_fns: Option<VxDmaBufFunctions>,
95
96    #[cfg(feature = "camera_adaptor")]
97    camera_adaptor_fns: Option<VxCameraAdaptorFunctions>,
98}
99
100impl Delegate {
101    /// Load an external delegate from a shared library with default options.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the library cannot be loaded, required symbols
106    /// are missing, or the delegate returns a null pointer.
107    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
108        Self::load_with_options(path, &DelegateOptions::default())
109    }
110
111    /// Load an external delegate with configuration options.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if the library cannot be loaded, required symbols
116    /// are missing, the delegate returns a null pointer, or any option key
117    /// or value contains an interior NUL byte.
118    pub fn load_with_options(path: impl AsRef<Path>, options: &DelegateOptions) -> Result<Self> {
119        // SAFETY: Loading the shared library via `libloading`. The library is
120        // kept alive in `_lib` for the lifetime of the `Delegate`.
121        let lib =
122            unsafe { libloading::Library::new(path.as_ref().as_os_str()) }.map_err(Error::from)?;
123
124        // SAFETY: Resolving the `tflite_plugin_create_delegate` symbol from
125        // the loaded library. The library is valid and loaded above.
126        let create_fn = unsafe {
127            lib.get::<unsafe extern "C" fn(
128                *const *const std::os::raw::c_char,
129                *const *const std::os::raw::c_char,
130                usize,
131                Option<unsafe extern "C" fn(*const std::os::raw::c_char)>,
132            ) -> *mut TfLiteDelegate>(b"tflite_plugin_create_delegate")
133        }
134        .map_err(Error::from)?;
135
136        // SAFETY: Resolving the `tflite_plugin_destroy_delegate` symbol from
137        // the same loaded library.
138        let destroy_fn = unsafe {
139            lib.get::<unsafe extern "C" fn(*mut TfLiteDelegate)>(b"tflite_plugin_destroy_delegate")
140        }
141        .map_err(Error::from)?;
142
143        // Convert options to C string arrays.
144        let (keys_c, values_c): (Vec<CString>, Vec<CString>) = options
145            .options
146            .iter()
147            .map(|(k, v)| {
148                Ok((
149                    CString::new(k.as_str()).map_err(|_| {
150                        Error::invalid_argument(format!(
151                            "option key \"{k}\" contains interior NUL byte"
152                        ))
153                    })?,
154                    CString::new(v.as_str()).map_err(|_| {
155                        Error::invalid_argument(format!(
156                            "option value \"{v}\" contains interior NUL byte"
157                        ))
158                    })?,
159                ))
160            })
161            .collect::<Result<Vec<_>>>()?
162            .into_iter()
163            .unzip();
164        let keys_ptrs: Vec<*const std::os::raw::c_char> =
165            keys_c.iter().map(|c| c.as_ptr()).collect();
166        let values_ptrs: Vec<*const std::os::raw::c_char> =
167            values_c.iter().map(|c| c.as_ptr()).collect();
168
169        // SAFETY: `create_fn` is a valid symbol resolved above. `keys_ptrs`
170        // and `values_ptrs` point to valid NUL-terminated C strings (from
171        // `CString::new`), or null when empty. `keys_c` and `values_c` are
172        // alive for this call, keeping the pointers valid.
173        let raw = unsafe {
174            create_fn(
175                if keys_ptrs.is_empty() {
176                    ptr::null()
177                } else {
178                    keys_ptrs.as_ptr()
179                },
180                if values_ptrs.is_empty() {
181                    ptr::null()
182                } else {
183                    values_ptrs.as_ptr()
184                },
185                options.options.len(),
186                None,
187            )
188        };
189
190        let delegate = NonNull::new(raw)
191            .ok_or_else(|| Error::null_pointer("tflite_plugin_create_delegate returned null"))?;
192
193        // Copy the destroy function pointer before lib is stored.
194        let free = *destroy_fn;
195
196        // Probe for VxDelegate extensions.
197        #[cfg(feature = "dmabuf")]
198        // SAFETY: `lib` is a valid loaded library. `try_load` resolves
199        // optional symbols; missing symbols return `None`, not UB.
200        let dmabuf_fns = unsafe { VxDmaBufFunctions::try_load(&lib) };
201
202        #[cfg(feature = "camera_adaptor")]
203        // SAFETY: Same as `VxDmaBufFunctions::try_load` above — resolves
204        // optional CameraAdaptor symbols from the loaded library.
205        let camera_adaptor_fns = unsafe { VxCameraAdaptorFunctions::try_load(&lib) };
206
207        Ok(Self {
208            delegate,
209            free,
210            _lib: lib,
211            #[cfg(feature = "dmabuf")]
212            dmabuf_fns,
213            #[cfg(feature = "camera_adaptor")]
214            camera_adaptor_fns,
215        })
216    }
217
218    /// Returns the raw delegate pointer.
219    ///
220    /// This is an escape hatch for advanced use cases that need direct
221    /// FFI access to the delegate.
222    #[must_use]
223    pub fn as_ptr(&self) -> *mut TfLiteDelegate {
224        self.delegate.as_ptr()
225    }
226
227    /// Access DMA-BUF extensions if available on this delegate.
228    #[cfg(feature = "dmabuf")]
229    #[must_use]
230    pub fn dmabuf(&self) -> Option<crate::dmabuf::DmaBuf<'_>> {
231        self.dmabuf_fns
232            .as_ref()
233            .map(|fns| crate::dmabuf::DmaBuf::new(self.delegate, fns))
234    }
235
236    /// Returns `true` if this delegate supports DMA-BUF zero-copy.
237    #[cfg(feature = "dmabuf")]
238    #[must_use]
239    pub fn has_dmabuf(&self) -> bool {
240        self.dmabuf_fns.is_some()
241    }
242
243    /// Access `CameraAdaptor` extensions if available on this delegate.
244    #[cfg(feature = "camera_adaptor")]
245    #[must_use]
246    pub fn camera_adaptor(&self) -> Option<crate::camera_adaptor::CameraAdaptor<'_>> {
247        self.camera_adaptor_fns
248            .as_ref()
249            .map(|fns| crate::camera_adaptor::CameraAdaptor::new(self.delegate, fns))
250    }
251
252    /// Returns `true` if this delegate supports `CameraAdaptor`.
253    #[cfg(feature = "camera_adaptor")]
254    #[must_use]
255    pub fn has_camera_adaptor(&self) -> bool {
256        self.camera_adaptor_fns.is_some()
257    }
258}
259
260#[allow(clippy::missing_fields_in_debug)]
261impl std::fmt::Debug for Delegate {
262    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263        let mut d = f.debug_struct("Delegate");
264        d.field("ptr", &self.delegate);
265
266        #[cfg(feature = "dmabuf")]
267        d.field("has_dmabuf", &self.dmabuf_fns.is_some());
268
269        #[cfg(feature = "camera_adaptor")]
270        d.field("has_camera_adaptor", &self.camera_adaptor_fns.is_some());
271
272        d.finish_non_exhaustive()
273    }
274}
275
276impl Drop for Delegate {
277    fn drop(&mut self) {
278        // SAFETY: The delegate pointer was created by `tflite_plugin_create_delegate`
279        // and `free` is the matching `tflite_plugin_destroy_delegate` from the same
280        // library, which is still loaded (held by `_lib`).
281        unsafe { (self.free)(self.delegate.as_ptr()) };
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn new_creates_empty_options() {
291        let opts = DelegateOptions::new();
292        let debug = format!("{opts:?}");
293        assert_eq!(debug, "DelegateOptions { options: [] }");
294    }
295
296    #[test]
297    fn builder_chaining() {
298        let opts = DelegateOptions::new().option("a", "1").option("b", "2");
299        assert_eq!(opts.options.len(), 2);
300    }
301
302    #[test]
303    fn default_matches_new() {
304        let from_new = format!("{:?}", DelegateOptions::new());
305        let from_default = format!("{:?}", DelegateOptions::default());
306        assert_eq!(from_new, from_default);
307    }
308
309    #[test]
310    fn clone_produces_equal_values() {
311        let opts = DelegateOptions::new().option("key", "value");
312        let cloned = opts.clone();
313        assert_eq!(format!("{opts:?}"), format!("{cloned:?}"));
314    }
315
316    #[test]
317    fn debug_formatting_not_empty() {
318        let opts = DelegateOptions::new().option("cache", "/tmp");
319        let debug = format!("{opts:?}");
320        assert!(!debug.is_empty());
321        assert!(debug.contains("DelegateOptions"));
322        assert!(debug.contains("cache"));
323        assert!(debug.contains("/tmp"));
324    }
325}