Skip to main content

ove/
macros.rs

1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! User-facing macros for the oveRTOS Rust SDK.
8//!
9//! All `#[macro_export]` macros are re-exported at crate root, so the
10//! location of this file is purely organizational.
11
12/// Generate `Drop` and thread-safety impls (plus `Debug` for the
13/// non-generic forms) for an oveRTOS handle wrapper struct that stores a
14/// nullable handle in a field named `handle`.
15///
16/// # Variants
17///
18/// - `(Name, destroy_fn, deinit_fn)` — emits `Debug`, `Drop`, `Send`, `Sync`.
19/// - `(Name, destroy_fn, deinit_fn, send_only)` — emits `Debug`, `Drop`,
20///   `Send`. No `Sync` impl.
21/// - `(Name<const N: usize>, destroy_fn, deinit_fn)` — const-generic;
22///   emits `Drop`, `Send`, `Sync` only (no `Debug`).
23#[doc(hidden)]
24#[macro_export]
25macro_rules! ove_handle_impl {
26    // Non-generic, Send + Sync
27    ($name:ident, $destroy:ident, $deinit:ident) => {
28        $crate::ove_handle_impl!(@debug $name);
29        $crate::ove_handle_impl!(@drop $name, $destroy, $deinit);
30        unsafe impl Send for $name {}
31        unsafe impl Sync for $name {}
32    };
33
34    // Non-generic, Send only (no Sync)
35    ($name:ident, $destroy:ident, $deinit:ident, send_only) => {
36        $crate::ove_handle_impl!(@debug $name);
37        $crate::ove_handle_impl!(@drop $name, $destroy, $deinit);
38        unsafe impl Send for $name {}
39    };
40
41    // Const-generic, Send + Sync
42    ($name:ident<const $N:ident: usize>, $destroy:ident, $deinit:ident) => {
43        impl<const $N: usize> Drop for $name<$N> {
44            fn drop(&mut self) {
45                if self.handle.is_null() { return; }
46                #[cfg(not(zero_heap))]
47                unsafe { $crate::bindings::$destroy(self.handle) }
48                #[cfg(zero_heap)]
49                unsafe { $crate::bindings::$deinit(self.handle) }
50            }
51        }
52        unsafe impl<const $N: usize> Send for $name<$N> {}
53        unsafe impl<const $N: usize> Sync for $name<$N> {}
54    };
55
56    (@debug $name:ident) => {
57        impl core::fmt::Debug for $name {
58            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59                f.debug_struct(stringify!($name))
60                    .field("handle", &format_args!("{:p}", self.handle))
61                    .finish()
62            }
63        }
64    };
65
66    (@drop $name:ident, $destroy:ident, $deinit:ident) => {
67        impl Drop for $name {
68            fn drop(&mut self) {
69                if self.handle.is_null() { return; }
70                #[cfg(not(zero_heap))]
71                unsafe { $crate::bindings::$destroy(self.handle) }
72                #[cfg(zero_heap)]
73                unsafe { $crate::bindings::$deinit(self.handle) }
74            }
75        }
76    };
77}
78
79// Legacy `log_fmt!`/`log_inf!`/`log_wrn!`/`log_err!` macros deleted in
80// favour of the `log` crate facade.  See `ove::log` for the `log::Log`
81// backend; call `ove::log::init()` once in `ove_main`, then use
82// `log::info!`/`warn!`/`error!`/`debug!`/`trace!` like any other Rust
83// crate.
84
85/// Declare a safe accessor for TFLite model data generated by `convert.py`.
86///
87/// Generates the `extern "C"` linkage for the `_model_data` and
88/// `_model_data_len` symbols and wraps them in a function returning
89/// `&'static [u8]`.  No `extern` or `unsafe` needed in application code.
90///
91/// # Example
92///
93/// ```ignore
94/// ove::model_data!(preprocessor_model,
95///     g_audio_preprocessor_int8_model_data,
96///     g_audio_preprocessor_int8_model_data_len);
97///
98/// let data: &[u8] = preprocessor_model();
99/// ```
100#[macro_export]
101macro_rules! model_data {
102    ($fn_name:ident, $data_sym:ident, $len_sym:ident) => {
103        unsafe extern "C" {
104            safe static $data_sym: u8;
105            safe static $len_sym: u32;
106        }
107        fn $fn_name() -> &'static [u8] {
108            unsafe { core::slice::from_raw_parts(&$data_sym, $len_sym as usize) }
109        }
110    };
111}
112
113// Legacy `ove::main!(fn_name)` declarative macro deleted in favour of
114// the `#[ove::main]` proc-macro attribute provided by the `ove-macros`
115// crate (re-exported at the crate root in `lib.rs`).
116//
117// Migration:
118//   fn app_main() { ... }
119//   ove::main!(app_main);
120// becomes
121//   #[ove::main]
122//   fn app_main() { ... }
123//
124// Lifetime caveat (same for both forms): anything worker threads
125// access after `app_main` returns must have `'static` storage — use
126// `static`/`InitCell`/`InitMut` cells.  A stack-local value in
127// `app_main` is dropped when the function unwinds; a thread that
128// captured a reference into it dangles.  On FreeRTOS the failure is
129// immediate (scheduler reclaims the main stack); on POSIX/NuttX/Zephyr
130// the UB is latent.
131
132// ---------------------------------------------------------------------------
133// Unified creation macros
134//
135// These macros work in both heap and zero-heap modes. In heap mode they call
136// `new()`. In zero-heap mode they declare function-local `static` storage
137// and call `from_static()`.
138// ---------------------------------------------------------------------------
139
140/// Create a [`crate::Mutex<T>`] around `$val` that works in both heap and
141/// zero-heap modes.
142///
143/// Use `()` as the value for a "no-data" lock (i.e. `Mutex<()>` used
144/// purely to synchronise access to other state) — matches
145/// `std::sync::Mutex<()>` / `parking_lot::Mutex<()>` convention.
146#[cfg(has_sync)]
147#[macro_export]
148macro_rules! mutex {
149    ($val:expr) => {{
150        #[cfg(not(zero_heap))]
151        {
152            $crate::Mutex::new($val).unwrap()
153        }
154        #[cfg(zero_heap)]
155        {
156            static mut _S: $crate::ffi::ove_mutex_storage_t = unsafe { core::mem::zeroed() };
157            unsafe { $crate::Mutex::from_static(core::ptr::addr_of_mut!(_S), $val) }.unwrap()
158        }
159    }};
160}
161
162/// Create a [`crate::RecursiveMutex`] that works in both heap and zero-heap modes.
163#[cfg(has_sync)]
164#[macro_export]
165macro_rules! recursive_mutex {
166    () => {{
167        #[cfg(not(zero_heap))]
168        {
169            $crate::RecursiveMutex::new().unwrap()
170        }
171        #[cfg(zero_heap)]
172        {
173            static mut _S: $crate::ffi::ove_mutex_storage_t = unsafe { core::mem::zeroed() };
174            unsafe { $crate::RecursiveMutex::from_static(core::ptr::addr_of_mut!(_S)) }.unwrap()
175        }
176    }};
177}
178
179/// Create a [`crate::Semaphore`] that works in both heap and zero-heap modes.
180#[cfg(has_sync)]
181#[macro_export]
182macro_rules! semaphore {
183    ($initial:expr, $max:expr) => {{
184        #[cfg(not(zero_heap))]
185        {
186            $crate::Semaphore::new($initial, $max).unwrap()
187        }
188        #[cfg(zero_heap)]
189        {
190            static mut _S: $crate::ffi::ove_sem_storage_t = unsafe { core::mem::zeroed() };
191            unsafe { $crate::Semaphore::from_static(core::ptr::addr_of_mut!(_S), $initial, $max) }
192                .unwrap()
193        }
194    }};
195}
196
197/// Create an [`crate::Event`] that works in both heap and zero-heap modes.
198#[cfg(has_sync)]
199#[macro_export]
200macro_rules! event {
201    () => {{
202        #[cfg(not(zero_heap))]
203        {
204            $crate::Event::new().unwrap()
205        }
206        #[cfg(zero_heap)]
207        {
208            static mut _S: $crate::ffi::ove_event_storage_t = unsafe { core::mem::zeroed() };
209            unsafe { $crate::Event::from_static(core::ptr::addr_of_mut!(_S)) }.unwrap()
210        }
211    }};
212}
213
214/// Create a [`crate::CondVar`] that works in both heap and zero-heap modes.
215#[cfg(has_sync)]
216#[macro_export]
217macro_rules! condvar {
218    () => {{
219        #[cfg(not(zero_heap))]
220        {
221            $crate::CondVar::new().unwrap()
222        }
223        #[cfg(zero_heap)]
224        {
225            static mut _S: $crate::ffi::ove_condvar_storage_t = unsafe { core::mem::zeroed() };
226            unsafe { $crate::CondVar::from_static(core::ptr::addr_of_mut!(_S)) }.unwrap()
227        }
228    }};
229}
230
231/// Create an [`crate::EventGroup`] that works in both heap and zero-heap modes.
232#[cfg(has_eventgroup)]
233#[macro_export]
234macro_rules! eventgroup {
235    () => {{
236        #[cfg(not(zero_heap))]
237        {
238            $crate::EventGroup::new().unwrap()
239        }
240        #[cfg(zero_heap)]
241        {
242            static mut _S: $crate::ffi::ove_eventgroup_storage_t = unsafe { core::mem::zeroed() };
243            unsafe { $crate::EventGroup::from_static(core::ptr::addr_of_mut!(_S)) }.unwrap()
244        }
245    }};
246}
247
248/// Create a [`crate::audio::Graph`] that works in both heap and zero-heap modes.
249///
250/// Mirrors the C `ove_audio_graph_create(pg, frames, nodes, channels,
251/// sample_bytes)` macro: in zero-heap mode emits a per-call-site `static`
252/// backing array sized by `nodes * frames * channels * sample_bytes` bytes
253/// and attaches it to the graph automatically; in heap mode just calls
254/// [`crate::audio::Graph::new`].  Yields a [`crate::Result`] wrapping a
255/// [`crate::audio::Graph`].
256///
257/// All arguments must be constant expressions.
258///
259/// # Example
260/// ```ignore
261/// // 2 non-sink nodes (source + processor), 512 frames, mono, S16 (2 bytes).
262/// let mut graph = ove::audio_graph!(512, 2, 1, 2)?;
263/// ```
264#[cfg(has_audio)]
265#[macro_export]
266macro_rules! audio_graph {
267    ($frames:expr, $nodes:expr, $channels:expr, $sample_bytes:expr) => {{
268        #[cfg(not(zero_heap))]
269        {
270            $crate::audio::Graph::new($frames)
271        }
272        #[cfg(zero_heap)]
273        {
274            const _STORAGE_BYTES: usize =
275                ($nodes) * ($frames) as usize * ($channels) * ($sample_bytes);
276            #[repr(C, align(4))]
277            struct _AudioBufStorage([u8; _STORAGE_BYTES]);
278            static mut _AUDIO_BUF: _AudioBufStorage = _AudioBufStorage([0; _STORAGE_BYTES]);
279            let slice: &'static mut [u8] = unsafe {
280                core::slice::from_raw_parts_mut(
281                    core::ptr::addr_of_mut!(_AUDIO_BUF) as *mut u8,
282                    _STORAGE_BYTES,
283                )
284            };
285            $crate::audio::Graph::new_with_storage($frames, slice)
286        }
287    }};
288}
289
290/// Create a [`crate::Queue`] that works in both heap and zero-heap modes.
291///
292/// # Example
293/// ```ignore
294/// let q = ove::queue!(u32, 8);
295/// ```
296#[cfg(has_queue)]
297#[macro_export]
298macro_rules! queue {
299    ($T:ty, $N:expr) => {{
300        #[cfg(not(zero_heap))]
301        {
302            $crate::Queue::<$T, $N>::new().unwrap()
303        }
304        #[cfg(zero_heap)]
305        {
306            static mut _S: $crate::ffi::ove_queue_storage_t = unsafe { core::mem::zeroed() };
307            static mut _B: [core::mem::MaybeUninit<$T>; $N] =
308                unsafe { core::mem::MaybeUninit::uninit().assume_init() };
309            unsafe {
310                $crate::Queue::<$T, $N>::from_static(
311                    core::ptr::addr_of_mut!(_S),
312                    core::ptr::addr_of_mut!(_B) as *mut _,
313                )
314            }
315            .unwrap()
316        }
317    }};
318}
319
320/// Create a [`crate::Timer`] that works in both heap and zero-heap modes.
321///
322/// # Example
323/// ```ignore
324/// let t = ove::timer!(my_callback, 100, false);
325/// ```
326#[cfg(has_timer)]
327#[macro_export]
328macro_rules! timer {
329    ($callback:expr, $period_ms:expr, $one_shot:expr) => {{
330        #[cfg(not(zero_heap))]
331        {
332            $crate::Timer::new($callback, $period_ms, $one_shot).unwrap()
333        }
334        #[cfg(zero_heap)]
335        {
336            static mut _S: $crate::ffi::ove_timer_storage_t = unsafe { core::mem::zeroed() };
337            unsafe {
338                $crate::Timer::from_static(
339                    core::ptr::addr_of_mut!(_S),
340                    $callback,
341                    $period_ms,
342                    $one_shot,
343                )
344            }
345            .unwrap()
346        }
347    }};
348}
349
350/// Create a [`crate::JoinHandle`] for a thread that works in both heap
351/// and zero-heap modes.
352///
353/// Uses the safe `fn()` entry pattern (no `StopToken`). The name is
354/// automatically null-terminated.  In zero-heap mode the storage is
355/// emitted as a `static $crate::ThreadStorage<$stack>` and passed by
356/// reference to [`crate::Builder::spawn_static_simple`] — no `static mut`,
357/// no `addr_of_mut!`, no `unsafe` in user-visible expansion.
358///
359/// # Example
360/// ```ignore
361/// let h = ove::thread!("worker", my_entry, Priority::Normal, 4096);
362/// h.detach();  // fire-and-forget; or store in InitCell<JoinHandle<()>>.
363/// ```
364#[macro_export]
365macro_rules! thread {
366    ($name:expr, $entry:expr, $prio:expr, $stack:expr) => {{
367        // SAFETY: `concat!($name, "\0")` is a comptime literal that ends
368        // with a single nul; the caller's $name must not contain inner
369        // nuls (responsibility of the macro caller — same as today).
370        let _name = unsafe {
371            core::ffi::CStr::from_bytes_with_nul_unchecked(concat!($name, "\0").as_bytes())
372        };
373        #[cfg(not(zero_heap))]
374        {
375            $crate::Thread::builder()
376                .name(_name)
377                .priority($prio)
378                .stack_size($stack)
379                .spawn_simple($entry)
380                .unwrap()
381        }
382        #[cfg(zero_heap)]
383        {
384            static _STORAGE: $crate::ThreadStorage<$stack> = $crate::ThreadStorage::new();
385            $crate::Thread::builder()
386                .name(_name)
387                .priority($prio)
388                .spawn_static_simple(&_STORAGE, $entry)
389                .unwrap()
390        }
391    }};
392}
393
394/// Create a [`crate::Stream`] that works in both heap and zero-heap modes.
395///
396/// # Example
397/// ```ignore
398/// let s = ove::stream!(256, 1);
399/// ```
400#[cfg(has_stream)]
401#[macro_export]
402macro_rules! stream {
403    ($N:expr, $trigger:expr) => {{
404        #[cfg(not(zero_heap))]
405        {
406            $crate::Stream::<$N>::new($trigger).unwrap()
407        }
408        #[cfg(zero_heap)]
409        {
410            static mut _S: $crate::ffi::ove_stream_storage_t = unsafe { core::mem::zeroed() };
411            static mut _B: [u8; $N] = [0u8; $N];
412            unsafe {
413                $crate::Stream::<$N>::from_static(
414                    core::ptr::addr_of_mut!(_S),
415                    core::ptr::addr_of_mut!(_B) as *mut _,
416                    $trigger,
417                )
418            }
419            .unwrap()
420        }
421    }};
422}
423
424/// Create a [`crate::Workqueue`] that works in both heap and zero-heap modes.
425///
426/// `$name` must be a `c"..."` literal — the underlying API requires a
427/// null-terminated string and the C-string literal makes the compiler
428/// enforce that.
429///
430/// # Example
431/// ```ignore
432/// let wq = ove::workqueue!(c"myq", Priority::Normal, 4096);
433/// ```
434#[cfg(has_workqueue)]
435#[macro_export]
436macro_rules! workqueue {
437    ($name:expr, $prio:expr, $stack:expr) => {{
438        #[cfg(not(zero_heap))]
439        {
440            $crate::Workqueue::new($name, $prio, $stack).unwrap()
441        }
442        #[cfg(all(zero_heap, not(rtos_zephyr)))]
443        {
444            static mut _S: $crate::ffi::ove_workqueue_storage_t = unsafe { core::mem::zeroed() };
445            // 8-byte align (ARM AAPCS) is sufficient on FreeRTOS/NuttX/POSIX.
446            #[repr(C, align(8))]
447            struct AlignedStack([u8; $stack]);
448            static mut _STACK: AlignedStack = AlignedStack([0u8; $stack]);
449            unsafe {
450                $crate::Workqueue::from_static(
451                    core::ptr::addr_of_mut!(_S),
452                    $name,
453                    $prio,
454                    $stack,
455                    core::ptr::addr_of_mut!(_STACK) as *mut _,
456                )
457            }
458            .unwrap()
459        }
460        #[cfg(all(zero_heap, rtos_zephyr))]
461        {
462            // Zephyr workqueues run on a kernel thread; the stack needs
463            // the same MPU treatment as `thread!` — power-of-2 aligned
464            // and rounded up by a 128-byte FPU guard pad.  Without
465            // this, the workqueue crashes inside `z_reset_time_slice`
466            // on the first work-handler dispatch.
467            static mut _S: $crate::ffi::ove_workqueue_storage_t = unsafe { core::mem::zeroed() };
468            const _STACK_TOTAL: usize = ($stack + 128usize).next_power_of_two();
469            #[repr(C, align(8192))]
470            struct ZStack([u8; _STACK_TOTAL]);
471            static mut _STACK: ZStack = ZStack([0u8; _STACK_TOTAL]);
472            unsafe {
473                $crate::Workqueue::from_static(
474                    core::ptr::addr_of_mut!(_S),
475                    $name,
476                    $prio,
477                    $stack,
478                    core::ptr::addr_of_mut!(_STACK) as *mut _,
479                )
480            }
481            .unwrap()
482        }
483    }};
484}
485
486/// Wrap a safe Rust `fn()` into an `ove_work_fn` C trampoline.
487///
488/// The oveRTOS C work handler has no `user_data` slot, so this just
489/// generates a `unsafe extern "C"` trampoline that calls the supplied
490/// safe function. Use this with [`crate::work!`] so app code never has to write
491/// `unsafe extern "C"` itself.
492///
493/// # Example
494/// ```ignore
495/// fn on_work() { /* ... */ }
496/// let w = ove::work!(ove::work_handler!(on_work));
497/// ```
498#[cfg(has_workqueue)]
499#[macro_export]
500macro_rules! work_handler {
501    ($f:ident) => {{
502        unsafe extern "C" fn _tramp(_w: $crate::ffi::ove_work_t) {
503            $f();
504        }
505        Some(_tramp as unsafe extern "C" fn($crate::ffi::ove_work_t))
506    }};
507}
508
509/// Create a [`crate::Work`] item that works in both heap and zero-heap modes.
510///
511/// The handler must be an `ove_work_fn` (an `unsafe extern "C" fn`). To
512/// avoid writing that yourself, wrap a safe `fn()` with [`work_handler!`].
513#[cfg(has_workqueue)]
514#[macro_export]
515macro_rules! work {
516    ($handler:expr) => {{
517        #[cfg(not(zero_heap))]
518        {
519            $crate::Work::new($handler).unwrap()
520        }
521        #[cfg(zero_heap)]
522        {
523            static mut _S: $crate::ffi::ove_work_storage_t = unsafe { core::mem::zeroed() };
524            unsafe { $crate::Work::from_static(core::ptr::addr_of_mut!(_S), $handler) }.unwrap()
525        }
526    }};
527}
528
529/// Create a [`crate::Watchdog`] that works in both heap and zero-heap modes.
530///
531/// The argument is the timeout in **milliseconds**, matching
532/// [`crate::Watchdog::new`].
533#[cfg(has_watchdog)]
534#[macro_export]
535macro_rules! watchdog {
536    ($timeout_ns:expr) => {{
537        #[cfg(not(zero_heap))]
538        {
539            $crate::Watchdog::new($timeout_ns).unwrap()
540        }
541        #[cfg(zero_heap)]
542        {
543            static mut _S: $crate::ffi::ove_watchdog_storage_t = unsafe { core::mem::zeroed() };
544            unsafe { $crate::Watchdog::from_static(core::ptr::addr_of_mut!(_S), $timeout_ns) }
545                .unwrap()
546        }
547    }};
548}
549
550/// Declare a `static` wrapped in [`crate::InitCell`] for cross-thread shared state.
551///
552/// # Example
553/// ```ignore
554/// ove::shared!(QUEUE: Queue<u32, 8>);
555/// // then use QUEUE.send(...) directly — Deref eliminates .get()
556/// ```
557#[macro_export]
558macro_rules! shared {
559    ($vis:vis $name:ident : $ty:ty) => {
560        $vis static $name: $crate::InitCell<$ty> = $crate::InitCell::new();
561    };
562}
563
564/// Declare a `static` wrapped in [`crate::InitMut`] for single-owner mutable state.
565///
566/// # Example
567/// ```ignore
568/// ove::shared_mut!(ENGINE: DspEngine);
569/// ```
570#[macro_export]
571macro_rules! shared_mut {
572    ($vis:vis $name:ident : $ty:ty) => {
573        $vis static $name: $crate::InitMut<$ty> = $crate::InitMut::new();
574    };
575}
576
577/// Declare a `static` [`crate::lvgl::EventHandler<T>`] for LVGL event callbacks.
578///
579/// Bundles a `&'static InitCell<T>` of shared state with a safe
580/// `fn(&T, EventCtx)` callback. Pass the resulting static to
581/// [`EventTarget::on_with`](crate::lvgl::EventTarget::on_with) or
582/// [`EventTarget::on_clicked_with`](crate::lvgl::EventTarget::on_clicked_with).
583///
584/// # Example
585/// ```ignore
586/// struct NavState { page: i32 }
587/// ove::shared!(NAV: ove::lvgl::LvCell<NavState>);
588/// fn on_next(state: &ove::lvgl::LvCell<NavState>, _e: ove::lvgl::EventCtx<'_>) {
589///     state.update(|s| NavState { page: s.page + 1 });
590/// }
591/// ove::event_handler!(NAV_NEXT: LvCell<NavState> = &NAV, on_next);
592/// // ...later: button.on_clicked_with(&NAV_NEXT);
593/// ```
594#[cfg(has_lvgl)]
595#[macro_export]
596macro_rules! event_handler {
597    ($vis:vis $name:ident : $t:ty = $cell:expr, $fn:expr) => {
598        $vis static $name: $crate::lvgl::EventHandler<$t> =
599            $crate::lvgl::EventHandler::new($cell, $fn);
600    };
601}
602
603/// Create a [`crate::net_tls::Session`] that works in both heap and zero-heap
604/// modes.  In zero-heap mode the session draws its mbedTLS state from a
605/// file-scope static and the per-protocol static buffer
606/// (`CONFIG_OVE_NET_TLS_HEAP_SIZE`).
607#[cfg(has_net_tls)]
608#[macro_export]
609macro_rules! tls_session {
610    () => {{
611        #[cfg(not(zero_heap))]
612        {
613            $crate::net_tls::Session::new().unwrap()
614        }
615        #[cfg(zero_heap)]
616        {
617            static mut _S: $crate::ffi::ove_tls_storage_t = unsafe { core::mem::zeroed() };
618            unsafe { $crate::net_tls::Session::from_static(core::ptr::addr_of_mut!(_S)) }.unwrap()
619        }
620    }};
621}
622
623/// Create a [`crate::net_http::Client`] that works in both heap and zero-heap
624/// modes.  In zero-heap mode the response body and headers borrow into the
625/// client's embedded `_resp_buf[CONFIG_OVE_NET_HTTP_MAX_RESPONSE]` and are
626/// valid until the next request.
627#[cfg(has_net_http)]
628#[macro_export]
629macro_rules! http_client {
630    () => {{
631        #[cfg(not(zero_heap))]
632        {
633            $crate::net_http::Client::new().unwrap()
634        }
635        #[cfg(zero_heap)]
636        {
637            static mut _S: $crate::ffi::ove_http_client_storage_t = unsafe { core::mem::zeroed() };
638            unsafe { $crate::net_http::Client::from_static(core::ptr::addr_of_mut!(_S)) }.unwrap()
639        }
640    }};
641}
642
643/// Create a [`crate::net_mqtt::Client`] that works in both heap and zero-heap
644/// modes.  In zero-heap mode the per-connection RX/TX buffers
645/// (`CONFIG_OVE_NET_MQTT_RX_BUF` / `CONFIG_OVE_NET_MQTT_TX_BUF`) live in the
646/// client storage.
647#[cfg(has_net_mqtt)]
648#[macro_export]
649macro_rules! mqtt_client {
650    () => {{
651        #[cfg(not(zero_heap))]
652        {
653            $crate::net_mqtt::Client::new().unwrap()
654        }
655        #[cfg(zero_heap)]
656        {
657            static mut _S: $crate::ffi::ove_mqtt_client_storage_t = unsafe { core::mem::zeroed() };
658            unsafe { $crate::net_mqtt::Client::from_static(core::ptr::addr_of_mut!(_S)) }.unwrap()
659        }
660    }};
661}