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::{c_void, 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::hal_ffi::HalDmaBufFunctions;
20
21#[cfg(feature = "dmabuf")]
22use edgefirst_tflite_sys::vx_ffi::VxDmaBufFunctions;
23
24#[cfg(feature = "camera_adaptor")]
25use edgefirst_tflite_sys::hal_ffi::HalCameraAdaptorFunctions;
26
27#[cfg(feature = "camera_adaptor")]
28use edgefirst_tflite_sys::vx_ffi::VxCameraAdaptorFunctions;
29
30// ---------------------------------------------------------------------------
31// DelegateOptions
32// ---------------------------------------------------------------------------
33
34/// Key-value options for configuring an external delegate.
35///
36/// # Examples
37///
38/// ```no_run
39/// use edgefirst_tflite::DelegateOptions;
40///
41/// let opts = DelegateOptions::new()
42///     .option("cache_file_path", "/tmp/vx_cache")
43///     .option("device_id", "0");
44/// ```
45#[derive(Debug, Default, Clone)]
46pub struct DelegateOptions {
47    options: Vec<(String, String)>,
48}
49
50impl DelegateOptions {
51    /// Create an empty set of delegate options.
52    #[must_use]
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Add a key-value option pair.
58    #[must_use]
59    pub fn option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
60        self.options.push((key.into(), value.into()));
61        self
62    }
63}
64
65// ---------------------------------------------------------------------------
66// Delegate
67// ---------------------------------------------------------------------------
68
69/// An external `TFLite` delegate for hardware acceleration.
70///
71/// Delegates are loaded from shared libraries that export the standard
72/// `tflite_plugin_create_delegate` / `tflite_plugin_destroy_delegate`
73/// entry points.
74///
75/// # Examples
76///
77/// ```no_run
78/// use edgefirst_tflite::{Delegate, DelegateOptions};
79///
80/// // Load delegate with default options
81/// let delegate = Delegate::load("libvx_delegate.so")?;
82///
83/// // Load delegate with options
84/// let delegate = Delegate::load_with_options(
85///     "libvx_delegate.so",
86///     &DelegateOptions::new()
87///         .option("cache_file_path", "/tmp/vx_cache")
88///         .option("device_id", "0"),
89/// )?;
90/// # Ok::<(), edgefirst_tflite::Error>(())
91/// ```
92#[allow(clippy::struct_field_names)]
93pub struct Delegate {
94    delegate: NonNull<TfLiteDelegate>,
95    free: unsafe extern "C" fn(*mut TfLiteDelegate),
96    // Keeps the delegate .so loaded for the delegate's lifetime.
97    _lib: libloading::Library,
98
99    #[cfg(feature = "dmabuf")]
100    hal_dmabuf_fns: Option<HalDmaBufFunctions>,
101
102    /// Inner delegate handle returned by `hal_dmabuf_get_instance()`.
103    ///
104    /// This is the opaque `hal_delegate_t` (`*mut c_void`) that HAL API
105    /// functions expect as their first argument. It is distinct from the
106    /// `TfLiteDelegate*` outer pointer and must be used for all HAL calls.
107    /// Both `DmaBuf` and `CameraAdaptor` share this same handle.
108    #[cfg(feature = "dmabuf")]
109    hal_delegate_handle: Option<*mut c_void>,
110
111    #[cfg(feature = "dmabuf")]
112    dmabuf_fns: Option<VxDmaBufFunctions>,
113
114    #[cfg(feature = "camera_adaptor")]
115    hal_camera_fns: Option<HalCameraAdaptorFunctions>,
116
117    #[cfg(feature = "camera_adaptor")]
118    camera_adaptor_fns: Option<VxCameraAdaptorFunctions>,
119}
120
121// SAFETY: `hal_delegate_handle` is a raw pointer obtained from
122// `hal_dmabuf_get_instance()`. The HAL contract guarantees this pointer
123// is valid and stable for the lifetime of the loaded delegate library.
124// `Delegate` is the sole owner and never shares the handle concurrently.
125#[cfg(feature = "dmabuf")]
126// SAFETY: See above — the handle is stable and not concurrently accessed.
127unsafe impl Send for Delegate {}
128#[cfg(feature = "dmabuf")]
129// SAFETY: All HAL API methods take `&self` and the underlying C functions
130// are thread-safe per the HAL contract.
131unsafe impl Sync for Delegate {}
132
133impl Delegate {
134    /// Load an external delegate from a shared library with default options.
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if the library cannot be loaded, required symbols
139    /// are missing, or the delegate returns a null pointer.
140    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
141        Self::load_with_options(path, &DelegateOptions::default())
142    }
143
144    /// Load an external delegate with configuration options.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if the library cannot be loaded, required symbols
149    /// are missing, the delegate returns a null pointer, or any option key
150    /// or value contains an interior NUL byte.
151    pub fn load_with_options(path: impl AsRef<Path>, options: &DelegateOptions) -> Result<Self> {
152        // SAFETY: Loading the shared library via `libloading`. The library is
153        // kept alive in `_lib` for the lifetime of the `Delegate`.
154        let lib =
155            unsafe { libloading::Library::new(path.as_ref().as_os_str()) }.map_err(Error::from)?;
156
157        // SAFETY: Resolving the `tflite_plugin_create_delegate` symbol from
158        // the loaded library. The library is valid and loaded above.
159        let create_fn = unsafe {
160            lib.get::<unsafe extern "C" fn(
161                *const *const std::os::raw::c_char,
162                *const *const std::os::raw::c_char,
163                usize,
164                Option<unsafe extern "C" fn(*const std::os::raw::c_char)>,
165            ) -> *mut TfLiteDelegate>(b"tflite_plugin_create_delegate")
166        }
167        .map_err(Error::from)?;
168
169        // SAFETY: Resolving the `tflite_plugin_destroy_delegate` symbol from
170        // the same loaded library.
171        let destroy_fn = unsafe {
172            lib.get::<unsafe extern "C" fn(*mut TfLiteDelegate)>(b"tflite_plugin_destroy_delegate")
173        }
174        .map_err(Error::from)?;
175
176        // Convert options to C string arrays.
177        let (keys_c, values_c): (Vec<CString>, Vec<CString>) = options
178            .options
179            .iter()
180            .map(|(k, v)| {
181                Ok((
182                    CString::new(k.as_str()).map_err(|_| {
183                        Error::invalid_argument(format!(
184                            "option key \"{k}\" contains interior NUL byte"
185                        ))
186                    })?,
187                    CString::new(v.as_str()).map_err(|_| {
188                        Error::invalid_argument(format!(
189                            "option value \"{v}\" contains interior NUL byte"
190                        ))
191                    })?,
192                ))
193            })
194            .collect::<Result<Vec<_>>>()?
195            .into_iter()
196            .unzip();
197        let keys_ptrs: Vec<*const std::os::raw::c_char> =
198            keys_c.iter().map(|c| c.as_ptr()).collect();
199        let values_ptrs: Vec<*const std::os::raw::c_char> =
200            values_c.iter().map(|c| c.as_ptr()).collect();
201
202        // SAFETY: `create_fn` is a valid symbol resolved above. `keys_ptrs`
203        // and `values_ptrs` point to valid NUL-terminated C strings (from
204        // `CString::new`), or null when empty. `keys_c` and `values_c` are
205        // alive for this call, keeping the pointers valid.
206        let raw = unsafe {
207            create_fn(
208                if keys_ptrs.is_empty() {
209                    ptr::null()
210                } else {
211                    keys_ptrs.as_ptr()
212                },
213                if values_ptrs.is_empty() {
214                    ptr::null()
215                } else {
216                    values_ptrs.as_ptr()
217                },
218                options.options.len(),
219                None,
220            )
221        };
222
223        let delegate = NonNull::new(raw)
224            .ok_or_else(|| Error::null_pointer("tflite_plugin_create_delegate returned null"))?;
225
226        // Copy the destroy function pointer before lib is stored.
227        let free = *destroy_fn;
228
229        // Probe for delegate DMA-BUF extensions.
230        #[cfg(feature = "dmabuf")]
231        // SAFETY: `lib` is a valid loaded library. `try_load` resolves
232        // optional symbols; missing symbols return `None`, not UB.
233        let hal_dmabuf_fns = unsafe { HalDmaBufFunctions::try_load(&lib) };
234
235        // Call `hal_dmabuf_get_instance()` to obtain the inner delegate handle.
236        // This is the opaque `hal_delegate_t` pointer that all HAL API calls
237        // expect. A null result means HAL is not available on this device.
238        #[cfg(feature = "dmabuf")]
239        let hal_delegate_handle: Option<*mut c_void> = hal_dmabuf_fns.as_ref().and_then(|fns| {
240            // SAFETY: `get_instance` is a valid function pointer loaded from
241            // the delegate library. It takes no arguments and returns an opaque
242            // handle that is valid for the lifetime of the library.
243            let ptr = unsafe { (fns.get_instance)() };
244            if ptr.is_null() {
245                None
246            } else {
247                Some(ptr)
248            }
249        });
250
251        #[cfg(feature = "dmabuf")]
252        // SAFETY: Same as above — resolves optional VxDelegate DMA-BUF
253        // symbols as a fallback for delegates that haven't adopted the
254        // HAL DMA-BUF API yet.
255        let dmabuf_fns = unsafe { VxDmaBufFunctions::try_load(&lib) };
256
257        #[cfg(feature = "camera_adaptor")]
258        // SAFETY: Same as above — resolves optional HAL Camera Adaptor
259        // symbols from the loaded library.
260        let hal_camera_fns = unsafe { HalCameraAdaptorFunctions::try_load(&lib) };
261
262        #[cfg(feature = "camera_adaptor")]
263        // SAFETY: Same as `VxDmaBufFunctions::try_load` above — resolves
264        // optional CameraAdaptor symbols from the loaded library.
265        let camera_adaptor_fns = unsafe { VxCameraAdaptorFunctions::try_load(&lib) };
266
267        Ok(Self {
268            delegate,
269            free,
270            _lib: lib,
271            #[cfg(feature = "dmabuf")]
272            hal_dmabuf_fns,
273            #[cfg(feature = "dmabuf")]
274            hal_delegate_handle,
275            #[cfg(feature = "dmabuf")]
276            dmabuf_fns,
277            #[cfg(feature = "camera_adaptor")]
278            hal_camera_fns,
279            #[cfg(feature = "camera_adaptor")]
280            camera_adaptor_fns,
281        })
282    }
283
284    /// Returns the raw delegate pointer.
285    ///
286    /// This is an escape hatch for advanced use cases that need direct
287    /// FFI access to the delegate.
288    #[must_use]
289    pub fn as_ptr(&self) -> *mut TfLiteDelegate {
290        self.delegate.as_ptr()
291    }
292
293    /// Access DMA-BUF extensions if available on this delegate.
294    ///
295    /// Returns `Some` if the delegate exports either the HAL Delegate
296    /// DMA-BUF API (`hal_dmabuf_*`) or the legacy `VxDelegate` DMA-BUF API.
297    /// The HAL API is preferred when both are available.
298    #[cfg(feature = "dmabuf")]
299    #[must_use]
300    pub fn dmabuf(&self) -> Option<crate::dmabuf::DmaBuf<'_>> {
301        if self.hal_dmabuf_fns.is_some() || self.dmabuf_fns.is_some() {
302            Some(crate::dmabuf::DmaBuf::new(
303                self.delegate,
304                self.hal_delegate_handle,
305                self.hal_dmabuf_fns.as_ref(),
306                self.dmabuf_fns.as_ref(),
307            ))
308        } else {
309            None
310        }
311    }
312
313    /// Returns `true` if this delegate supports DMA-BUF zero-copy.
314    #[cfg(feature = "dmabuf")]
315    #[must_use]
316    pub fn has_dmabuf(&self) -> bool {
317        self.hal_dmabuf_fns.is_some() || self.dmabuf_fns.is_some()
318    }
319
320    /// Access `CameraAdaptor` extensions if available on this delegate.
321    ///
322    /// Returns `Some` if the delegate exports either the HAL Delegate
323    /// Camera Adaptor API (`hal_camera_adaptor_*`) or the legacy
324    /// `VxDelegate` `CameraAdaptor` API. The HAL API is preferred when
325    /// both are available.
326    #[cfg(feature = "camera_adaptor")]
327    #[must_use]
328    pub fn camera_adaptor(&self) -> Option<crate::camera_adaptor::CameraAdaptor<'_>> {
329        if self.hal_camera_fns.is_some() || self.camera_adaptor_fns.is_some() {
330            // The CameraAdaptor HAL API reuses the same inner delegate handle
331            // as the DMA-BUF HAL API — there is no separate
332            // `hal_camera_adaptor_get_instance`.
333            #[cfg(feature = "dmabuf")]
334            let hal_handle = self.hal_delegate_handle;
335            #[cfg(not(feature = "dmabuf"))]
336            let hal_handle: Option<*mut std::ffi::c_void> = None;
337
338            Some(crate::camera_adaptor::CameraAdaptor::new(
339                self.delegate,
340                hal_handle,
341                self.hal_camera_fns.as_ref(),
342                self.camera_adaptor_fns.as_ref(),
343            ))
344        } else {
345            None
346        }
347    }
348
349    /// Returns `true` if this delegate supports `CameraAdaptor`.
350    #[cfg(feature = "camera_adaptor")]
351    #[must_use]
352    pub fn has_camera_adaptor(&self) -> bool {
353        self.hal_camera_fns.is_some() || self.camera_adaptor_fns.is_some()
354    }
355}
356
357#[allow(clippy::missing_fields_in_debug)]
358impl std::fmt::Debug for Delegate {
359    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360        let mut d = f.debug_struct("Delegate");
361        d.field("ptr", &self.delegate);
362
363        #[cfg(feature = "dmabuf")]
364        d.field("has_hal_dmabuf", &self.hal_dmabuf_fns.is_some());
365
366        #[cfg(feature = "dmabuf")]
367        d.field(
368            "has_hal_delegate_handle",
369            &self.hal_delegate_handle.is_some(),
370        );
371
372        #[cfg(feature = "dmabuf")]
373        d.field("has_vx_dmabuf", &self.dmabuf_fns.is_some());
374
375        #[cfg(feature = "camera_adaptor")]
376        d.field("has_hal_camera_adaptor", &self.hal_camera_fns.is_some());
377
378        #[cfg(feature = "camera_adaptor")]
379        d.field("has_vx_camera_adaptor", &self.camera_adaptor_fns.is_some());
380
381        d.finish_non_exhaustive()
382    }
383}
384
385impl Drop for Delegate {
386    fn drop(&mut self) {
387        // SAFETY: The delegate pointer was created by `tflite_plugin_create_delegate`
388        // and `free` is the matching `tflite_plugin_destroy_delegate` from the same
389        // library, which is still loaded (held by `_lib`).
390        unsafe { (self.free)(self.delegate.as_ptr()) };
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[test]
399    fn new_creates_empty_options() {
400        let opts = DelegateOptions::new();
401        let debug = format!("{opts:?}");
402        assert_eq!(debug, "DelegateOptions { options: [] }");
403    }
404
405    #[test]
406    fn builder_chaining() {
407        let opts = DelegateOptions::new().option("a", "1").option("b", "2");
408        assert_eq!(opts.options.len(), 2);
409    }
410
411    #[test]
412    fn default_matches_new() {
413        let from_new = format!("{:?}", DelegateOptions::new());
414        let from_default = format!("{:?}", DelegateOptions::default());
415        assert_eq!(from_new, from_default);
416    }
417
418    #[test]
419    fn clone_produces_equal_values() {
420        let opts = DelegateOptions::new().option("key", "value");
421        let cloned = opts.clone();
422        assert_eq!(format!("{opts:?}"), format!("{cloned:?}"));
423    }
424
425    #[test]
426    fn debug_formatting_not_empty() {
427        let opts = DelegateOptions::new().option("cache", "/tmp");
428        let debug = format!("{opts:?}");
429        assert!(!debug.is_empty());
430        assert!(debug.contains("DelegateOptions"));
431        assert!(debug.contains("cache"));
432        assert!(debug.contains("/tmp"));
433    }
434}