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