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