Skip to main content

rusteron_code_gen/
common.rs

1use crate::AeronErrorType::Unknown;
2#[cfg(feature = "backtrace")]
3use std::backtrace::Backtrace;
4use std::cell::UnsafeCell;
5use std::fmt::Formatter;
6use std::mem::MaybeUninit;
7use std::ops::{Deref, DerefMut};
8pub enum CResource<T> {
9    OwnedOnHeap(std::rc::Rc<ManagedCResource<T>>),
10    /// Always initialised by construction (zeroed or `new(v)`). Never store
11    /// `uninit()` — `Clone` and `get()` assume it's valid.
12    OwnedOnStack(std::mem::MaybeUninit<T>),
13    Borrowed(*mut T),
14}
15
16impl<T: Clone> Clone for CResource<T> {
17    fn clone(&self) -> Self {
18        // SAFETY: each branch only dereferences pointers/references that are
19        // valid by construction. `OwnedOnStack` upholds the initialised-by-
20        // construction invariant documented on the variant, so `assume_init_ref`
21        // is sound.
22        unsafe {
23            match self {
24                CResource::OwnedOnHeap(r) => CResource::OwnedOnHeap(r.clone()),
25                CResource::OwnedOnStack(r) => CResource::OwnedOnStack(MaybeUninit::new(r.assume_init_ref().clone())),
26                CResource::Borrowed(r) => CResource::Borrowed(r.clone()),
27            }
28        }
29    }
30}
31
32impl<T> CResource<T> {
33    #[inline]
34    pub fn get(&self) -> *mut T {
35        match self {
36            CResource::OwnedOnHeap(r) => r.get(),
37            CResource::OwnedOnStack(r) => r.as_ptr() as *mut T,
38            CResource::Borrowed(r) => *r,
39        }
40    }
41
42    #[inline]
43    // to prevent the dependencies from being dropped as you have a copy here
44    pub fn add_dependency<D: std::any::Any>(&self, dep: D) {
45        match self {
46            CResource::OwnedOnHeap(r) => r.add_dependency(dep),
47            CResource::OwnedOnStack(_) | CResource::Borrowed(_) => {
48                unreachable!("only owned on heap")
49            }
50        }
51    }
52    #[inline]
53    pub fn get_dependency<V: Clone + 'static>(&self) -> Option<V> {
54        match self {
55            CResource::OwnedOnHeap(r) => r.get_dependency(),
56            CResource::OwnedOnStack(_) | CResource::Borrowed(_) => None,
57        }
58    }
59
60    #[inline]
61    pub fn as_owned(&self) -> Option<&std::rc::Rc<ManagedCResource<T>>> {
62        match self {
63            CResource::OwnedOnHeap(r) => Some(r),
64            CResource::OwnedOnStack(_) | CResource::Borrowed(_) => None,
65        }
66    }
67}
68
69impl<T> std::fmt::Debug for CResource<T> {
70    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
71        let name = std::any::type_name::<T>();
72
73        match self {
74            CResource::OwnedOnHeap(r) => {
75                write!(f, "{name} heap({:?})", r)
76            }
77            CResource::OwnedOnStack(r) => {
78                write!(f, "{name} stack({:?})", *r)
79            }
80            CResource::Borrowed(r) => {
81                write!(f, "{name} borrowed ({:?})", r)
82            }
83        }
84    }
85}
86
87/// A custom struct for managing C resources with automatic cleanup.
88///
89/// It handles initialisation and clean-up of the resource and ensures that resources
90/// are properly released when they go out of scope.
91#[allow(dead_code)]
92pub struct ManagedCResource<T> {
93    resource: *mut T,
94    cleanup: Option<Box<dyn FnMut(*mut *mut T) -> i32>>,
95    cleanup_struct: bool,
96    /// if someone externally rusteron calls close
97    close_already_called: std::cell::Cell<bool>,
98    /// if there is a c method to verify it someone has closed it, only few structs have this functionality
99    check_for_is_closed: Option<fn(*mut T) -> bool>,
100    /// this will be called if closed hasn't already happened even if its borrowed
101    auto_close: std::cell::Cell<bool>,
102    /// indicates if the underlying resource has already been handed off and should not be re-polled
103    resource_released: std::cell::Cell<bool>,
104    /// Keeps deps alive (e.g. the Aeron client while a pub/sub exists).
105    /// Mutated only at construction from the owning thread — no locking,
106    /// same Send-over-Rc unsoundness stance. Empty vec doesn't allocate.
107    dependencies: UnsafeCell<Vec<std::rc::Rc<dyn std::any::Any>>>,
108}
109
110impl<T> std::fmt::Debug for ManagedCResource<T> {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        let mut debug_struct = f.debug_struct("ManagedCResource");
113
114        if !self.close_already_called.get()
115            && !self.resource.is_null()
116            && !self.check_for_is_closed.as_ref().map_or(false, |f| f(self.resource))
117        {
118            debug_struct.field("resource", &self.resource);
119        }
120
121        debug_struct.field("type", &std::any::type_name::<T>()).finish()
122    }
123}
124
125impl<T> ManagedCResource<T> {
126    /// Creates a new ManagedCResource with a given initializer and cleanup function.
127    ///
128    /// The initializer is a closure that attempts to initialize the resource.
129    /// If initialization fails, the initializer should return an error code.
130    /// The cleanup function is used to release the resource when it's no longer needed.
131    /// `cleanup_struct` where it should clean up the struct in rust
132    pub fn new(
133        init: impl FnOnce(*mut *mut T) -> i32,
134        cleanup: Option<Box<dyn FnMut(*mut *mut T) -> i32>>,
135        cleanup_struct: bool,
136        check_for_is_closed: Option<fn(*mut T) -> bool>,
137    ) -> Result<Self, AeronCError> {
138        let resource = Self::initialise(init)?;
139
140        let result = Self {
141            resource,
142            cleanup,
143            cleanup_struct,
144            close_already_called: std::cell::Cell::new(false),
145            check_for_is_closed,
146            auto_close: std::cell::Cell::new(false),
147            resource_released: std::cell::Cell::new(false),
148            dependencies: UnsafeCell::new(vec![]),
149        };
150        #[cfg(feature = "extra-logging")]
151        log::info!("created c resource: {:?}", result);
152        Ok(result)
153    }
154
155    pub fn initialise(init: impl FnOnce(*mut *mut T) -> i32 + Sized) -> Result<*mut T, AeronCError> {
156        let mut resource: *mut T = std::ptr::null_mut();
157        let result = init(&mut resource);
158        if result < 0 || resource.is_null() {
159            return Err(AeronCError::from_code(result));
160        }
161        Ok(resource)
162    }
163
164    pub fn is_closed_already_called(&self) -> bool {
165        self.close_already_called.get()
166            || self.resource.is_null()
167            || self.check_for_is_closed.as_ref().map_or(false, |f| f(self.resource))
168    }
169
170    /// Gets a raw pointer to the resource.
171    #[inline(always)]
172    pub fn get(&self) -> *mut T {
173        self.resource
174    }
175
176    #[inline(always)]
177    pub fn get_mut(&self) -> &mut T {
178        unsafe { &mut *self.resource }
179    }
180
181    #[inline]
182    // to prevent the dependencies from being dropped as you have a copy here
183    pub fn add_dependency<D: std::any::Any>(&self, dep: D) {
184        if let Some(dep) = (&dep as &dyn std::any::Any).downcast_ref::<std::rc::Rc<dyn std::any::Any>>() {
185            unsafe {
186                (*self.dependencies.get()).push(dep.clone());
187            }
188        } else {
189            unsafe {
190                (*self.dependencies.get()).push(std::rc::Rc::new(dep));
191            }
192        }
193    }
194
195    #[inline]
196    pub fn get_dependency<V: Clone + 'static>(&self) -> Option<V> {
197        unsafe {
198            (*self.dependencies.get())
199                .iter()
200                .filter_map(|x| x.as_ref().downcast_ref::<V>().cloned())
201                .next()
202        }
203    }
204
205    #[inline]
206    pub fn is_resource_released(&self) -> bool {
207        self.resource_released.get()
208    }
209
210    #[inline]
211    pub fn mark_resource_released(&self) {
212        self.resource_released.set(true);
213    }
214
215    /// Closes the resource by calling the cleanup function.
216    ///
217    /// If cleanup fails, it returns an `AeronError`.
218    pub fn close(&mut self) -> Result<(), AeronCError> {
219        if self.close_already_called.get() {
220            return Ok(());
221        }
222        self.close_already_called.set(true);
223
224        let already_closed = self.check_for_is_closed.as_ref().map_or(false, |f| f(self.resource));
225
226        if let Some(mut cleanup) = self.cleanup.take() {
227            if !self.resource.is_null() {
228                if !already_closed {
229                    let result = cleanup(&mut self.resource);
230                    if result < 0 {
231                        return Err(AeronCError::from_code(result));
232                    }
233                }
234                self.resource = std::ptr::null_mut();
235            }
236        }
237
238        Ok(())
239    }
240}
241
242impl<T> Drop for ManagedCResource<T> {
243    fn drop(&mut self) {
244        if !self.resource.is_null() {
245            let already_closed = self.close_already_called.get()
246                || self.check_for_is_closed.as_ref().map_or(false, |f| f(self.resource));
247
248            let resource = if already_closed {
249                self.resource
250            } else {
251                self.resource.clone()
252            };
253
254            if !already_closed {
255                // Ensure the clean-up function is called when the resource is dropped.
256                #[cfg(feature = "extra-logging")]
257                log::info!("closing c resource: {:?}", self);
258                let _ = self.close(); // Ignore errors during an automatic drop to avoid panics.
259            }
260            self.close_already_called.set(true);
261
262            if self.cleanup_struct {
263                #[cfg(feature = "extra-logging")]
264                log::info!("closing rust struct resource: {:?}", resource);
265                unsafe {
266                    let _ = Box::from_raw(resource);
267                }
268            }
269        }
270    }
271}
272
273#[derive(Debug, PartialOrd, Eq, PartialEq, Clone)]
274pub enum AeronErrorType {
275    GenericError,
276    ClientErrorDriverTimeout,
277    ClientErrorClientTimeout,
278    ClientErrorConductorServiceTimeout,
279    ClientErrorBufferFull,
280    PublicationBackPressured,
281    PublicationAdminAction,
282    PublicationClosed,
283    PublicationMaxPositionExceeded,
284    PublicationError,
285    TimedOut,
286    Unknown(i32),
287}
288
289impl From<AeronErrorType> for AeronCError {
290    fn from(value: AeronErrorType) -> Self {
291        AeronCError::from_code(value.code())
292    }
293}
294
295impl AeronErrorType {
296    pub fn code(&self) -> i32 {
297        match self {
298            AeronErrorType::GenericError => -1,
299            AeronErrorType::ClientErrorDriverTimeout => -1000,
300            AeronErrorType::ClientErrorClientTimeout => -1001,
301            AeronErrorType::ClientErrorConductorServiceTimeout => -1002,
302            AeronErrorType::ClientErrorBufferFull => -1003,
303            AeronErrorType::PublicationBackPressured => -2,
304            AeronErrorType::PublicationAdminAction => -3,
305            AeronErrorType::PublicationClosed => -4,
306            AeronErrorType::PublicationMaxPositionExceeded => -5,
307            AeronErrorType::PublicationError => -6,
308            AeronErrorType::TimedOut => -234324,
309            AeronErrorType::Unknown(code) => *code,
310        }
311    }
312
313    pub fn is_back_pressured(&self) -> bool {
314        self == &AeronErrorType::PublicationBackPressured
315    }
316
317    pub fn is_admin_action(&self) -> bool {
318        self == &AeronErrorType::PublicationAdminAction
319    }
320
321    pub fn is_back_pressured_or_admin_action(&self) -> bool {
322        self.is_back_pressured() || self.is_admin_action()
323    }
324
325    pub fn from_code(code: i32) -> Self {
326        match code {
327            -1 => AeronErrorType::GenericError,
328            -1000 => AeronErrorType::ClientErrorDriverTimeout,
329            -1001 => AeronErrorType::ClientErrorClientTimeout,
330            -1002 => AeronErrorType::ClientErrorConductorServiceTimeout,
331            -1003 => AeronErrorType::ClientErrorBufferFull,
332            -2 => AeronErrorType::PublicationBackPressured,
333            -3 => AeronErrorType::PublicationAdminAction,
334            -4 => AeronErrorType::PublicationClosed,
335            -5 => AeronErrorType::PublicationMaxPositionExceeded,
336            -6 => AeronErrorType::PublicationError,
337            -234324 => AeronErrorType::TimedOut,
338            _ => Unknown(code),
339        }
340    }
341
342    pub fn to_string(&self) -> &'static str {
343        match self {
344            AeronErrorType::GenericError => "Generic Error",
345            AeronErrorType::ClientErrorDriverTimeout => "Client Error Driver Timeout",
346            AeronErrorType::ClientErrorClientTimeout => "Client Error Client Timeout",
347            AeronErrorType::ClientErrorConductorServiceTimeout => "Client Error Conductor Service Timeout",
348            AeronErrorType::ClientErrorBufferFull => "Client Error Buffer Full",
349            AeronErrorType::PublicationBackPressured => "Publication Back Pressured",
350            AeronErrorType::PublicationAdminAction => "Publication Admin Action",
351            AeronErrorType::PublicationClosed => "Publication Closed",
352            AeronErrorType::PublicationMaxPositionExceeded => "Publication Max Position Exceeded",
353            AeronErrorType::PublicationError => "Publication Error",
354            AeronErrorType::TimedOut => "Timed Out",
355            AeronErrorType::Unknown(_) => "Unknown Error",
356        }
357    }
358}
359
360/// Represents an Aeron-specific error with a code and an optional message.
361///
362/// The error code is derived from Aeron C API calls.
363/// Use `get_last_err_message()` to retrieve the last human-readable message, if available.
364#[derive(Eq, PartialEq, Clone)]
365pub struct AeronCError {
366    pub code: i32,
367}
368
369impl AeronCError {
370    /// Creates an AeronError from the error code returned by Aeron.
371    ///
372    /// Error codes below zero are considered failure.
373    pub fn from_code(code: i32) -> Self {
374        #[cfg(feature = "backtrace")]
375        {
376            if code < 0 {
377                let backtrace = Backtrace::capture();
378                let backtrace = format!("{:?}", backtrace);
379
380                // Compile the backtrace-parsing regex ONCE, not per error.
381                // `from_code` sits on the error path; re-compiling the regex on
382                // every Aeron error (including back-pressure retries) is wasteful.
383                static BACKTRACE_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
384                let re = BACKTRACE_RE
385                    .get_or_init(|| regex::Regex::new(r#"fn: "([^"]+)", file: "([^"]+)", line: (\d+)"#).unwrap());
386                let mut lines = String::new();
387                re.captures_iter(&backtrace).for_each(|cap| {
388                    let function = &cap[1];
389                    let mut file = cap[2].to_string();
390                    let line = &cap[3];
391                    if file.starts_with("./") {
392                        file = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), &file[2..]);
393                    } else if file.starts_with("/rustc/") {
394                        file = file.split("/").last().unwrap().to_string();
395                    }
396                    // log in intellij friendly error format so can hyperlink to source code in stack trace
397                    lines.push_str(&format!(" {file}:{line} in {function}\n"));
398                });
399
400                log::error!(
401                    "Aeron C error code: {}, kind: '{:?}'\n{}",
402                    code,
403                    AeronErrorType::from_code(code),
404                    lines
405                );
406            }
407        }
408        AeronCError { code }
409    }
410
411    pub fn kind(&self) -> AeronErrorType {
412        AeronErrorType::from_code(self.code)
413    }
414
415    pub fn is_back_pressured(&self) -> bool {
416        self.kind().is_back_pressured()
417    }
418
419    pub fn is_admin_action(&self) -> bool {
420        self.kind().is_admin_action()
421    }
422
423    pub fn is_back_pressured_or_admin_action(&self) -> bool {
424        self.kind().is_back_pressured_or_admin_action()
425    }
426}
427
428/// # Handler
429///
430/// `Handler` is a struct that wraps a raw pointer and a drop flag.
431///
432/// Memory is freed automatically when `Handler` goes out of scope (via `Drop`).
433/// You must ensure the `Handler` outlives the Aeron session that uses it, since Aeron
434/// holds a raw `clientd` pointer to the boxed value and will call callbacks until closed.
435///
436/// Call `release()` early if you want to free the memory before the `Handler` drops.
437///
438/// ## Example
439///
440/// ```no_compile
441/// use rusteron_code_gen::Handler;
442/// let handler = Handler::leak(your_value);
443/// // handler is freed automatically when it goes out of scope
444/// ```
445pub struct Handler<T> {
446    raw_ptr: *mut T,
447    should_drop: bool,
448}
449
450unsafe impl<T> Send for Handler<T> {}
451unsafe impl<T> Sync for Handler<T> {}
452
453/// Utility method for setting empty handlers
454pub struct Handlers;
455
456impl<T> Handler<T> {
457    pub fn leak(handler: T) -> Self {
458        let raw_ptr = Box::into_raw(Box::new(handler)) as *mut _;
459        #[cfg(feature = "extra-logging")]
460        log::info!("creating handler {:?}", raw_ptr);
461        Self {
462            raw_ptr,
463            should_drop: true,
464        }
465    }
466
467    pub fn is_none(&self) -> bool {
468        self.raw_ptr.is_null()
469    }
470
471    pub fn as_raw(&self) -> *mut std::os::raw::c_void {
472        self.raw_ptr as *mut std::os::raw::c_void
473    }
474
475    pub fn release(&mut self) {
476        if self.should_drop && !self.raw_ptr.is_null() {
477            unsafe {
478                #[cfg(feature = "extra-logging")]
479                log::info!("dropping handler {:?}", self.raw_ptr);
480                let _ = Box::from_raw(self.raw_ptr as *mut T);
481                self.should_drop = false;
482                // Null the pointer so a subsequent `Deref`/`DerefMut`/`is_none()`
483                // cannot reach freed memory. Without this, `release()` left
484                // `raw_ptr` dangling (freed-but-not-nulled) and any later deref
485                // would be use-after-free. `Drop` only acts when `should_drop`,
486                // which we already cleared, so the null is safe.
487                self.raw_ptr = std::ptr::null_mut();
488            }
489        }
490    }
491
492    pub unsafe fn new(raw_ptr: *mut T, should_drop: bool) -> Self {
493        Self { raw_ptr, should_drop }
494    }
495}
496
497impl<T> Drop for Handler<T> {
498    fn drop(&mut self) {
499        if self.should_drop && !self.raw_ptr.is_null() {
500            log::error!(
501                "Handler<{}> at {:?} is being dropped but release() was never called — \
502                 memory leak: {} bytes. Call release() explicitly when the C side no longer holds the pointer.",
503                std::any::type_name::<T>(),
504                self.raw_ptr,
505                std::mem::size_of::<T>(),
506            );
507        }
508    }
509}
510
511impl<T> Deref for Handler<T> {
512    type Target = T;
513
514    fn deref(&self) -> &Self::Target {
515        unsafe { &*self.raw_ptr as &T }
516    }
517}
518
519impl<T> DerefMut for Handler<T> {
520    fn deref_mut(&mut self) -> &mut Self::Target {
521        unsafe { &mut *self.raw_ptr as &mut T }
522    }
523}
524
525pub fn find_unused_udp_port(start_port: u16) -> Option<u16> {
526    let end_port = u16::MAX;
527
528    for port in start_port..=end_port {
529        if is_udp_port_available(port) {
530            return Some(port);
531        }
532    }
533
534    None
535}
536
537pub fn is_udp_port_available(port: u16) -> bool {
538    std::net::UdpSocket::bind(("127.0.0.1", port)).is_ok()
539}
540
541/// Represents the Aeron URI parser and handler.
542pub struct ChannelUri {}
543
544impl ChannelUri {
545    pub const AERON_SCHEME: &'static str = "aeron";
546    pub const SPY_QUALIFIER: &'static str = "aeron-spy";
547    pub const MAX_URI_LENGTH: usize = 4095;
548}
549
550pub const DRIVER_TIMEOUT_MS_DEFAULT: u64 = 10_000;
551pub const AERON_DIR_PROP_NAME: &str = "aeron.dir";
552pub const AERON_IPC_MEDIA: &str = "aeron:ipc";
553pub const AERON_UDP_MEDIA: &str = "aeron:udp";
554pub const SPY_PREFIX: &str = "aeron-spy:";
555pub const TAG_PREFIX: &str = "tag:";
556
557/// Enum for media types.
558#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
559pub enum Media {
560    Ipc,
561    Udp,
562}
563
564impl Media {
565    pub fn as_str(&self) -> &'static str {
566        match self {
567            Media::Ipc => "ipc",
568            Media::Udp => "udp",
569        }
570    }
571}
572
573/// Enum for control modes.
574#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
575pub enum ControlMode {
576    Manual,
577    Dynamic,
578    /// this is a beta feature useful when dealing with docker containers and networking
579    Response,
580}
581
582impl ControlMode {
583    pub fn as_str(&self) -> &'static str {
584        match self {
585            ControlMode::Manual => "manual",
586            ControlMode::Dynamic => "dynamic",
587            ControlMode::Response => "response",
588        }
589    }
590}
591
592#[cfg(test)]
593#[allow(dead_code)]
594pub(crate) mod test_alloc {
595    use std::alloc::{GlobalAlloc, Layout, System};
596    use std::env;
597    use std::fs::OpenOptions;
598    #[allow(unused_imports)]
599    use std::os::unix::fs::OpenOptionsExt;
600    use std::sync::atomic::{AtomicIsize, Ordering};
601
602    /// A simple global allocator that tracks the net allocation count.
603    /// Used mainly for testing memory leaks or unintended allocations.
604    pub struct TrackingAllocator {
605        allocs: AtomicIsize,
606    }
607
608    impl TrackingAllocator {
609        pub const fn new() -> Self {
610            Self {
611                allocs: AtomicIsize::new(0),
612            }
613        }
614        pub fn current(&self) -> isize {
615            self.allocs.load(Ordering::SeqCst)
616        }
617    }
618
619    unsafe impl GlobalAlloc for TrackingAllocator {
620        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
621            self.allocs.fetch_add(1, Ordering::SeqCst);
622            System.alloc(layout)
623        }
624        unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
625            self.allocs.fetch_sub(1, Ordering::SeqCst);
626            System.dealloc(ptr, layout)
627        }
628        unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
629            self.allocs.fetch_add(1, Ordering::SeqCst);
630            System.alloc_zeroed(layout)
631        }
632        unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
633            System.realloc(ptr, layout, new_size)
634        }
635    }
636
637    #[global_allocator]
638    static GLOBAL: TrackingAllocator = TrackingAllocator::new();
639
640    /// Returns the current number of net allocations
641    pub fn current_allocs() -> isize {
642        GLOBAL.current()
643    }
644
645    /// Asserts that no allocations occur within the provided closure.
646    /// Uses a file lock to ensure exclusive access across threads/tests.
647    pub fn assert_no_allocation<F: FnOnce()>(f: F) {
648        let tmp = env::temp_dir().join("rusteron_allocation.lck");
649
650        #[cfg(unix)]
651        let file = {
652            OpenOptions::new()
653                .read(true)
654                .write(true)
655                .create(true)
656                .mode(0o600)
657                .open(&tmp)
658                .expect("Failed to open allocation lock file")
659        };
660        #[cfg(not(unix))]
661        let file = {
662            OpenOptions::new()
663                .read(true)
664                .write(true)
665                .create(true)
666                .open(&tmp)
667                .expect("Failed to open allocation lock file")
668        };
669
670        let mut lock = fd_lock::RwLock::new(file);
671        let lock = lock.write().expect("Failed to acquire file lock");
672
673        let before = current_allocs();
674        f();
675        let after = current_allocs();
676        let diff = (after - before).abs();
677        assert!(
678            diff < 50,
679            "Expected no allocation leak, but alloc count changed from {} to {} (diff {})",
680            before,
681            after,
682            diff
683        );
684
685        drop(lock)
686    }
687}
688
689pub trait IntoCString {
690    fn into_c_string(self) -> std::ffi::CString;
691}
692
693impl IntoCString for std::ffi::CString {
694    fn into_c_string(self) -> std::ffi::CString {
695        self
696    }
697}
698
699impl IntoCString for &str {
700    fn into_c_string(self) -> std::ffi::CString {
701        #[cfg(feature = "extra-logging")]
702        log::info!("created c string on heap: {:?}", self);
703
704        std::ffi::CString::new(self).expect("failed to create CString")
705    }
706}
707
708impl IntoCString for String {
709    fn into_c_string(self) -> std::ffi::CString {
710        #[cfg(feature = "extra-logging")]
711        log::info!("created c string on heap: {:?}", self);
712
713        std::ffi::CString::new(self).expect("failed to create CString")
714    }
715}
716
717#[cfg(test)]
718mod handler_tests {
719    use super::*;
720
721    #[test]
722    fn release_nulls_pointer_so_is_none_is_true() {
723        let mut handler = Handler::leak(42u32);
724        // Boxed value lives on the heap before release.
725        assert!(!handler.is_none(), "freshly leaked handler must be non-null");
726
727        handler.release();
728
729        // After release the inner pointer is nulled, not dangling — so a later
730        // Deref/is_none cannot reach freed memory.
731        assert!(handler.is_none(), "release() must null raw_ptr (was a UAF)");
732    }
733
734    #[test]
735    fn release_is_idempotent_no_double_free() {
736        let mut handler = Handler::leak(99u64);
737        handler.release();
738        // A second release must be a no-op: the guard `should_drop && !null`
739        // is false, so Box::from_raw is not called again (no double-free).
740        handler.release();
741        assert!(handler.is_none());
742    }
743
744    #[test]
745    fn release_then_drop_is_silent() {
746        // After release, should_drop is false and raw_ptr is null, so Drop must
747        // not log the "memory leak" warning nor touch freed memory.
748        let mut handler = Handler::leak(7u16);
749        handler.release();
750        drop(handler);
751    }
752}