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