Skip to main content

rusteron_code_gen/
common.rs

1// ─── Compilation contexts (read before adding code here) ────────────────────────────
2// This file compiles in TWO contexts:
3//   1. as `mod common` inside rusteron-code-gen itself (unit tests, shared types), and
4//   2. verbatim inside every generated crate's `aeron.rs` (via COMMON_CODE include_str).
5// Consequence: nothing here may depend on impls that live in `aeron_custom*.rs` — those
6// exist only in context 2 (e.g. AeronCError's Debug/Display). That is why AeronOfferError
7// hand-rolls its Debug. Crate-specific code belongs in `aeron_custom.rs` (all crates) or
8// `aeron_custom_<crate>.rs` (one crate); build-script-only code goes in `build_common.rs`.
9// ─────────────────────────────────────────────────────────────────────────────────────
10
11use crate::AeronErrorType::Unknown;
12#[cfg(feature = "backtrace")]
13use std::backtrace::Backtrace;
14use std::cell::UnsafeCell;
15use std::fmt::Formatter;
16use std::mem::MaybeUninit;
17use std::ops::Deref;
18#[allow(unused_imports)]
19use std::ops::DerefMut;
20
21/// Reference-counting smart pointer: `Rc` by default, `Arc` under the
22/// `multi-threaded` feature. Swap is transparent — `RcOrArc::new`, `.clone()`,
23/// `strong_count` all work on both.
24#[cfg(not(feature = "multi-threaded"))]
25pub type RcOrArc<T> = std::rc::Rc<T>;
26#[cfg(feature = "multi-threaded")]
27pub type RcOrArc<T> = std::sync::Arc<T>;
28
29#[cfg(not(feature = "multi-threaded"))]
30pub type RefCellOrMutex<T> = std::cell::RefCell<T>;
31#[cfg(feature = "multi-threaded")]
32pub type RefCellOrMutex<T> = std::sync::Mutex<T>;
33
34#[cfg(not(feature = "multi-threaded"))]
35pub type CleanupBox<T> = Box<dyn FnMut(*mut *mut T) -> i32>;
36#[cfg(feature = "multi-threaded")]
37pub type CleanupBox<T> = Box<dyn FnMut(*mut *mut T) -> i32 + Send>;
38
39pub enum CResource<T> {
40    OwnedOnHeap(RcOrArc<ManagedCResource<T>>),
41    /// Always initialised by construction (zeroed or `new(v)`). Never store
42    /// `uninit()` — `Clone` and `get()` assume it's valid.
43    OwnedOnStack(std::mem::MaybeUninit<T>),
44    Borrowed(*mut T),
45}
46
47// `CResource<T>` deliberately does NOT implement `Send`/`Sync` here — not even
48// bounded on `T: Send`/`Sync`
49
50impl<T: Clone> Clone for CResource<T> {
51    fn clone(&self) -> Self {
52        // SAFETY: each branch only dereferences pointers/references that are
53        // valid by construction. `OwnedOnStack` upholds the initialised-by-
54        // construction invariant documented on the variant, so `assume_init_ref`
55        // is sound.
56        unsafe {
57            match self {
58                CResource::OwnedOnHeap(r) => CResource::OwnedOnHeap(r.clone()),
59                CResource::OwnedOnStack(r) => CResource::OwnedOnStack(MaybeUninit::new(r.assume_init_ref().clone())),
60                CResource::Borrowed(r) => CResource::Borrowed(r.clone()),
61            }
62        }
63    }
64}
65
66impl<T> CResource<T> {
67    #[inline]
68    pub fn get(&self) -> *mut T {
69        match self {
70            CResource::OwnedOnHeap(r) => r.get(),
71            CResource::OwnedOnStack(r) => r.as_ptr() as *mut T,
72            CResource::Borrowed(r) => *r,
73        }
74    }
75
76    #[inline]
77    // to prevent the dependencies from being dropped as you have a copy here
78    pub fn add_dependency<D: std::any::Any>(&self, dep: D) {
79        match self {
80            CResource::OwnedOnHeap(r) => r.add_dependency(dep),
81            CResource::OwnedOnStack(_) | CResource::Borrowed(_) => {
82                unreachable!("only owned on heap")
83            }
84        }
85    }
86
87    /// Test-only: see [`ManagedCResource::dependency_len`].
88    #[cfg(test)]
89    #[allow(dead_code)]
90    pub(crate) fn dependency_len(&self) -> usize {
91        match self {
92            CResource::OwnedOnHeap(r) => r.dependency_len(),
93            CResource::OwnedOnStack(_) | CResource::Borrowed(_) => 0,
94        }
95    }
96
97    #[inline]
98    pub fn get_dependency<V: Clone + 'static>(&self) -> Option<V> {
99        match self {
100            CResource::OwnedOnHeap(r) => r.get_dependency(),
101            CResource::OwnedOnStack(_) | CResource::Borrowed(_) => None,
102        }
103    }
104
105    #[inline]
106    pub fn as_owned(&self) -> Option<&RcOrArc<ManagedCResource<T>>> {
107        match self {
108            CResource::OwnedOnHeap(r) => Some(r),
109            CResource::OwnedOnStack(_) | CResource::Borrowed(_) => None,
110        }
111    }
112
113    /// Run the clean-up / close on the resource via its shared state.
114    ///
115    /// For `OwnedOnHeap` resources this calls `close_shared` on the
116    /// `ManagedCResource` — the FFI close fires exactly once across all
117    /// clones.  Stack and borrowed resources are no-ops (they don't own a
118    /// cleanup closure).
119    #[allow(dead_code)]
120    #[inline]
121    pub(crate) fn close_resource(&self) -> Result<(), AeronCError> {
122        match self {
123            CResource::OwnedOnHeap(r) => r.close_shared(),
124            CResource::OwnedOnStack(_) | CResource::Borrowed(_) => Ok(()),
125        }
126    }
127
128    /// Run a custom close function through the same shared close gate.
129    ///
130    /// This is used for close methods that take extra parameters, such as
131    /// Aeron's close-complete notification callback.  The custom close still
132    /// consumes the wrapper handle and still closes exactly once across clones.
133    #[allow(dead_code)]
134    #[inline]
135    pub(crate) fn close_resource_with(&self, cleanup: impl FnMut(*mut *mut T) -> i32) -> Result<(), AeronCError> {
136        match self {
137            CResource::OwnedOnHeap(r) => r.close_shared_with(cleanup),
138            CResource::OwnedOnStack(_) | CResource::Borrowed(_) => Ok(()),
139        }
140    }
141
142    /// Close an owner resource only when this is the last shared reference.
143    ///
144    /// This is for owner/client handles (e.g. Aeron/AeronArchive) whose C close
145    /// frees child resources.  If child handles still hold dependency clones, we
146    /// must defer to natural Rc teardown to preserve child-before-parent order.
147    #[allow(dead_code)]
148    #[inline]
149    pub(crate) fn close_resource_deferred_if_shared(&self) -> Result<(), AeronCError> {
150        match self {
151            CResource::OwnedOnHeap(r) => {
152                let refs = RcOrArc::strong_count(r);
153                if refs > 1 {
154                    log::info!(
155                        "close deferred for {} because {} references are still alive",
156                        std::any::type_name::<T>(),
157                        refs
158                    );
159                    Ok(())
160                } else {
161                    r.close_shared()
162                }
163            }
164            CResource::OwnedOnStack(_) | CResource::Borrowed(_) => Ok(()),
165        }
166    }
167}
168
169impl<T> std::fmt::Debug for CResource<T> {
170    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
171        let name = std::any::type_name::<T>();
172
173        match self {
174            CResource::OwnedOnHeap(r) => {
175                write!(f, "{name} heap({:?})", r)
176            }
177            CResource::OwnedOnStack(r) => {
178                write!(f, "{name} stack({:?})", *r)
179            }
180            CResource::Borrowed(r) => {
181                write!(f, "{name} borrowed ({:?})", r)
182            }
183        }
184    }
185}
186
187/// A custom struct for managing C resources with automatic cleanup.
188///
189/// It handles initialisation and clean-up of the resource and ensures that resources
190/// are properly released when they go out of scope. All teardown goes through the
191/// single `cleanup` closure (if set), which is the FFI close function (e.g.
192/// `aeron_close`). The Rc dependency graph ensures parents outlive children
193/// structurally — you cannot race `aeron_close` ahead of a live child handle.
194#[allow(dead_code)]
195#[allow(dead_code)]
196pub struct ManagedCResource<T> {
197    #[cfg(not(feature = "multi-threaded"))]
198    resource: std::cell::Cell<*mut T>,
199    #[cfg(feature = "multi-threaded")]
200    resource: std::sync::atomic::AtomicPtr<T>,
201
202    #[cfg(not(feature = "multi-threaded"))]
203    cleanup: UnsafeCell<Option<CleanupBox<T>>>,
204    #[cfg(feature = "multi-threaded")]
205    cleanup: std::sync::Mutex<Option<CleanupBox<T>>>,
206
207    cleanup_struct: bool,
208
209    manual_close_required: bool,
210
211    #[cfg(not(feature = "multi-threaded"))]
212    close_already_called: std::cell::Cell<bool>,
213    #[cfg(feature = "multi-threaded")]
214    close_already_called: std::sync::atomic::AtomicBool,
215
216    #[cfg(not(feature = "multi-threaded"))]
217    resource_released: std::cell::Cell<bool>,
218    #[cfg(feature = "multi-threaded")]
219    resource_released: std::sync::atomic::AtomicBool,
220
221    #[cfg(not(feature = "multi-threaded"))]
222    dependencies: UnsafeCell<Vec<RcOrArc<dyn std::any::Any>>>,
223    #[cfg(feature = "multi-threaded")]
224    dependencies: std::sync::Mutex<Vec<RcOrArc<dyn std::any::Any>>>,
225}
226
227impl<T> std::fmt::Debug for ManagedCResource<T> {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        let mut debug = f.debug_struct("ManagedCResource");
230        if self.get_close_already_called() {
231            debug.field("resource", &"<closed>");
232        } else {
233            debug.field("resource", &self.get());
234        }
235        debug.field("type", &std::any::type_name::<T>()).finish()
236    }
237}
238
239impl<T> ManagedCResource<T> {
240    /// Creates a new ManagedCResource with a given initializer and cleanup function.
241    ///
242    /// The initializer is a closure that attempts to initialize the resource.
243    /// If initialization fails, the initializer should return an error code.
244    /// The cleanup function is used to release the resource when it's no longer needed.
245    /// `cleanup_struct` where it should clean up the struct in rust
246    pub fn new(
247        init: impl FnOnce(*mut *mut T) -> i32,
248        cleanup: Option<CleanupBox<T>>,
249        cleanup_struct: bool,
250    ) -> Result<Self, AeronCError> {
251        let resource = Self::initialise(init)?;
252        // Compute before `cleanup` is moved into the struct literal below.
253        // `is_none()` borrows `cleanup` immutably; the move happens later.
254        let manual_close_required = cleanup.is_none() && !cleanup_struct;
255
256        let result = Self {
257            #[cfg(not(feature = "multi-threaded"))]
258            resource: std::cell::Cell::new(resource),
259            #[cfg(feature = "multi-threaded")]
260            resource: std::sync::atomic::AtomicPtr::new(resource),
261
262            #[cfg(not(feature = "multi-threaded"))]
263            cleanup: UnsafeCell::new(cleanup),
264            #[cfg(feature = "multi-threaded")]
265            cleanup: std::sync::Mutex::new(cleanup),
266
267            cleanup_struct,
268            manual_close_required,
269
270            #[cfg(not(feature = "multi-threaded"))]
271            close_already_called: std::cell::Cell::new(false),
272            #[cfg(feature = "multi-threaded")]
273            close_already_called: std::sync::atomic::AtomicBool::new(false),
274
275            #[cfg(not(feature = "multi-threaded"))]
276            resource_released: std::cell::Cell::new(false),
277            #[cfg(feature = "multi-threaded")]
278            resource_released: std::sync::atomic::AtomicBool::new(false),
279
280            #[cfg(not(feature = "multi-threaded"))]
281            dependencies: UnsafeCell::new(vec![]),
282            #[cfg(feature = "multi-threaded")]
283            dependencies: std::sync::Mutex::new(vec![]),
284        };
285        #[cfg(feature = "extra-logging")]
286        log::info!("created c resource: {:?}", result);
287        Ok(result)
288    }
289
290    pub fn initialise(init: impl FnOnce(*mut *mut T) -> i32 + Sized) -> Result<*mut T, AeronCError> {
291        let mut resource: *mut T = std::ptr::null_mut();
292        let result = init(&mut resource);
293        if result < 0 || resource.is_null() {
294            return Err(AeronCError::from_code(result));
295        }
296        Ok(resource)
297    }
298
299    /// Gets a raw pointer to the resource.
300    #[inline(always)]
301    pub fn get(&self) -> *mut T {
302        #[cfg(not(feature = "multi-threaded"))]
303        {
304            self.resource.get()
305        }
306        #[cfg(feature = "multi-threaded")]
307        {
308            self.resource.load(std::sync::atomic::Ordering::Acquire)
309        }
310    }
311
312    #[inline(always)]
313    fn set_resource(&self, val: *mut T) {
314        #[cfg(not(feature = "multi-threaded"))]
315        {
316            self.resource.set(val);
317        }
318        #[cfg(feature = "multi-threaded")]
319        {
320            self.resource.store(val, std::sync::atomic::Ordering::Release);
321        }
322    }
323
324    #[inline(always)]
325    fn get_close_already_called(&self) -> bool {
326        #[cfg(not(feature = "multi-threaded"))]
327        {
328            self.close_already_called.get()
329        }
330        #[cfg(feature = "multi-threaded")]
331        {
332            self.close_already_called.load(std::sync::atomic::Ordering::Acquire)
333        }
334    }
335
336    #[inline(always)]
337    fn set_close_already_called(&self, val: bool) {
338        #[cfg(not(feature = "multi-threaded"))]
339        {
340            self.close_already_called.set(val);
341        }
342        #[cfg(feature = "multi-threaded")]
343        {
344            self.close_already_called
345                .store(val, std::sync::atomic::Ordering::Release);
346        }
347    }
348
349    #[inline(always)]
350    fn get_resource_released(&self) -> bool {
351        #[cfg(not(feature = "multi-threaded"))]
352        {
353            self.resource_released.get()
354        }
355        #[cfg(feature = "multi-threaded")]
356        {
357            self.resource_released.load(std::sync::atomic::Ordering::Acquire)
358        }
359    }
360
361    #[inline(always)]
362    fn set_resource_released(&self, val: bool) {
363        #[cfg(not(feature = "multi-threaded"))]
364        {
365            self.resource_released.set(val);
366        }
367        #[cfg(feature = "multi-threaded")]
368        {
369            self.resource_released.store(val, std::sync::atomic::Ordering::Release);
370        }
371    }
372
373    /// Mutable access to the underlying C struct, minted from `&self`.
374    ///
375    /// # Safety
376    /// No other reference (`&` or `&mut`) to the underlying struct may be
377    /// alive while the returned `&mut` is in use.
378    #[inline(always)]
379    pub unsafe fn get_mut(&self) -> &mut T {
380        unsafe { &mut *self.get() }
381    }
382
383    #[inline]
384    // to prevent the dependencies from being dropped as you have a copy here
385    pub fn add_dependency<D: std::any::Any>(&self, dep: D) {
386        if let Some(dep) = (&dep as &dyn std::any::Any).downcast_ref::<RcOrArc<dyn std::any::Any>>() {
387            #[cfg(not(feature = "multi-threaded"))]
388            unsafe {
389                (*self.dependencies.get()).push(dep.clone());
390            }
391            #[cfg(feature = "multi-threaded")]
392            {
393                self.dependencies.lock().unwrap().push(dep.clone());
394            }
395        } else {
396            #[cfg(not(feature = "multi-threaded"))]
397            unsafe {
398                (*self.dependencies.get()).push(RcOrArc::new(dep));
399            }
400            #[cfg(feature = "multi-threaded")]
401            {
402                self.dependencies.lock().unwrap().push(RcOrArc::new(dep));
403            }
404        }
405    }
406
407    #[inline]
408    pub fn get_dependency<V: Clone + 'static>(&self) -> Option<V> {
409        #[cfg(not(feature = "multi-threaded"))]
410        unsafe {
411            (*self.dependencies.get())
412                .iter()
413                .filter_map(|x| x.as_ref().downcast_ref::<V>().cloned())
414                .next()
415        }
416        #[cfg(feature = "multi-threaded")]
417        {
418            self.dependencies
419                .lock()
420                .unwrap()
421                .iter()
422                .filter_map(|x| x.as_ref().downcast_ref::<V>().cloned())
423                .next()
424        }
425    }
426
427    #[inline]
428    pub fn is_resource_released(&self) -> bool {
429        self.get_resource_released()
430    }
431
432    /// Test-only: number of dependencies currently anchored on this resource (e.g. via
433    /// [`Self::add_dependency`]). Used to document/measure dependency-list growth — see
434    /// `handler_dependencies_on_client_grow_with_subscriptions_and_reset_on_client_drop`.
435    #[cfg(test)]
436    #[allow(dead_code)]
437    pub(crate) fn dependency_len(&self) -> usize {
438        #[cfg(not(feature = "multi-threaded"))]
439        unsafe {
440            (*self.dependencies.get()).len()
441        }
442        #[cfg(feature = "multi-threaded")]
443        {
444            self.dependencies.lock().unwrap().len()
445        }
446    }
447
448    #[inline]
449    pub fn mark_resource_released(&self) {
450        self.set_resource_released(true);
451        // The C client frees async resources when their poll completes (created,
452        // errored, or cancelled). Null the stale pointer so any later use faults
453        // deterministically on null instead of reading freed memory.
454        self.set_resource(std::ptr::null_mut());
455    }
456
457    /// Closes the resource through a shared reference.
458    ///
459    /// Like `close(&mut self)` but works with `&self`, enabling explicit close
460    /// on handles that share the resource via `Rc`.  The cleanup closure is
461    /// accessed through `UnsafeCell` interior mutability; the
462    /// `close_already_called` gate ensures it is only taken once regardless of
463    /// how many clones call `close_shared()`.
464    ///
465    /// This is the method called by the generated `close(self)` method on
466    /// wrapper types.
467    pub(crate) fn close_shared(&self) -> Result<(), AeronCError> {
468        if self.get_close_already_called() {
469            return Ok(());
470        }
471
472        // SAFETY: this library deliberately uses Rc/Cell/UnsafeCell for
473        // single-threaded low-latency handles. close_shared() is not Sync; the
474        // first caller takes the cleanup closure and either completes close or
475        // restores the closure on failure so a later call/drop can retry.
476        #[cfg(not(feature = "multi-threaded"))]
477        let cleanup = unsafe { (*self.cleanup.get()).take() };
478        #[cfg(feature = "multi-threaded")]
479        let cleanup = self.cleanup.lock().unwrap().take();
480
481        if let Some(mut cleanup) = cleanup {
482            let mut resource = self.get();
483            if !resource.is_null() {
484                let result = cleanup(&mut resource);
485                if result < 0 {
486                    #[cfg(not(feature = "multi-threaded"))]
487                    unsafe {
488                        *self.cleanup.get() = Some(cleanup);
489                    }
490                    #[cfg(feature = "multi-threaded")]
491                    {
492                        *self.cleanup.lock().unwrap() = Some(cleanup);
493                    }
494                    return Err(AeronCError::from_code(result));
495                }
496            }
497
498            self.set_close_already_called(true);
499            if !self.cleanup_struct {
500                // C-owned resources have been freed by the close function.
501                // Null the shared pointer so clones cannot keep using a
502                // dangling pointer after explicit close.
503                self.set_resource(std::ptr::null_mut());
504            }
505        } else {
506            self.set_close_already_called(true);
507        }
508
509        Ok(())
510    }
511
512    /// Closes the resource with a caller-supplied C close function.
513    ///
514    /// The stored default cleanup is taken first so Drop cannot later run it a
515    /// second time.  If the custom close fails, the default cleanup is restored
516    /// and the resource remains retryable.
517    #[allow(dead_code)]
518    pub(crate) fn close_shared_with(
519        &self,
520        mut custom_cleanup: impl FnMut(*mut *mut T) -> i32,
521    ) -> Result<(), AeronCError> {
522        if self.get_close_already_called() {
523            return Ok(());
524        }
525
526        #[cfg(not(feature = "multi-threaded"))]
527        let stored_cleanup = unsafe { (*self.cleanup.get()).take() };
528        #[cfg(feature = "multi-threaded")]
529        let stored_cleanup = self.cleanup.lock().unwrap().take();
530
531        let mut resource = self.get();
532        if !resource.is_null() {
533            let result = custom_cleanup(&mut resource);
534            if result < 0 {
535                #[cfg(not(feature = "multi-threaded"))]
536                unsafe {
537                    *self.cleanup.get() = stored_cleanup;
538                }
539                #[cfg(feature = "multi-threaded")]
540                {
541                    *self.cleanup.lock().unwrap() = stored_cleanup;
542                }
543                return Err(AeronCError::from_code(result));
544            }
545        }
546
547        self.set_close_already_called(true);
548        if !self.cleanup_struct {
549            self.set_resource(std::ptr::null_mut());
550        }
551
552        Ok(())
553    }
554}
555
556impl<T> Drop for ManagedCResource<T> {
557    fn drop(&mut self) {
558        // Capture whether close ran BEFORE Drop — close_shared() below would set
559        // the flag even when the cleanup closure is None, hiding the leak signal.
560        let close_ran_before_drop = self.get_close_already_called();
561        // Delegate to close_shared() which handles single-execution, error
562        // logging, and pointer-nulling.  close_already_called prevents
563        // double-execution if the resource was already closed through
564        // another clone.
565        if !close_ran_before_drop {
566            if let Err(e) = self.close_shared() {
567                log::warn!(
568                    "cleanup failed for {} during Drop with code {}",
569                    std::any::type_name::<T>(),
570                    e.code,
571                );
572            }
573        }
574
575        if self.manual_close_required && !close_ran_before_drop {
576            #[cfg(not(feature = "multi-threaded"))]
577            let has_dependency = !unsafe { (*self.dependencies.get()).is_empty() };
578            #[cfg(feature = "multi-threaded")]
579            let has_dependency = !self.dependencies.lock().unwrap().is_empty();
580            if !has_dependency {
581                let resource = self.get();
582                if !resource.is_null() {
583                    #[cfg(feature = "strict-lifecycle")]
584                    panic!(
585                        "ManagedCResource<{}> dropped without explicit close and no cleanup closure \
586                         — resource leaked. Call close()/close_now() before drop, or supply a \
587                         cleanup closure at construction.",
588                        std::any::type_name::<T>()
589                    );
590                    #[cfg(not(feature = "strict-lifecycle"))]
591                    log::warn!(
592                        "ManagedCResource<{}> dropped without explicit close and no cleanup closure \
593                         — resource likely leaked. Call close()/close_now() before drop, or supply a \
594                         cleanup closure at construction.",
595                        std::any::type_name::<T>()
596                    );
597                }
598            }
599        }
600
601        if self.cleanup_struct {
602            let resource = self.get();
603            if !resource.is_null() {
604                #[cfg(feature = "extra-logging")]
605                log::info!("closing rust struct resource: {:?}", resource);
606                unsafe {
607                    let _ = Box::from_raw(resource);
608                }
609                self.set_resource(std::ptr::null_mut());
610            }
611        }
612    }
613}
614
615#[derive(Debug, PartialOrd, Eq, PartialEq, Clone)]
616pub enum AeronErrorType {
617    GenericError,
618    ClientErrorDriverTimeout,
619    ClientErrorClientTimeout,
620    ClientErrorConductorServiceTimeout,
621    ClientErrorBufferFull,
622    PublicationBackPressured,
623    PublicationAdminAction,
624    PublicationClosed,
625    PublicationMaxPositionExceeded,
626    PublicationError,
627    TimedOut,
628    Unknown(i32),
629}
630
631impl From<AeronErrorType> for AeronCError {
632    fn from(value: AeronErrorType) -> Self {
633        AeronCError::from_code(value.code())
634    }
635}
636
637impl AeronErrorType {
638    pub fn code(&self) -> i32 {
639        match self {
640            AeronErrorType::GenericError => -1,
641            AeronErrorType::ClientErrorDriverTimeout => -1000,
642            AeronErrorType::ClientErrorClientTimeout => -1001,
643            AeronErrorType::ClientErrorConductorServiceTimeout => -1002,
644            AeronErrorType::ClientErrorBufferFull => -1003,
645            AeronErrorType::PublicationBackPressured => -2,
646            AeronErrorType::PublicationAdminAction => -3,
647            AeronErrorType::PublicationClosed => -4,
648            AeronErrorType::PublicationMaxPositionExceeded => -5,
649            AeronErrorType::PublicationError => -6,
650            AeronErrorType::TimedOut => -234324,
651            AeronErrorType::Unknown(code) => *code,
652        }
653    }
654
655    pub fn is_back_pressured(&self) -> bool {
656        self == &AeronErrorType::PublicationBackPressured
657    }
658
659    pub fn is_admin_action(&self) -> bool {
660        self == &AeronErrorType::PublicationAdminAction
661    }
662
663    pub fn is_back_pressured_or_admin_action(&self) -> bool {
664        self.is_back_pressured() || self.is_admin_action()
665    }
666
667    pub fn from_code(code: i32) -> Self {
668        match code {
669            -1 => AeronErrorType::GenericError,
670            -1000 => AeronErrorType::ClientErrorDriverTimeout,
671            -1001 => AeronErrorType::ClientErrorClientTimeout,
672            -1002 => AeronErrorType::ClientErrorConductorServiceTimeout,
673            -1003 => AeronErrorType::ClientErrorBufferFull,
674            -2 => AeronErrorType::PublicationBackPressured,
675            -3 => AeronErrorType::PublicationAdminAction,
676            -4 => AeronErrorType::PublicationClosed,
677            -5 => AeronErrorType::PublicationMaxPositionExceeded,
678            -6 => AeronErrorType::PublicationError,
679            -234324 => AeronErrorType::TimedOut,
680            _ => Unknown(code),
681        }
682    }
683
684    pub fn to_string(&self) -> &'static str {
685        match self {
686            AeronErrorType::GenericError => "Generic Error",
687            AeronErrorType::ClientErrorDriverTimeout => "Client Error Driver Timeout",
688            AeronErrorType::ClientErrorClientTimeout => "Client Error Client Timeout",
689            AeronErrorType::ClientErrorConductorServiceTimeout => "Client Error Conductor Service Timeout",
690            AeronErrorType::ClientErrorBufferFull => "Client Error Buffer Full",
691            AeronErrorType::PublicationBackPressured => "Publication Back Pressured",
692            AeronErrorType::PublicationAdminAction => "Publication Admin Action",
693            AeronErrorType::PublicationClosed => "Publication Closed",
694            AeronErrorType::PublicationMaxPositionExceeded => "Publication Max Position Exceeded",
695            AeronErrorType::PublicationError => "Publication Error",
696            AeronErrorType::TimedOut => "Timed Out",
697            AeronErrorType::Unknown(_) => "Unknown Error",
698        }
699    }
700}
701
702/// Aeron C API error: code + optional message.
703///
704/// Construction is allocation-free (never reads `aeron_errmsg()`), so retry loops
705/// that discard the error stay cheap. Attach the message via `capture_errmsg()`
706/// (at the error site) or `with_message()`; `Display` / `get_last_err_message()`
707/// otherwise read the live `aeron_errmsg()` buffer.
708#[derive(Clone)]
709pub struct AeronCError {
710    pub code: i32,
711    /// Attached via `capture_errmsg()` / `with_message()`; `None` otherwise.
712    msg: Option<String>,
713}
714
715/// Equality is on `code` only — the attached message is advisory.
716impl PartialEq for AeronCError {
717    fn eq(&self, other: &Self) -> bool {
718        self.code == other.code
719    }
720}
721impl Eq for AeronCError {}
722
723impl AeronCError {
724    /// Construct from an Aeron error code (`< 0` is failure).
725    ///
726    /// Allocation-free; does not read `aeron_errmsg()`. Use `capture_errmsg()` to
727    /// attach the message when the error will be stored or logged later.
728    pub fn from_code(code: i32) -> Self {
729        #[cfg(feature = "backtrace")]
730        {
731            if code < 0 {
732                let backtrace = Backtrace::capture();
733                let backtrace = format!("{:?}", backtrace);
734
735                // Compile the backtrace-parsing regex ONCE, not per error.
736                // `from_code` sits on the error path; re-compiling the regex on
737                // every Aeron error (including back-pressure retries) is wasteful.
738                static BACKTRACE_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
739                let re = BACKTRACE_RE
740                    .get_or_init(|| regex::Regex::new(r#"fn: "([^"]+)", file: "([^"]+)", line: (\d+)"#).unwrap());
741                let mut lines = String::new();
742                re.captures_iter(&backtrace).for_each(|cap| {
743                    let function = &cap[1];
744                    let mut file = cap[2].to_string();
745                    let line = &cap[3];
746                    if file.starts_with("./") {
747                        file = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), &file[2..]);
748                    } else if file.starts_with("/rustc/") {
749                        file = file.split("/").last().unwrap().to_string();
750                    }
751                    // log in intellij friendly error format so can hyperlink to source code in stack trace
752                    lines.push_str(&format!(" {file}:{line} in {function}\n"));
753                });
754
755                log::error!(
756                    "Aeron C error code: {}, kind: '{:?}'\n{}",
757                    code,
758                    AeronErrorType::from_code(code),
759                    lines
760                );
761            }
762        }
763        AeronCError { code, msg: None }
764    }
765
766    /// [`Self::from_code`] with an attached message.
767    pub fn with_message(code: i32, msg: impl Into<String>) -> Self {
768        let mut err = Self::from_code(code);
769        err.msg = Some(msg.into());
770        err
771    }
772
773    /// Message attached via `capture_errmsg()` / `with_message()`, if any.
774    pub fn message(&self) -> Option<&str> {
775        self.msg.as_deref()
776    }
777
778    pub fn kind(&self) -> AeronErrorType {
779        AeronErrorType::from_code(self.code)
780    }
781
782    pub fn is_back_pressured(&self) -> bool {
783        self.kind().is_back_pressured()
784    }
785
786    pub fn is_admin_action(&self) -> bool {
787        self.kind().is_admin_action()
788    }
789
790    pub fn is_back_pressured_or_admin_action(&self) -> bool {
791        self.kind().is_back_pressured_or_admin_action()
792    }
793}
794
795/// Typed error for `offer` / `try_claim` on a publication.
796///
797/// Aeron returns a negative *sentinel* instead of a stream position. These are not
798/// errno-style codes — `-1` here means "not connected", not a generic error — hence
799/// this dedicated type. Use [`Self::is_retryable`] to drive retry loops.
800#[derive(Clone, PartialEq, Eq)]
801pub enum AeronOfferError {
802    /// No subscriber is connected (`-1`). Usually transient: a subscriber may
803    /// connect later. Retryable.
804    NotConnected,
805    /// Flow control or a full term buffer is applying back pressure (`-2`).
806    /// Retry after idling. Retryable.
807    BackPressured,
808    /// An administrative action (e.g. term rotation) is in progress (`-3`).
809    /// Retry immediately. Retryable.
810    AdminAction,
811    /// The publication is closed (`-4`). Fatal for this handle.
812    Closed,
813    /// The maximum stream position was reached (`-5`). Fatal: a new publication
814    /// (new session) is required.
815    MaxPositionExceeded,
816    /// More than [`MAX_OFFER_PARTS`] buffers passed to `offer_parts` — a caller
817    /// bug, not an Aeron wire error. Fatal (fix the call site).
818    TooManyParts,
819    /// Any other negative value (`-6` / unexpected). Fatal; inspect the inner
820    /// [`AeronCError`] and `Aeron::errmsg()` for detail.
821    Error(AeronCError),
822}
823impl AeronOfferError {
824    /// Maps a raw offer/try_claim return to `Ok(position)` or a typed error.
825    #[inline]
826    pub fn from_position(position: i64) -> Result<i64, Self> {
827        if position >= 0 {
828            return Ok(position);
829        }
830        Err(match position {
831            -1 => AeronOfferError::NotConnected,
832            -2 => AeronOfferError::BackPressured,
833            -3 => AeronOfferError::AdminAction,
834            -4 => AeronOfferError::Closed,
835            -5 => AeronOfferError::MaxPositionExceeded,
836            _ => AeronOfferError::Error(AeronCError::from_code(position as i32)),
837        })
838    }
839
840    /// A retry (possibly after idling / waiting for a subscriber) can succeed.
841    #[inline]
842    pub fn is_retryable(&self) -> bool {
843        matches!(
844            self,
845            AeronOfferError::NotConnected | AeronOfferError::BackPressured | AeronOfferError::AdminAction
846        )
847    }
848
849    /// The publication will never accept this offer again; recreate or give up.
850    #[inline]
851    pub fn is_fatal(&self) -> bool {
852        !self.is_retryable()
853    }
854}
855
856impl std::fmt::Display for AeronOfferError {
857    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
858        match self {
859            AeronOfferError::NotConnected => write!(f, "publication not connected"),
860            AeronOfferError::BackPressured => write!(f, "publication back pressured"),
861            AeronOfferError::AdminAction => write!(f, "publication admin action in progress"),
862            AeronOfferError::Closed => write!(f, "publication closed"),
863            AeronOfferError::MaxPositionExceeded => write!(f, "publication max position exceeded"),
864            AeronOfferError::TooManyParts => write!(f, "too many parts in offer_parts (max 8)"),
865            AeronOfferError::Error(e) => write!(f, "publication error (code {})", e.code),
866        }
867    }
868}
869
870impl std::fmt::Debug for AeronOfferError {
871    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
872        std::fmt::Display::fmt(self, f)
873    }
874}
875
876impl std::error::Error for AeronOfferError {}
877
878/// # Handler
879/// **Heap-allocated, reference-counted** callback holder for callbacks the C
880/// client **retains** (fires later, possibly many times, from the conductor thread).
881///
882/// `Handler<T>` wraps `Arc<UnsafeCell<T>>`. The callback value lives on the heap
883/// and is freed only when the last clone drops. The raw `clientd` pointer handed
884/// to C is `&T` (via [`Handler::as_raw`]); C keeps firing it for as long as it
885/// holds the callback, so the `Handler` must outlive that — methods that register
886/// a retained callback ([`AeronContext::set_error_handler`], the image lifecycle
887/// handlers on `async_add_subscription`, `set_on_available_image`, …) store a
888/// clone of the `Handler` inside the registering resource (as a dependency), so
889/// the value is guaranteed to outlive the C side's use of it. No manual
890/// `release()` is needed.
891///
892/// ## Async close: why the handler must outlive the resource, not just the call
893///
894/// The C close for a resource holding one of these handlers (e.g.
895/// `aeron_subscription_close`) is **asynchronous** — it only requests the close;
896/// the conductor thread may still fire the callback (e.g. `on_available_image`)
897/// after `close()`/`drop` has already returned on the calling thread. Freeing the
898/// handler's value as soon as the Rust-side handle is dropped would therefore
899/// risk a use-after-free from that still-in-flight callback.
900///
901/// 0.2.x solves this by cloning the `Handler` into the *client's* dependency
902/// list (not just the subscription's) when the callback is registered — see the
903/// docs on `async_add_subscription` and friends. That keeps the value alive for
904/// the client's entire lifetime, independent of when any individual subscription
905/// or resource closes, so there is no window where the conductor thread can call
906/// into a freed handler. The trade-off is that handler clones accumulate on the
907/// client's dependency list for as long as the client lives (each is just one
908/// small `Arc` clone per registration, dropped in bulk when the client itself
909/// drops).
910///
911/// This differs from the Aeron C++ wrapper, which instead stores the handler
912/// inside the `AsyncAddSubscription` object and deletes it as the *final* step
913/// of `on_cmd_close_subscription`, i.e. it ties the handler's lifetime to the
914/// close actually completing on the conductor thread, rather than to the client.
915/// That avoids the unbounded accumulation this crate accepts, at the cost of a
916/// conductor-side hook. If you are migrating C++ code that assumed
917/// close-then-immediately-free semantics, be aware 0.2.x's handlers instead live
918/// until the client drops.
919///
920/// # Heap vs stack — when to reach for `Handler` vs a `*_fn` / `*_once` method
921///
922/// | Callback kind | Where the closure lives | API |
923/// |---|---|---|
924/// | **Retained** (C stores it; fires later / repeatedly) | **heap** (`Handler`/`Arc`) | `set_error_handler(Some(Handler::new(...)))`, `async_add_subscription(.., Some(&h), ..)`, `poll(Some(&h), limit)` |
925/// | **Sync / call-only** (C fires it during the call, then is done) | **stack** (borrowed `FnMut`, zero allocation) | `poll_fn(\|msg, hdr\| ..., limit)`, the generated `*_once` variants |
926///
927/// Prefer the stack form (`poll_fn`, `*_once`) on the hot path: it borrows the
928/// closure for the duration of the call only, so there is no `Arc`, no heap
929/// allocation, and the closure may borrow local state. Reach for `Handler`
930/// (heap) when the callback must survive past the registering call — image
931/// lifecycle handlers, error handlers, counters callbacks, anything the
932/// conductor invokes asynchronously.
933///
934/// The reference count is atomic (`Arc`), so a `Handler` may be moved to another
935/// thread; it is deliberately **not `Sync`** — callbacks fire from the conductor
936/// thread and must not be shared concurrently.
937///
938/// ## Example
939///
940/// ```no_compile
941/// use rusteron_code_gen::Handler;
942/// let handler = Handler::new(your_value);
943/// // the value is freed when the last clone of `handler` goes out of scope
944/// ```
945pub struct Handler<T> {
946    inner: std::sync::Arc<UnsafeCell<T>>,
947}
948
949// Arc's refcount is atomic, so moving a Handler (or a clone) to another thread is fine
950// as long as T itself is Send. No Sync: the C conductor thread may call into T via the
951// raw clientd pointer, so shared &Handler across threads would race on T.
952unsafe impl<T: Send> Send for Handler<T> {}
953
954/// Under the `multi-threaded` feature, `Handler` is also `Sync` so callbacks can
955/// be registered from one thread and the handle shared across threads. The
956/// underlying `Arc<UnsafeCell<T>>` is `!Sync` by construction; this impl follows
957/// the same "accepted unsoundness" policy as the handle-type impls — callbacks
958/// fire from the conductor thread only, never concurrently.
959#[cfg(feature = "multi-threaded")]
960unsafe impl<T: Send> Sync for Handler<T> {}
961
962impl<T> Clone for Handler<T> {
963    fn clone(&self) -> Self {
964        Self {
965            inner: self.inner.clone(),
966        }
967    }
968}
969
970/// Utility method for setting empty handlers
971pub struct Handlers;
972
973/// Type-level "no callback" sentinel.
974///
975/// Pass [`Handlers::NONE`] (which is `None::<&Handler<NoHandler>>`) to any
976/// callback-accepting method to leave that callback unset. `NoHandler` implements
977/// every generated callback trait, so the method's callback generic is inferred as
978/// `NoHandler` without a per-callback helper or turbofish — including methods with
979/// several callback parameters (e.g. `async_add_subscription`'s image handlers).
980/// Its callback methods are unreachable: the C side is handed a null callback +
981/// null clientd, so they can never fire.
982pub struct NoHandler;
983
984impl Handlers {
985    /// `None` for any callback parameter — pins the callback generic to
986    /// [`NoHandler`] so type inference works without a per-callback helper or
987    /// turbofish. Replaces `Handlers::no_available_image_handler()` /
988    /// `no_unavailable_image_handler()` / `no_reserved_value_supplier_handler()`
989    /// / … with one constant. Parallels `Option::None`.
990    pub const NONE: Option<&'static Handler<NoHandler>> = None;
991}
992
993impl<T> Handler<T> {
994    pub fn new(handler: T) -> Self {
995        let inner = std::sync::Arc::new(UnsafeCell::new(handler));
996        #[cfg(feature = "extra-logging")]
997        log::info!("creating handler {:?}", inner.get());
998        Self { inner }
999    }
1000
1001    #[inline(always)]
1002    pub fn as_raw(&self) -> *mut std::os::raw::c_void {
1003        self.inner.get() as *mut std::os::raw::c_void
1004    }
1005
1006    /// Get a mutable reference to the inner value.
1007    ///
1008    /// # Safety
1009    /// Caller must ensure that no other references to the inner value are active.
1010    #[inline(always)]
1011    pub unsafe fn get_mut(&self) -> &mut T {
1012        unsafe { &mut *self.inner.get() }
1013    }
1014}
1015
1016impl<T> Deref for Handler<T> {
1017    type Target = T;
1018
1019    #[inline(always)]
1020    fn deref(&self) -> &Self::Target {
1021        unsafe { &*self.inner.get() }
1022    }
1023}
1024
1025pub fn find_unused_udp_port(start_port: u16) -> Option<u16> {
1026    let end_port = u16::MAX;
1027
1028    for port in start_port..=end_port {
1029        if is_udp_port_available(port) {
1030            return Some(port);
1031        }
1032    }
1033
1034    None
1035}
1036
1037pub fn is_udp_port_available(port: u16) -> bool {
1038    std::net::UdpSocket::bind(("127.0.0.1", port)).is_ok()
1039}
1040
1041/// Represents the Aeron URI parser and handler.
1042pub struct ChannelUri {}
1043
1044impl ChannelUri {
1045    pub const AERON_SCHEME: &'static str = "aeron";
1046    pub const SPY_QUALIFIER: &'static str = "aeron-spy";
1047    pub const MAX_URI_LENGTH: usize = 4095;
1048
1049    /// Return `channel` with a `session-id` param added (replacing any existing one).
1050    ///
1051    /// Mirrors Java's `ChannelUri.addSessionId` — the standard way to build a channel
1052    /// that joins a specific session, e.g. when subscribing to an archive replay:
1053    ///
1054    /// ```
1055    /// # use rusteron_code_gen::ChannelUri;
1056    /// assert_eq!(
1057    ///     ChannelUri::add_session_id("aeron:ipc", 42),
1058    ///     "aeron:ipc?session-id=42"
1059    /// );
1060    /// assert_eq!(
1061    ///     ChannelUri::add_session_id("aeron:udp?endpoint=localhost:20121", -123),
1062    ///     "aeron:udp?endpoint=localhost:20121|session-id=-123"
1063    /// );
1064    /// ```
1065    pub fn add_session_id(channel: &str, session_id: i32) -> String {
1066        Self::set_param(channel, "session-id", &session_id.to_string())
1067    }
1068
1069    /// Return `channel` with URI param `key=value` set, replacing an existing `key`
1070    /// param if present. Other params keep their relative order; `key` goes last.
1071    pub fn set_param(channel: &str, key: &str, value: &str) -> String {
1072        let (base, params) = match channel.split_once('?') {
1073            None => (channel, ""),
1074            Some((base, params)) => (base, params),
1075        };
1076        let mut out = String::with_capacity(channel.len() + key.len() + value.len() + 2);
1077        out.push_str(base);
1078        out.push('?');
1079        for param in params.split('|') {
1080            if param.is_empty() || param.split('=').next() == Some(key) {
1081                continue;
1082            }
1083            out.push_str(param);
1084            out.push('|');
1085        }
1086        out.push_str(key);
1087        out.push('=');
1088        out.push_str(value);
1089        out
1090    }
1091}
1092
1093pub const DRIVER_TIMEOUT_MS_DEFAULT: u64 = 10_000;
1094pub const AERON_DIR_PROP_NAME: &str = "aeron.dir";
1095pub const AERON_IPC_MEDIA: &str = "aeron:ipc";
1096pub const AERON_UDP_MEDIA: &str = "aeron:udp";
1097pub const SPY_PREFIX: &str = "aeron-spy:";
1098pub const TAG_PREFIX: &str = "tag:";
1099
1100/// Enum for media types.
1101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1102pub enum Media {
1103    Ipc,
1104    Udp,
1105}
1106
1107impl Media {
1108    pub fn as_str(&self) -> &'static str {
1109        match self {
1110            Media::Ipc => "ipc",
1111            Media::Udp => "udp",
1112        }
1113    }
1114}
1115
1116/// Enum for control modes.
1117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1118pub enum ControlMode {
1119    Manual,
1120    Dynamic,
1121    /// this is a beta feature useful when dealing with docker containers and networking
1122    Response,
1123}
1124
1125impl ControlMode {
1126    pub fn as_str(&self) -> &'static str {
1127        match self {
1128            ControlMode::Manual => "manual",
1129            ControlMode::Dynamic => "dynamic",
1130            ControlMode::Response => "response",
1131        }
1132    }
1133}
1134
1135#[cfg(test)]
1136#[allow(dead_code)]
1137pub(crate) mod test_alloc {
1138    use std::alloc::{GlobalAlloc, Layout, System};
1139    use std::env;
1140    use std::fs::OpenOptions;
1141    #[allow(unused_imports)]
1142    use std::os::unix::fs::OpenOptionsExt;
1143    use std::sync::atomic::{AtomicIsize, Ordering};
1144
1145    /// A simple global allocator that tracks the net allocation count.
1146    /// Used mainly for testing memory leaks or unintended allocations.
1147    pub struct TrackingAllocator {
1148        allocs: AtomicIsize,
1149    }
1150
1151    impl TrackingAllocator {
1152        pub const fn new() -> Self {
1153            Self {
1154                allocs: AtomicIsize::new(0),
1155            }
1156        }
1157        pub fn current(&self) -> isize {
1158            self.allocs.load(Ordering::SeqCst)
1159        }
1160    }
1161
1162    unsafe impl GlobalAlloc for TrackingAllocator {
1163        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
1164            self.allocs.fetch_add(1, Ordering::SeqCst);
1165            unsafe { System.alloc(layout) }
1166        }
1167        unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
1168            self.allocs.fetch_sub(1, Ordering::SeqCst);
1169            unsafe { System.dealloc(ptr, layout) }
1170        }
1171        unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
1172            self.allocs.fetch_add(1, Ordering::SeqCst);
1173            unsafe { System.alloc_zeroed(layout) }
1174        }
1175        unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
1176            unsafe { System.realloc(ptr, layout, new_size) }
1177        }
1178    }
1179
1180    #[global_allocator]
1181    static GLOBAL: TrackingAllocator = TrackingAllocator::new();
1182
1183    /// Returns the current number of net allocations
1184    pub fn current_allocs() -> isize {
1185        GLOBAL.current()
1186    }
1187
1188    /// Asserts that no allocations occur within the provided closure.
1189    /// Uses a file lock to ensure exclusive access across threads/tests.
1190    pub fn assert_no_allocation<F: FnOnce()>(f: F) {
1191        let tmp = env::temp_dir().join("rusteron_allocation.lck");
1192
1193        #[cfg(unix)]
1194        let file = {
1195            OpenOptions::new()
1196                .read(true)
1197                .write(true)
1198                .create(true)
1199                .mode(0o600)
1200                .open(&tmp)
1201                .expect("Failed to open allocation lock file")
1202        };
1203        #[cfg(not(unix))]
1204        let file = {
1205            OpenOptions::new()
1206                .read(true)
1207                .write(true)
1208                .create(true)
1209                .open(&tmp)
1210                .expect("Failed to open allocation lock file")
1211        };
1212
1213        let mut lock = fd_lock::RwLock::new(file);
1214        let lock = lock.write().expect("Failed to acquire file lock");
1215
1216        // Background threads from earlier #[serial] tests (driver/conductor shutdown,
1217        // captured log buffers) can allocate or free during our window and produce
1218        // spurious deltas. Take the baseline only once the global count is stable.
1219        let mut before = current_allocs();
1220        let settle_deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
1221        loop {
1222            std::thread::sleep(std::time::Duration::from_millis(5));
1223            let now = current_allocs();
1224            if now == before || std::time::Instant::now() > settle_deadline {
1225                before = now;
1226                break;
1227            }
1228            before = now;
1229        }
1230
1231        f();
1232        let after = current_allocs();
1233        let diff = (after - before).abs();
1234        assert!(
1235            diff < 50,
1236            "Expected no allocation leak, but alloc count changed from {} to {} (diff {})",
1237            before,
1238            after,
1239            diff
1240        );
1241
1242        drop(lock)
1243    }
1244}
1245
1246/// `format!` for C strings: builds the formatted [`String`] and converts it to a
1247/// [`CString`](std::ffi::CString) in one visibly-named step.
1248///
1249/// This is the recommended way to build **dynamic** channel URIs and other C-string
1250/// arguments. The `c`-prefix keeps the heap allocation greppable and visible at the
1251/// call site — important for latency-sensitive code review — while removing the
1252/// `format!(...).into_c_string()` noise:
1253///
1254/// ```
1255/// # use rusteron_code_gen::cformat;
1256/// let port = 4040;
1257/// let uri = cformat!("aeron:udp?endpoint=localhost:{port}");
1258/// assert_eq!(uri.to_bytes(), b"aeron:udp?endpoint=localhost:4040");
1259/// ```
1260///
1261/// The three-tier pattern for C-string arguments (cheapest first):
1262/// 1. **`c"aeron:ipc"` literals** — compile-time `&'static CStr`, zero runtime cost.
1263///    Use for every constant channel/name.
1264/// 2. **`cformat!(...)`** — one heap allocation (the formatted `String`; `CString::new`
1265///    reuses its buffer). Use for dynamic URIs built once per stream/reconnect.
1266/// 3. **Reuse** — build the `CString` once, store it, pass `&it` on every call
1267///    (zero-copy via `&CString → &CStr` deref). Use for anything on a repeated path.
1268///
1269/// Panics if the formatted string contains an interior nul byte.
1270#[macro_export]
1271macro_rules! cformat {
1272    ($($arg:tt)*) => {
1273        ::std::ffi::CString::new(::std::format!($($arg)*))
1274            .expect("nul byte in cformat! string")
1275    };
1276}
1277
1278pub trait IntoCString {
1279    fn into_c_string(self) -> std::ffi::CString;
1280}
1281
1282impl IntoCString for std::ffi::CString {
1283    fn into_c_string(self) -> std::ffi::CString {
1284        self
1285    }
1286}
1287
1288impl IntoCString for &str {
1289    fn into_c_string(self) -> std::ffi::CString {
1290        #[cfg(feature = "extra-logging")]
1291        log::info!("created c string on heap: {:?}", self);
1292
1293        std::ffi::CString::new(self).expect("failed to create CString")
1294    }
1295}
1296
1297impl IntoCString for String {
1298    fn into_c_string(self) -> std::ffi::CString {
1299        #[cfg(feature = "extra-logging")]
1300        log::info!("created c string on heap: {:?}", self);
1301
1302        std::ffi::CString::new(self).expect("failed to create CString")
1303    }
1304}
1305
1306#[cfg(test)]
1307mod handler_tests {
1308    use super::*;
1309
1310    #[test]
1311    fn clones_share_the_same_clientd_pointer() {
1312        let handler = Handler::new(42u32);
1313        let clone = handler.clone();
1314        // C receives the same clientd pointer regardless of which clone
1315        // registered it, so callbacks always see the same value.
1316        assert_eq!(handler.as_raw(), clone.as_raw());
1317        assert_eq!(*handler, 42);
1318    }
1319
1320    #[test]
1321    fn value_dropped_exactly_once_when_last_clone_drops() {
1322        use std::sync::atomic::{AtomicUsize, Ordering};
1323        static DROPS: AtomicUsize = AtomicUsize::new(0);
1324        struct Counted;
1325        impl Drop for Counted {
1326            fn drop(&mut self) {
1327                DROPS.fetch_add(1, Ordering::SeqCst);
1328            }
1329        }
1330
1331        let handler = Handler::new(Counted);
1332        let clone = handler.clone();
1333        drop(handler);
1334        assert_eq!(DROPS.load(Ordering::SeqCst), 0, "value must outlive remaining clones");
1335        drop(clone);
1336        assert_eq!(DROPS.load(Ordering::SeqCst), 1, "value freed exactly once on last drop");
1337    }
1338}
1339
1340#[cfg(test)]
1341mod managed_c_resource_lifecycle_tests {
1342    use super::*;
1343
1344    // These tests pin down `manual_close_required` — the field that powers the
1345    // Drop-time leak warning. The exact `AeronCncMetadata::load_from_file` bug
1346    // was a `new(_, None, false)` resource, so the first test asserts that
1347    // construction shape trips the flag. The resource pointers are dummies
1348    // (never dereferenced); only the field value is checked.
1349
1350    #[test]
1351    #[cfg(not(feature = "strict-lifecycle"))] // would panic on drop under strict-lifecycle
1352    fn manual_close_required_true_for_none_cleanup_no_struct() {
1353        // The bug shape: owned, no cleanup closure, no Rust struct ownership.
1354        let r: ManagedCResource<u8> = ManagedCResource::new(
1355            |ctx| {
1356                unsafe { *ctx = 0x1 as *mut u8 };
1357                1
1358            },
1359            None,
1360            false,
1361        )
1362        .unwrap_or_else(|e| panic!("init failed: code {}", e.code));
1363        assert!(
1364            r.manual_close_required,
1365            "owned + None cleanup + no struct ownership must require manual close"
1366        );
1367    }
1368
1369    #[test]
1370    fn manual_close_required_false_when_cleanup_closure_present() {
1371        let r: ManagedCResource<u8> = ManagedCResource::new(
1372            |ctx| {
1373                unsafe { *ctx = 0x1 as *mut u8 };
1374                1
1375            },
1376            Some(Box::new(|_ctx| 0)),
1377            false,
1378        )
1379        .unwrap_or_else(|e| panic!("init failed: code {}", e.code));
1380        assert!(
1381            !r.manual_close_required,
1382            "real cleanup closure means Drop frees the resource — no warning needed"
1383        );
1384    }
1385
1386    #[test]
1387    fn manual_close_required_false_when_struct_owned() {
1388        // Generated `new(_, None, true)` resources: Rust frees the Box itself
1389        // via Box::from_raw in the cleanup_struct branch of Drop.
1390        let r: ManagedCResource<u8> = ManagedCResource::new(
1391            |ctx| {
1392                unsafe { *ctx = Box::into_raw(Box::new(0u8)) };
1393                1
1394            },
1395            None,
1396            true,
1397        )
1398        .unwrap_or_else(|e| panic!("init failed: code {}", e.code));
1399        assert!(
1400            !r.manual_close_required,
1401            "cleanup_struct=true means Rust owns and frees the struct — no warning needed"
1402        );
1403    }
1404}