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}