Skip to main content

rusteron_code_gen/
lib.rs

1#![allow(improper_ctypes_definitions)]
2#![allow(non_upper_case_globals)]
3#![allow(non_camel_case_types)]
4#![allow(non_snake_case)]
5#![allow(clippy::all)]
6#![allow(unused_unsafe)]
7#![allow(unused_variables)]
8#![doc = include_str!("../README.md")]
9
10mod arg_classifier;
11mod common;
12mod generator;
13mod parser;
14pub mod test_logger;
15
16pub use arg_classifier::*;
17pub use common::*;
18pub use generator::*;
19pub use parser::*;
20
21use proc_macro2::TokenStream;
22use std::fs::OpenOptions;
23use std::io::Write;
24use std::path::Path;
25use std::process::{Command, Stdio};
26
27pub const CUSTOM_AERON_CODE: &str = include_str!("./aeron_custom.rs");
28pub const CUSTOM_ARCHIVE_CODE: &str = include_str!("./aeron_custom_archive.rs");
29pub const COMMON_CODE: &str = include_str!("./common.rs");
30
31/// Minimal `AeronArchiveError` stub for the `archive` trybuild test only — the real
32/// type (with typed codes, parsing, Display) lives in `aeron_custom_archive.rs` and is
33/// pulled into rusteron-archive via its build.rs. The trybuild test compiles generated
34/// code in isolation, so the real CUSTOM_ARCHIVE_CODE can't be used (it references
35/// hand-written rusteron-archive/src/lib.rs types). Generated aeron_archive_t methods
36/// only reference `AeronArchiveError::from_code(i32)`, so a bare struct suffices.
37#[cfg(test)]
38const TRYBUILD_ARCHIVE_ERROR_STUB: &str = r#"
39    #[derive(Debug, Clone)]
40    pub struct AeronArchiveError { pub code: i32, pub message: String }
41    impl AeronArchiveError { pub fn from_code(code: i32) -> Self { Self { code, message: String::new() } } }
42    impl std::fmt::Display for AeronArchiveError {
43        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "archive error {}", self.code) }
44    }
45    impl std::error::Error for AeronArchiveError {}
46"#;
47
48pub fn append_to_file(file_path: &str, code: &str) -> std::io::Result<()> {
49    let path = Path::new(file_path);
50    if let Some(parent) = path.parent() {
51        if !parent.as_os_str().is_empty() {
52            std::fs::create_dir_all(parent)?;
53        }
54    }
55
56    let mut file = OpenOptions::new().create(true).write(true).append(true).open(path)?;
57
58    writeln!(file, "\n{}", code)?;
59
60    Ok(())
61}
62
63#[allow(dead_code)]
64pub fn format_with_rustfmt(code: &str) -> Result<String, std::io::Error> {
65    let mut rustfmt = Command::new("rustfmt")
66        .stdin(Stdio::piped())
67        .stdout(Stdio::piped())
68        .spawn()?;
69
70    if let Some(mut stdin) = rustfmt.stdin.take() {
71        stdin.write_all(code.as_bytes())?;
72    }
73
74    let output = rustfmt.wait_with_output()?;
75
76    if !output.status.success() {
77        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
78        return Err(std::io::Error::new(
79            std::io::ErrorKind::Other,
80            format!("rustfmt failed: {}", stderr),
81        ));
82    }
83
84    let formatted_code = String::from_utf8_lossy(&output.stdout).to_string();
85
86    // If the input was non-empty but the output is empty, that's an error
87    // But if the input was empty/whitespace only, empty output is fine
88    if !code.trim().is_empty() && formatted_code.trim().is_empty() {
89        Err(std::io::Error::new(
90            std::io::ErrorKind::Other,
91            "rustfmt produced empty output",
92        ))
93    } else {
94        Ok(formatted_code)
95    }
96}
97
98#[allow(dead_code)]
99pub fn format_token_stream(tokens: TokenStream) -> String {
100    let code = tokens.to_string();
101
102    match format_with_rustfmt(&code) {
103        Ok(formatted_code) if !formatted_code.trim().is_empty() => formatted_code,
104        _ => code.replace("{", "{\n"), // Fallback to unformatted code in case of error
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use crate::generator::MEDIA_DRIVER_BINDINGS;
111    use crate::parser::parse_bindings;
112    use crate::{
113        append_to_file, format_token_stream, format_with_rustfmt, ARCHIVE_BINDINGS, CLIENT_BINDINGS, CUSTOM_AERON_CODE,
114        TRYBUILD_ARCHIVE_ERROR_STUB,
115    };
116    use proc_macro2::TokenStream;
117    use std::fs;
118
119    // valgrind can give false positives, so we don't want to run on tests which are 100% rust
120    // and do not have any chance of any undefined behaviour i.e. parsing rs and generating code
121    fn running_under_valgrind() -> bool {
122        std::env::var_os("RUSTERON_VALGRIND").is_some()
123    }
124
125    #[test]
126    #[cfg(not(target_os = "windows"))] // the generated bindings have different sizes
127    fn client() {
128        if running_under_valgrind() {
129            return;
130        }
131
132        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
133            .join("bindings")
134            .join("client.rs");
135        let mut bindings = parse_bindings(&path);
136        assert_eq!(
137            "AeronImageFragmentAssembler",
138            bindings
139                .wrappers
140                .get("aeron_image_fragment_assembler_t")
141                .unwrap()
142                .class_name
143        );
144        assert_eq!(
145            0,
146            bindings.methods.len(),
147            "expected all methods to have been matched {:#?}",
148            bindings.methods
149        );
150
151        let file = write_to_file(TokenStream::new(), true, "client.rs");
152        let bindings_copy = bindings.clone();
153        for handler in bindings.handlers.iter_mut() {
154            // need to run this first so I know the FnMut(xxxx) which is required in generate_rust_code
155            let _ = crate::generate_handlers(handler, &bindings_copy);
156        }
157        for (p, w) in bindings.wrappers.values().enumerate() {
158            let code = crate::generate_rust_code(w, &bindings.wrappers, p == 0, true, true, &bindings.handlers);
159            write_to_file(code, false, "client.rs");
160        }
161        let bindings_copy = bindings.clone();
162        for handler in bindings.handlers.iter_mut() {
163            let code = crate::generate_handlers(handler, &bindings_copy);
164            append_to_file(&file, &format_with_rustfmt(&code.to_string()).unwrap()).unwrap();
165        }
166
167        let t = trybuild::TestCases::new();
168        append_to_file(&file, "use bindings::*; mod bindings { ").unwrap();
169        append_to_file(&file, CLIENT_BINDINGS).unwrap();
170        append_to_file(&file, "}").unwrap();
171        append_to_file(&file, CUSTOM_AERON_CODE).unwrap();
172        append_to_file(&file, "\npub fn main() {}\n").unwrap();
173        t.pass(file)
174    }
175
176    #[test]
177    #[cfg(not(target_os = "windows"))] // the generated bindings have different sizes
178    fn media_driver() {
179        if running_under_valgrind() {
180            return;
181        }
182
183        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
184            .join("bindings")
185            .join("media-driver.rs");
186        let mut bindings = parse_bindings(&path);
187        assert_eq!(
188            "AeronImageFragmentAssembler",
189            bindings
190                .wrappers
191                .get("aeron_image_fragment_assembler_t")
192                .unwrap()
193                .class_name
194        );
195
196        let file = write_to_file(TokenStream::new(), true, "md.rs");
197
198        let bindings_copy = bindings.clone();
199        for handler in bindings.handlers.iter_mut() {
200            // need to run this first so I know the FnMut(xxxx) which is required in generate_rust_code
201            let _ = crate::generate_handlers(handler, &bindings_copy);
202        }
203        for (p, w) in bindings
204            .wrappers
205            .values()
206            .filter(|w| !w.type_name.contains("pthread") && !w.type_name.contains("_t_"))
207            .enumerate()
208        {
209            let code = crate::generate_rust_code(w, &bindings.wrappers, p == 0, true, true, &bindings.handlers);
210            write_to_file(code, false, "md.rs");
211        }
212        let bindings_copy = bindings.clone();
213        for handler in bindings.handlers.iter_mut() {
214            let code = crate::generate_handlers(handler, &bindings_copy);
215            append_to_file(&file, &format_with_rustfmt(&code.to_string()).unwrap()).unwrap();
216        }
217        let t = trybuild::TestCases::new();
218        append_to_file(&file, "use bindings::*; mod bindings { ").unwrap();
219        append_to_file(&file, MEDIA_DRIVER_BINDINGS).unwrap();
220        append_to_file(&file, "}").unwrap();
221        append_to_file(&file, CUSTOM_AERON_CODE).unwrap();
222        append_to_file(&file, "\npub fn main() {}\n").unwrap();
223        t.pass(&file)
224    }
225
226    #[test]
227    #[cfg(not(target_os = "windows"))] // the generated bindings have different sizes
228    fn archive() {
229        if running_under_valgrind() {
230            return;
231        }
232
233        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
234            .join("bindings")
235            .join("archive.rs");
236        let mut bindings = parse_bindings(&path);
237        assert_eq!(
238            "AeronImageFragmentAssembler",
239            bindings
240                .wrappers
241                .get("aeron_image_fragment_assembler_t")
242                .unwrap()
243                .class_name
244        );
245
246        let file = write_to_file(TokenStream::new(), true, "archive.rs");
247        let bindings_copy = bindings.clone();
248        for handler in bindings.handlers.iter_mut() {
249            // need to run this first so I know the FnMut(xxxx) which is required in generate_rust_code
250            let _ = crate::generate_handlers(handler, &bindings_copy);
251        }
252        for (p, w) in bindings.wrappers.values().enumerate() {
253            let code = crate::generate_rust_code(w, &bindings.wrappers, p == 0, true, true, &bindings.handlers);
254            write_to_file(code, false, "archive.rs");
255        }
256        let bindings_copy = bindings.clone();
257        for handler in bindings.handlers.iter_mut() {
258            let code = crate::generate_handlers(handler, &bindings_copy);
259            append_to_file(&file, &format_with_rustfmt(&code.to_string()).unwrap()).unwrap();
260        }
261        let t = trybuild::TestCases::new();
262        append_to_file(&file, "use bindings::*; mod bindings { ").unwrap();
263        append_to_file(&file, ARCHIVE_BINDINGS).unwrap();
264        append_to_file(&file, "}").unwrap();
265        append_to_file(&file, CUSTOM_AERON_CODE).unwrap();
266        // The generated aeron_archive_t methods reference AeronArchiveError, which in the
267        // real crate is supplied by CUSTOM_ARCHIVE_CODE. That file also references
268        // hand-written rusteron-archive/src/lib.rs types (e.g. AeronArchiveAsyncConnect),
269        // so it can't be compiled standalone here. Inject a minimal stub instead — the
270        // trybuild test only checks generated signatures compile; the real type's
271        // behaviour is covered by rusteron-archive's own tests.
272        append_to_file(&file, TRYBUILD_ARCHIVE_ERROR_STUB).unwrap();
273        append_to_file(&file, "\npub fn main() {}\n").unwrap();
274        t.pass(file)
275    }
276
277    /// Regenerate `aeron.rs` exactly as `build.rs` does (single rustfmt pass over
278    /// the whole stream, test-modules disabled) and assert it byte-matches the
279    /// committed `docs-rs/aeron.rs`. Catches "forgot to rebuild with
280    /// `COPY_BINDINGS=true`" drift as a CI failure instead of a stale docs.rs.
281    fn assert_aeron_rs_snapshot_matches(
282        bindings_file: &str,
283        snapshot_rel_path: &str,
284        skip_filter: fn(&str) -> bool,
285        extra_custom_code: &[&str],
286    ) {
287        if running_under_valgrind() {
288            return;
289        }
290
291        let bindings_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
292            .join("bindings")
293            .join(bindings_file);
294        let snapshot_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(snapshot_rel_path);
295
296        let mut bindings = crate::parse_bindings_with_custom(&bindings_path, extra_custom_code);
297        // First pass: populate FnMut(...) names required by generate_rust_code.
298        let bindings_copy = bindings.clone();
299        for handler in bindings.handlers.iter_mut() {
300            let _ = crate::generate_handlers(handler, &bindings_copy);
301        }
302
303        // Mirror build.rs: build ONE stream, format in a single rustfmt pass.
304        let mut stream = TokenStream::new();
305        for (p, w) in bindings
306            .wrappers
307            .values()
308            .filter(|w| skip_filter(&w.type_name))
309            .enumerate()
310        {
311            let code = crate::generate_rust_code(
312                w,
313                &bindings.wrappers,
314                p == 0,
315                false, // build.rs uses `false` (no test modules) for the committed snapshot
316                true,
317                &bindings.handlers,
318            );
319            stream.extend(code);
320        }
321        let bindings_copy = bindings.clone();
322        for handler in bindings.handlers.iter_mut() {
323            let code = crate::generate_handlers(handler, &bindings_copy);
324            stream.extend(code);
325        }
326
327        let generated = format_with_rustfmt(&stream.to_string())
328            .unwrap_or_else(|_| panic!("rustfmt failed on regenerated {bindings_file}"));
329        let committed = fs::read_to_string(&snapshot_path)
330            .unwrap_or_else(|_| panic!("missing committed snapshot: {}", snapshot_path.display()));
331
332        // Normalise surrounding whitespace: build.rs writes the snapshot via
333        // `append_to_file`, which wraps content in `\n{}\n`, so leading/trailing
334        // blank lines are a write artifact, not content drift.
335        let norm = |s: &str| s.trim().to_string();
336        let (generated, committed) = (norm(&generated), norm(&committed));
337        if generated != committed {
338            // First diverging line gives a focused failure message.
339            let mut line_no = 0;
340            for (i, (g, c)) in generated.lines().zip(committed.lines()).enumerate() {
341                if g != c {
342                    line_no = i + 1;
343                    break;
344                }
345            }
346            if line_no == 0 {
347                line_no = generated.lines().count().min(committed.lines().count()) + 1;
348            }
349            panic!(
350                "docs-rs snapshot drift in {}: generated aeron.rs differs from committed \
351                 {} (first divergence near line {}). GENERATED first 3 lines:\n{}\n\
352                 COMMITTED first 3 lines:\n{}\nRebuild with `COPY_BINDINGS=true` \
353                 (e.g. `just build`) and commit the regenerated snapshot.",
354                bindings_file,
355                snapshot_path.display(),
356                line_no,
357                generated.lines().take(3).collect::<Vec<_>>().join("\n"),
358                committed.lines().take(3).collect::<Vec<_>>().join("\n"),
359            );
360        }
361    }
362
363    #[test]
364    #[cfg(not(target_os = "windows"))]
365    fn client_aeron_rs_matches_committed_snapshot() {
366        assert_aeron_rs_snapshot_matches(
367            "client.rs",
368            "../rusteron-client/docs-rs/aeron.rs",
369            |t| t.starts_with("aeron_"),
370            &[],
371        );
372    }
373
374    #[test]
375    #[cfg(not(target_os = "windows"))]
376    fn archive_aeron_rs_matches_committed_snapshot() {
377        // Archive's build.rs merges CUSTOM_ARCHIVE_CODE into the generator skip-list
378        // (so hand-written archive methods like ReplayMerge::poll_fn/poll_once suppress
379        // the generated variant). The snapshot regeneration must mirror that, or the
380        // committed snapshot (built with the merge) drifts from the regeneration.
381        //
382        // Not macOS-gated: the test parses the COMMITTED bindings file
383        // (rusteron-code-gen/bindings/archive.rs), so regeneration is deterministic
384        // regardless of host platform.
385        assert_aeron_rs_snapshot_matches(
386            "archive.rs",
387            "../rusteron-archive/docs-rs/aeron.rs",
388            |t| t.starts_with("aeron_"),
389            &[crate::CUSTOM_ARCHIVE_CODE],
390        );
391    }
392
393    #[test]
394    #[cfg(not(target_os = "windows"))]
395    fn media_driver_aeron_rs_matches_committed_snapshot() {
396        // Mirrors the media_driver trybuild test's filter.
397        assert_aeron_rs_snapshot_matches(
398            "media-driver.rs",
399            "../rusteron-media-driver/docs-rs/aeron.rs",
400            |t| !t.contains("pthread") && !t.contains("_t_"),
401            &[],
402        );
403    }
404
405    /// `aeron_custom.rs` is a verbatim copy (not generated) into each crate's
406    /// `docs-rs/`. Assert the committed copies stay byte-identical to the source
407    /// of truth (`rusteron-code-gen/src/aeron_custom.rs`).
408    #[test]
409    fn aeron_custom_rs_snapshots_match_source() {
410        // each crate's committed aeron_custom.rs = the common custom code plus any
411        // per-crate custom code appended by its build config (archive only, today)
412        let no_extra: &[&str] = &[];
413        for (crate_name, extra) in [
414            ("client", no_extra),
415            ("archive", &[crate::CUSTOM_ARCHIVE_CODE][..]),
416            ("media-driver", no_extra),
417        ] {
418            // emulate append_to_file: each chunk is written as "\n{chunk}\n"
419            let mut source = format!("\n{}\n", CUSTOM_AERON_CODE);
420            for code in extra {
421                source.push_str(&format!("\n{}\n", code));
422            }
423            let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
424                .join(format!("../rusteron-{crate_name}/docs-rs/aeron_custom.rs"));
425            let committed = fs::read_to_string(&path)
426                .unwrap_or_else(|_| panic!("missing committed aeron_custom.rs: {}", path.display()));
427            assert_eq!(
428                committed.trim(),
429                source.trim(),
430                "rusteron-{crate_name}/docs-rs/aeron_custom.rs drifted from \
431                 rusteron-code-gen/src/aeron_custom*.rs. Rebuild with `COPY_BINDINGS=true`.",
432            );
433        }
434    }
435
436    fn write_to_file(rust_code: TokenStream, delete: bool, name: &str) -> String {
437        let src = format_token_stream(rust_code);
438        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("../target/{}", name));
439        let path = path.to_str().unwrap();
440        if delete {
441            let _ = fs::remove_file(path);
442        }
443        append_to_file(path, &src).unwrap();
444        path.to_string()
445    }
446}
447
448#[cfg(test)]
449mod test {
450    use crate::{CResource, CleanupBox, ManagedCResource};
451
452    use crate::common::RcOrArc;
453    use std::sync::atomic::{AtomicBool, Ordering};
454    use std::sync::Arc;
455    #[allow(unused_imports)]
456    use RcOrArc as Rc;
457
458    fn make_resource(val: i32) -> *mut i32 {
459        Box::into_raw(Box::new(val))
460    }
461
462    unsafe fn reclaim_resource(ptr: *mut i32) {
463        if !ptr.is_null() {
464            let _ = Box::from_raw(ptr);
465        }
466    }
467
468    #[test]
469    fn test_drop_calls_cleanup_non_borrowed_no_cleanup_struct() {
470        let flag = Arc::new(AtomicBool::new(false));
471        let flag_clone = flag.clone();
472        let resource_ptr = make_resource(10);
473
474        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
475            flag_clone.store(true, Ordering::SeqCst);
476            // Set the resource to null to simulate cleanup.
477            unsafe {
478                reclaim_resource(*res);
479                *res = std::ptr::null_mut();
480            }
481            0
482        }) as CleanupBox<i32>);
483
484        {
485            let _resource = ManagedCResource::new(
486                |res: *mut *mut i32| {
487                    unsafe {
488                        *res = resource_ptr;
489                    }
490                    0
491                },
492                cleanup,
493                false,
494            );
495            assert!(_resource.is_ok())
496        }
497        assert!(flag.load(Ordering::SeqCst));
498    }
499
500    #[test]
501    fn test_drop_calls_cleanup_non_borrowed_with_cleanup_struct() {
502        let flag = Arc::new(AtomicBool::new(false));
503        let flag_clone = flag.clone();
504        let resource_ptr = make_resource(20);
505
506        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
507            flag_clone.store(true, Ordering::SeqCst);
508            unsafe {
509                *res = std::ptr::null_mut();
510            }
511            0
512        }) as CleanupBox<i32>);
513
514        {
515            let _resource = ManagedCResource::new(
516                |res: *mut *mut i32| {
517                    unsafe {
518                        *res = resource_ptr;
519                    }
520                    0
521                },
522                cleanup,
523                true,
524            );
525            assert!(_resource.is_ok())
526        }
527        assert!(flag.load(Ordering::SeqCst));
528    }
529
530    #[test]
531    fn test_drop_does_not_call_cleanup_if_already_closed() {
532        let flag = Arc::new(AtomicBool::new(false));
533        let flag_clone = flag.clone();
534        let resource_ptr = make_resource(30);
535
536        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
537            flag_clone.store(true, Ordering::SeqCst);
538            unsafe {
539                reclaim_resource(*res);
540                *res = std::ptr::null_mut();
541            }
542            0
543        }) as CleanupBox<i32>);
544
545        let resource = ManagedCResource::new(
546            |res: *mut *mut i32| {
547                unsafe {
548                    *res = resource_ptr;
549                }
550                0
551            },
552            cleanup,
553            false,
554        );
555        assert!(resource.is_ok());
556
557        if let Ok(ref resource) = resource {
558            assert!(resource.close_shared().is_ok());
559        }
560
561        // Reset the flag to ensure drop does not call cleanup a second time.
562        flag.store(false, Ordering::SeqCst);
563        drop(resource);
564        assert!(!flag.load(Ordering::SeqCst));
565    }
566
567    #[test]
568    fn close_resource_closes_shared_resource_once_across_clones() {
569        let close_count = RcOrArc::new(std::sync::atomic::AtomicI32::new(0));
570        let close_count_for_cleanup = close_count.clone();
571        let resource_ptr = make_resource(40);
572
573        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
574            close_count_for_cleanup.fetch_add(1, Ordering::SeqCst);
575            unsafe {
576                reclaim_resource(*res);
577                *res = std::ptr::null_mut();
578            }
579            0
580        }) as CleanupBox<i32>);
581
582        let resource = ManagedCResource::new(
583            |res: *mut *mut i32| {
584                unsafe {
585                    *res = resource_ptr;
586                }
587                0
588            },
589            cleanup,
590            false,
591        );
592        assert!(resource.is_ok());
593        let resource = resource.ok().unwrap();
594        let inner = Rc::new(resource);
595        let handle_one = CResource::OwnedOnHeap(inner.clone());
596        let handle_two = CResource::OwnedOnHeap(inner);
597
598        assert!(!handle_one.get().is_null());
599        assert!(!handle_two.get().is_null());
600
601        assert!(handle_one.close_resource().is_ok());
602        assert_eq!(1, close_count.load(Ordering::SeqCst));
603        assert!(handle_one.get().is_null());
604        assert!(handle_two.get().is_null());
605
606        assert!(handle_two.close_resource().is_ok());
607        assert_eq!(1, close_count.load(Ordering::SeqCst));
608    }
609
610    #[test]
611    fn close_resource_defers_owner_close_while_dependent_is_alive() {
612        let close_count = RcOrArc::new(std::sync::atomic::AtomicI32::new(0));
613        let close_count_for_cleanup = close_count.clone();
614        let owner_ptr = make_resource(60);
615        let child_ptr = make_resource(61);
616
617        let owner_cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
618            close_count_for_cleanup.fetch_add(1, Ordering::SeqCst);
619            unsafe {
620                reclaim_resource(*res);
621                *res = std::ptr::null_mut();
622            }
623            0
624        }) as CleanupBox<i32>);
625
626        let owner = ManagedCResource::new(
627            |res: *mut *mut i32| {
628                unsafe {
629                    *res = owner_ptr;
630                }
631                0
632            },
633            owner_cleanup,
634            false,
635        );
636        assert!(owner.is_ok());
637        let owner = CResource::OwnedOnHeap(Rc::new(owner.ok().unwrap()));
638
639        let child = ManagedCResource::new(
640            |res: *mut *mut i32| {
641                unsafe {
642                    *res = child_ptr;
643                }
644                0
645            },
646            Some(Box::new(move |res| {
647                unsafe {
648                    reclaim_resource(*res);
649                    *res = std::ptr::null_mut();
650                }
651                0
652            })),
653            false,
654        );
655        assert!(child.is_ok());
656        let child = CResource::OwnedOnHeap(Rc::new(child.ok().unwrap()));
657        child.add_dependency(owner.clone());
658
659        assert!(owner.close_resource_deferred_if_shared().is_ok());
660        assert_eq!(0, close_count.load(Ordering::SeqCst));
661        assert!(!owner.get().is_null());
662
663        drop(owner);
664        drop(child);
665        assert_eq!(1, close_count.load(Ordering::SeqCst));
666    }
667
668    #[test]
669    fn close_resource_deferral_is_retryable_if_owner_close_fails_after_dependent_drops() {
670        let close_count = RcOrArc::new(std::sync::atomic::AtomicI32::new(0));
671        let close_count_for_cleanup = close_count.clone();
672        let owner_ptr = make_resource(70);
673        let child_ptr = make_resource(71);
674
675        let owner_cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
676            let count = close_count_for_cleanup.fetch_add(1, Ordering::SeqCst) + 1;
677            if count == 1 {
678                return -1;
679            }
680            unsafe {
681                reclaim_resource(*res);
682                *res = std::ptr::null_mut();
683            }
684            0
685        }) as CleanupBox<i32>);
686
687        let owner = ManagedCResource::new(
688            |res: *mut *mut i32| {
689                unsafe {
690                    *res = owner_ptr;
691                }
692                0
693            },
694            owner_cleanup,
695            false,
696        );
697        assert!(owner.is_ok());
698        let owner = CResource::OwnedOnHeap(Rc::new(owner.ok().unwrap()));
699        let owner_retry_handle = owner.clone();
700
701        let child = ManagedCResource::new(
702            |res: *mut *mut i32| {
703                unsafe {
704                    *res = child_ptr;
705                }
706                0
707            },
708            Some(Box::new(move |res| {
709                unsafe {
710                    reclaim_resource(*res);
711                    *res = std::ptr::null_mut();
712                }
713                0
714            })),
715            false,
716        );
717        assert!(child.is_ok());
718        let child = CResource::OwnedOnHeap(Rc::new(child.ok().unwrap()));
719        child.add_dependency(owner.clone());
720
721        assert!(owner.close_resource_deferred_if_shared().is_ok());
722        drop(owner);
723        drop(child);
724        assert_eq!(0, close_count.load(Ordering::SeqCst));
725        assert!(!owner_retry_handle.get().is_null());
726
727        assert!(owner_retry_handle.close_resource_deferred_if_shared().is_err());
728        assert_eq!(1, close_count.load(Ordering::SeqCst));
729        assert!(!owner_retry_handle.get().is_null());
730
731        assert!(owner_retry_handle.close_resource_deferred_if_shared().is_ok());
732        assert_eq!(2, close_count.load(Ordering::SeqCst));
733        assert!(owner_retry_handle.get().is_null());
734    }
735
736    #[test]
737    fn close_resource_propagates_failure_and_allows_retry() {
738        let close_count = RcOrArc::new(std::sync::atomic::AtomicI32::new(0));
739        let close_count_for_cleanup = close_count.clone();
740        let resource_ptr = make_resource(50);
741
742        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
743            let count = close_count_for_cleanup.fetch_add(1, Ordering::SeqCst) + 1;
744            if count == 1 {
745                return -1;
746            }
747            unsafe {
748                reclaim_resource(*res);
749                *res = std::ptr::null_mut();
750            }
751            0
752        }) as CleanupBox<i32>);
753
754        let resource = ManagedCResource::new(
755            |res: *mut *mut i32| {
756                unsafe {
757                    *res = resource_ptr;
758                }
759                0
760            },
761            cleanup,
762            false,
763        );
764        assert!(resource.is_ok());
765        let resource = resource.ok().unwrap();
766        let handle = CResource::OwnedOnHeap(Rc::new(resource));
767
768        assert!(handle.close_resource().is_err());
769        assert_eq!(1, close_count.load(Ordering::SeqCst));
770        assert!(!handle.get().is_null());
771
772        assert!(handle.close_resource().is_ok());
773        assert_eq!(2, close_count.load(Ordering::SeqCst));
774        assert!(handle.get().is_null());
775    }
776
777    #[test]
778    fn close_resource_with_uses_custom_cleanup_once_across_clones() {
779        let default_close_count = RcOrArc::new(std::sync::atomic::AtomicI32::new(0));
780        let custom_close_count = RcOrArc::new(std::sync::atomic::AtomicI32::new(0));
781        let default_close_count_for_cleanup = default_close_count.clone();
782        let resource_ptr = make_resource(80);
783
784        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
785            default_close_count_for_cleanup.fetch_add(1, Ordering::SeqCst);
786            unsafe {
787                reclaim_resource(*res);
788                *res = std::ptr::null_mut();
789            }
790            0
791        }) as CleanupBox<i32>);
792
793        let resource = ManagedCResource::new(
794            |res: *mut *mut i32| {
795                unsafe {
796                    *res = resource_ptr;
797                }
798                0
799            },
800            cleanup,
801            false,
802        );
803        assert!(resource.is_ok());
804        let inner = Rc::new(resource.ok().unwrap());
805        let handle_one = CResource::OwnedOnHeap(inner.clone());
806        let handle_two = CResource::OwnedOnHeap(inner);
807
808        let custom_close_count_for_cleanup = custom_close_count.clone();
809        assert!(handle_one
810            .close_resource_with(move |res| {
811                custom_close_count_for_cleanup.fetch_add(1, Ordering::SeqCst);
812                unsafe {
813                    reclaim_resource(*res);
814                    *res = std::ptr::null_mut();
815                }
816                0
817            })
818            .is_ok());
819
820        assert_eq!(0, default_close_count.load(Ordering::SeqCst));
821        assert_eq!(1, custom_close_count.load(Ordering::SeqCst));
822        assert!(handle_one.get().is_null());
823        assert!(handle_two.get().is_null());
824
825        assert!(handle_two.close_resource().is_ok());
826        assert_eq!(0, default_close_count.load(Ordering::SeqCst));
827        assert_eq!(1, custom_close_count.load(Ordering::SeqCst));
828    }
829
830    #[test]
831    fn close_resource_with_failure_restores_default_cleanup_for_retry() {
832        let default_close_count = RcOrArc::new(std::sync::atomic::AtomicI32::new(0));
833        let custom_close_count = RcOrArc::new(std::sync::atomic::AtomicI32::new(0));
834        let default_close_count_for_cleanup = default_close_count.clone();
835        let resource_ptr = make_resource(90);
836
837        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
838            default_close_count_for_cleanup.fetch_add(1, Ordering::SeqCst);
839            unsafe {
840                reclaim_resource(*res);
841                *res = std::ptr::null_mut();
842            }
843            0
844        }) as CleanupBox<i32>);
845
846        let resource = ManagedCResource::new(
847            |res: *mut *mut i32| {
848                unsafe {
849                    *res = resource_ptr;
850                }
851                0
852            },
853            cleanup,
854            false,
855        );
856        assert!(resource.is_ok());
857        let handle = CResource::OwnedOnHeap(Rc::new(resource.ok().unwrap()));
858
859        let custom_close_count_for_cleanup = custom_close_count.clone();
860        assert!(handle
861            .close_resource_with(move |_res| {
862                custom_close_count_for_cleanup.fetch_add(1, Ordering::SeqCst);
863                -1
864            })
865            .is_err());
866
867        assert_eq!(0, default_close_count.load(Ordering::SeqCst));
868        assert_eq!(1, custom_close_count.load(Ordering::SeqCst));
869        assert!(!handle.get().is_null());
870
871        assert!(handle.close_resource().is_ok());
872        assert_eq!(1, default_close_count.load(Ordering::SeqCst));
873        assert_eq!(1, custom_close_count.load(Ordering::SeqCst));
874        assert!(handle.get().is_null());
875    }
876
877    #[test]
878    fn close_resource_is_noop_for_stack_and_borrowed_resources() {
879        let mut borrowed_value = 10;
880        let borrowed = CResource::Borrowed(&mut borrowed_value as *mut i32);
881        assert!(borrowed.close_resource().is_ok());
882        assert_eq!(10, borrowed_value);
883
884        let stack = CResource::OwnedOnStack(std::mem::MaybeUninit::new(20));
885        assert!(stack.close_resource().is_ok());
886        assert_eq!(20, unsafe { *stack.get() });
887    }
888
889    // NOTE: test_drop_does_not_call_cleanup_if_check_for_is_closed_* removed
890    // because check_for_is_closed was deleted — the cleanup closure + Rc graph
891    // are now the sole teardown mechanism (see rusteron-code-gen/src/common.rs).
892}