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 common;
11mod generator;
12mod parser;
13pub mod test_logger;
14
15pub use common::*;
16pub use generator::*;
17pub use parser::*;
18
19use proc_macro2::TokenStream;
20use std::fs::OpenOptions;
21use std::io::Write;
22use std::path::Path;
23use std::process::{Command, Stdio};
24
25pub const CUSTOM_AERON_CODE: &str = include_str!("./aeron_custom.rs");
26pub const COMMON_CODE: &str = include_str!("./common.rs");
27
28pub fn append_to_file(file_path: &str, code: &str) -> std::io::Result<()> {
29    let path = Path::new(file_path);
30    if let Some(parent) = path.parent() {
31        if !parent.as_os_str().is_empty() {
32            std::fs::create_dir_all(parent)?;
33        }
34    }
35
36    let mut file = OpenOptions::new()
37        .create(true)
38        .write(true)
39        .append(true)
40        .open(path)?;
41
42    writeln!(file, "\n{}", code)?;
43
44    Ok(())
45}
46
47#[allow(dead_code)]
48pub fn format_with_rustfmt(code: &str) -> Result<String, std::io::Error> {
49    let mut rustfmt = Command::new("rustfmt")
50        .stdin(Stdio::piped())
51        .stdout(Stdio::piped())
52        .spawn()?;
53
54    if let Some(mut stdin) = rustfmt.stdin.take() {
55        stdin.write_all(code.as_bytes())?;
56    }
57
58    let output = rustfmt.wait_with_output()?;
59
60    if !output.status.success() {
61        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
62        return Err(std::io::Error::new(
63            std::io::ErrorKind::Other,
64            format!("rustfmt failed: {}", stderr),
65        ));
66    }
67
68    let formatted_code = String::from_utf8_lossy(&output.stdout).to_string();
69
70    // If the input was non-empty but the output is empty, that's an error
71    // But if the input was empty/whitespace only, empty output is fine
72    if !code.trim().is_empty() && formatted_code.trim().is_empty() {
73        Err(std::io::Error::new(
74            std::io::ErrorKind::Other,
75            "rustfmt produced empty output",
76        ))
77    } else {
78        Ok(formatted_code)
79    }
80}
81
82#[allow(dead_code)]
83pub fn format_token_stream(tokens: TokenStream) -> String {
84    let code = tokens.to_string();
85
86    match format_with_rustfmt(&code) {
87        Ok(formatted_code) if !formatted_code.trim().is_empty() => formatted_code,
88        _ => code.replace("{", "{\n"), // Fallback to unformatted code in case of error
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use crate::generator::MEDIA_DRIVER_BINDINGS;
95    use crate::parser::parse_bindings;
96    use crate::{
97        append_to_file, format_token_stream, format_with_rustfmt, ARCHIVE_BINDINGS,
98        CLIENT_BINDINGS, CUSTOM_AERON_CODE,
99    };
100    use proc_macro2::TokenStream;
101    use std::fs;
102
103    // valgrind can give false positives, so we don't want to run on tests which are 100% rust
104    // and do not have any chance of any undefined behaviour i.e. parsing rs and generating code
105    fn running_under_valgrind() -> bool {
106        std::env::var_os("RUSTERON_VALGRIND").is_some()
107    }
108
109    #[test]
110    #[cfg(not(target_os = "windows"))] // the generated bindings have different sizes
111    fn client() {
112        if running_under_valgrind() {
113            return;
114        }
115
116        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
117            .join("bindings")
118            .join("client.rs");
119        let mut bindings = parse_bindings(&path);
120        assert_eq!(
121            "AeronImageFragmentAssembler",
122            bindings
123                .wrappers
124                .get("aeron_image_fragment_assembler_t")
125                .unwrap()
126                .class_name
127        );
128        assert_eq!(
129            0,
130            bindings.methods.len(),
131            "expected all methods to have been matched {:#?}",
132            bindings.methods
133        );
134
135        let file = write_to_file(TokenStream::new(), true, "client.rs");
136        let bindings_copy = bindings.clone();
137        for handler in bindings.handlers.iter_mut() {
138            // need to run this first so I know the FnMut(xxxx) which is required in generate_rust_code
139            let _ = crate::generate_handlers(handler, &bindings_copy);
140        }
141        for (p, w) in bindings.wrappers.values().enumerate() {
142            let code = crate::generate_rust_code(
143                w,
144                &bindings.wrappers,
145                p == 0,
146                true,
147                true,
148                &bindings.handlers,
149            );
150            write_to_file(code, false, "client.rs");
151        }
152        let bindings_copy = bindings.clone();
153        for handler in bindings.handlers.iter_mut() {
154            let code = crate::generate_handlers(handler, &bindings_copy);
155            append_to_file(&file, &format_with_rustfmt(&code.to_string()).unwrap()).unwrap();
156        }
157
158        let t = trybuild::TestCases::new();
159        append_to_file(&file, "use bindings::*; mod bindings { ").unwrap();
160        append_to_file(&file, CLIENT_BINDINGS).unwrap();
161        append_to_file(&file, "}").unwrap();
162        append_to_file(&file, CUSTOM_AERON_CODE).unwrap();
163        append_to_file(&file, "\npub fn main() {}\n").unwrap();
164        t.pass(file)
165    }
166
167    #[test]
168    #[cfg(not(target_os = "windows"))] // the generated bindings have different sizes
169    fn media_driver() {
170        if running_under_valgrind() {
171            return;
172        }
173
174        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
175            .join("bindings")
176            .join("media-driver.rs");
177        let mut bindings = parse_bindings(&path);
178        assert_eq!(
179            "AeronImageFragmentAssembler",
180            bindings
181                .wrappers
182                .get("aeron_image_fragment_assembler_t")
183                .unwrap()
184                .class_name
185        );
186
187        let file = write_to_file(TokenStream::new(), true, "md.rs");
188
189        let bindings_copy = bindings.clone();
190        for handler in bindings.handlers.iter_mut() {
191            // need to run this first so I know the FnMut(xxxx) which is required in generate_rust_code
192            let _ = crate::generate_handlers(handler, &bindings_copy);
193        }
194        for (p, w) in bindings
195            .wrappers
196            .values()
197            .filter(|w| !w.type_name.contains("_t_") && w.type_name != "in_addr")
198            .enumerate()
199        {
200            let code = crate::generate_rust_code(
201                w,
202                &bindings.wrappers,
203                p == 0,
204                true,
205                true,
206                &bindings.handlers,
207            );
208            write_to_file(code, false, "md.rs");
209        }
210        let bindings_copy = bindings.clone();
211        for handler in bindings.handlers.iter_mut() {
212            let code = crate::generate_handlers(handler, &bindings_copy);
213            append_to_file(&file, &format_with_rustfmt(&code.to_string()).unwrap()).unwrap();
214        }
215        let t = trybuild::TestCases::new();
216        append_to_file(&file, "use bindings::*; mod bindings { ").unwrap();
217        append_to_file(&file, MEDIA_DRIVER_BINDINGS).unwrap();
218        append_to_file(&file, "}").unwrap();
219        append_to_file(&file, CUSTOM_AERON_CODE).unwrap();
220        append_to_file(&file, "\npub fn main() {}\n").unwrap();
221        t.pass(&file)
222    }
223
224    #[test]
225    #[cfg(not(target_os = "windows"))] // the generated bindings have different sizes
226    fn archive() {
227        if running_under_valgrind() {
228            return;
229        }
230
231        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
232            .join("bindings")
233            .join("archive.rs");
234        let mut bindings = parse_bindings(&path);
235        assert_eq!(
236            "AeronImageFragmentAssembler",
237            bindings
238                .wrappers
239                .get("aeron_image_fragment_assembler_t")
240                .unwrap()
241                .class_name
242        );
243
244        let file = write_to_file(TokenStream::new(), true, "archive.rs");
245        let bindings_copy = bindings.clone();
246        for handler in bindings.handlers.iter_mut() {
247            // need to run this first so I know the FnMut(xxxx) which is required in generate_rust_code
248            let _ = crate::generate_handlers(handler, &bindings_copy);
249        }
250        for (p, w) in bindings.wrappers.values().enumerate() {
251            let code = crate::generate_rust_code(
252                w,
253                &bindings.wrappers,
254                p == 0,
255                true,
256                true,
257                &bindings.handlers,
258            );
259            write_to_file(code, false, "archive.rs");
260        }
261        let bindings_copy = bindings.clone();
262        for handler in bindings.handlers.iter_mut() {
263            let code = crate::generate_handlers(handler, &bindings_copy);
264            append_to_file(&file, &format_with_rustfmt(&code.to_string()).unwrap()).unwrap();
265        }
266        let t = trybuild::TestCases::new();
267        append_to_file(&file, "use bindings::*; mod bindings { ").unwrap();
268        append_to_file(&file, ARCHIVE_BINDINGS).unwrap();
269        append_to_file(&file, "}").unwrap();
270        append_to_file(&file, CUSTOM_AERON_CODE).unwrap();
271        append_to_file(&file, "\npub fn main() {}\n").unwrap();
272        t.pass(file)
273    }
274
275    /// Regenerate `aeron.rs` exactly as `build.rs` does (single rustfmt pass over
276    /// the whole stream, test-modules disabled) and assert it byte-matches the
277    /// committed `docs-rs/aeron.rs`. Catches "forgot to rebuild with
278    /// `COPY_BINDINGS=true`" drift as a CI failure instead of a stale docs.rs.
279    fn assert_aeron_rs_snapshot_matches(
280        bindings_file: &str,
281        snapshot_rel_path: &str,
282        skip_filter: fn(&str) -> bool,
283    ) {
284        if running_under_valgrind() {
285            return;
286        }
287
288        let bindings_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
289            .join("bindings")
290            .join(bindings_file);
291        let snapshot_path =
292            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(snapshot_rel_path);
293
294        let mut bindings = parse_bindings(&bindings_path);
295        // First pass: populate FnMut(...) names required by generate_rust_code.
296        let bindings_copy = bindings.clone();
297        for handler in bindings.handlers.iter_mut() {
298            let _ = crate::generate_handlers(handler, &bindings_copy);
299        }
300
301        // Mirror build.rs: build ONE stream, format in a single rustfmt pass.
302        let mut stream = TokenStream::new();
303        for (p, w) in bindings
304            .wrappers
305            .values()
306            .filter(|w| skip_filter(&w.type_name))
307            .enumerate()
308        {
309            let code = crate::generate_rust_code(
310                w,
311                &bindings.wrappers,
312                p == 0,
313                false, // build.rs uses `false` (no test modules) for the committed snapshot
314                true,
315                &bindings.handlers,
316            );
317            stream.extend(code);
318        }
319        let bindings_copy = bindings.clone();
320        for handler in bindings.handlers.iter_mut() {
321            let code = crate::generate_handlers(handler, &bindings_copy);
322            stream.extend(code);
323        }
324
325        let generated = format_with_rustfmt(&stream.to_string())
326            .unwrap_or_else(|_| panic!("rustfmt failed on regenerated {bindings_file}"));
327        let committed = fs::read_to_string(&snapshot_path)
328            .unwrap_or_else(|_| panic!("missing committed snapshot: {}", snapshot_path.display()));
329
330        // Normalise surrounding whitespace: build.rs writes the snapshot via
331        // `append_to_file`, which wraps content in `\n{}\n`, so leading/trailing
332        // blank lines are a write artifact, not content drift.
333        let norm = |s: &str| s.trim().to_string();
334        let (generated, committed) = (norm(&generated), norm(&committed));
335        if generated != committed {
336            // First diverging line gives a focused failure message.
337            let mut line_no = 0;
338            for (i, (g, c)) in generated.lines().zip(committed.lines()).enumerate() {
339                if g != c {
340                    line_no = i + 1;
341                    break;
342                }
343            }
344            if line_no == 0 {
345                line_no = generated.lines().count().min(committed.lines().count()) + 1;
346            }
347            panic!(
348                "docs-rs snapshot drift in {}: generated aeron.rs differs from committed \
349                 {} (first divergence near line {}). GENERATED first 3 lines:\n{}\n\
350                 COMMITTED first 3 lines:\n{}\nRebuild with `COPY_BINDINGS=true` \
351                 (e.g. `just build`) and commit the regenerated snapshot.",
352                bindings_file,
353                snapshot_path.display(),
354                line_no,
355                generated.lines().take(3).collect::<Vec<_>>().join("\n"),
356                committed.lines().take(3).collect::<Vec<_>>().join("\n"),
357            );
358        }
359    }
360
361    #[test]
362    #[cfg(not(target_os = "windows"))]
363    fn client_aeron_rs_matches_committed_snapshot() {
364        assert_aeron_rs_snapshot_matches(
365            "client.rs",
366            "../rusteron-client/docs-rs/aeron.rs",
367            |_| true,
368        );
369    }
370
371    #[test]
372    #[cfg(not(target_os = "windows"))]
373    #[cfg(target_os = "macos")]
374    fn archive_aeron_rs_matches_committed_snapshot() {
375        assert_aeron_rs_snapshot_matches(
376            "archive.rs",
377            "../rusteron-archive/docs-rs/aeron.rs",
378            |_| true,
379        );
380    }
381
382    #[test]
383    #[cfg(not(target_os = "windows"))]
384    #[cfg(target_os = "macos")]
385    fn media_driver_aeron_rs_matches_committed_snapshot() {
386        // Mirrors the media_driver trybuild test's filter.
387        assert_aeron_rs_snapshot_matches(
388            "media-driver.rs",
389            "../rusteron-media-driver/docs-rs/aeron.rs",
390            |t| !t.contains("_t_") && t != "in_addr",
391        );
392    }
393
394    /// `aeron_custom.rs` is a verbatim copy (not generated) into each crate's
395    /// `docs-rs/`. Assert the committed copies stay byte-identical to the source
396    /// of truth (`rusteron-code-gen/src/aeron_custom.rs`).
397    #[test]
398    fn aeron_custom_rs_snapshots_match_source() {
399        let source = CUSTOM_AERON_CODE.trim();
400        for crate_name in ["client", "archive", "media-driver"] {
401            let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
402                .join(format!("../rusteron-{crate_name}/docs-rs/aeron_custom.rs"));
403            let committed = fs::read_to_string(&path).unwrap_or_else(|_| {
404                panic!("missing committed aeron_custom.rs: {}", path.display())
405            });
406            assert_eq!(
407                committed.trim(),
408                source,
409                "rusteron-{crate_name}/docs-rs/aeron_custom.rs drifted from \
410                 rusteron-code-gen/src/aeron_custom.rs. Rebuild with `COPY_BINDINGS=true`.",
411            );
412        }
413    }
414
415    fn write_to_file(rust_code: TokenStream, delete: bool, name: &str) -> String {
416        let src = format_token_stream(rust_code);
417        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
418            .join(format!("../target/{}", name));
419        let path = path.to_str().unwrap();
420        if delete {
421            let _ = fs::remove_file(path);
422        }
423        append_to_file(path, &src).unwrap();
424        path.to_string()
425    }
426}
427
428#[cfg(test)]
429mod test {
430    use crate::ManagedCResource;
431
432    use std::sync::atomic::{AtomicBool, Ordering};
433    use std::sync::Arc;
434
435    fn make_resource(val: i32) -> *mut i32 {
436        Box::into_raw(Box::new(val))
437    }
438
439    unsafe fn reclaim_resource(ptr: *mut i32) {
440        if !ptr.is_null() {
441            let _ = Box::from_raw(ptr);
442        }
443    }
444
445    #[test]
446    fn test_drop_calls_cleanup_non_borrowed_no_cleanup_struct() {
447        let flag = Arc::new(AtomicBool::new(false));
448        let flag_clone = flag.clone();
449        let resource_ptr = make_resource(10);
450
451        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
452            flag_clone.store(true, Ordering::SeqCst);
453            // Set the resource to null to simulate cleanup.
454            unsafe {
455                reclaim_resource(*res);
456                *res = std::ptr::null_mut();
457            }
458            0
459        }) as Box<dyn FnMut(*mut *mut i32) -> i32>);
460
461        {
462            let _resource = ManagedCResource::new(
463                |res: *mut *mut i32| {
464                    unsafe {
465                        *res = resource_ptr;
466                    }
467                    0
468                },
469                cleanup,
470                false,
471                None,
472            );
473            assert!(_resource.is_ok())
474        }
475        assert!(flag.load(Ordering::SeqCst));
476    }
477
478    #[test]
479    fn test_drop_calls_cleanup_non_borrowed_with_cleanup_struct() {
480        let flag = Arc::new(AtomicBool::new(false));
481        let flag_clone = flag.clone();
482        let resource_ptr = make_resource(20);
483
484        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
485            flag_clone.store(true, Ordering::SeqCst);
486            unsafe {
487                *res = std::ptr::null_mut();
488            }
489            0
490        }) as Box<dyn FnMut(*mut *mut i32) -> i32>);
491
492        {
493            let _resource = ManagedCResource::new(
494                |res: *mut *mut i32| {
495                    unsafe {
496                        *res = resource_ptr;
497                    }
498                    0
499                },
500                cleanup,
501                true,
502                None,
503            );
504            assert!(_resource.is_ok())
505        }
506        assert!(flag.load(Ordering::SeqCst));
507    }
508
509    #[test]
510    fn test_drop_does_not_call_cleanup_if_already_closed() {
511        let flag = Arc::new(AtomicBool::new(false));
512        let flag_clone = flag.clone();
513        let resource_ptr = make_resource(30);
514
515        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
516            flag_clone.store(true, Ordering::SeqCst);
517            unsafe {
518                reclaim_resource(*res);
519                *res = std::ptr::null_mut();
520            }
521            0
522        }) as Box<dyn FnMut(*mut *mut i32) -> i32>);
523
524        let mut resource = ManagedCResource::new(
525            |res: *mut *mut i32| {
526                unsafe {
527                    *res = resource_ptr;
528                }
529                0
530            },
531            cleanup,
532            false,
533            None,
534        );
535        assert!(resource.is_ok());
536
537        if let Ok(ref mut resource) = &mut resource {
538            assert!(resource.close().is_ok())
539        }
540
541        // Reset the flag to ensure drop does not call cleanup a second time.
542        flag.store(false, Ordering::SeqCst);
543        drop(resource);
544        assert!(!flag.load(Ordering::SeqCst));
545    }
546
547    #[test]
548    fn test_drop_does_not_call_cleanup_if_check_for_is_closed_returns_true() {
549        let flag = Arc::new(AtomicBool::new(false));
550        let flag_clone = flag.clone();
551        let resource_ptr = make_resource(60);
552
553        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
554            flag_clone.store(true, Ordering::SeqCst);
555            unsafe {
556                reclaim_resource(*res);
557                *res = std::ptr::null_mut();
558            }
559            0
560        }) as Box<dyn FnMut(*mut *mut i32) -> i32>);
561
562        let check_fn = Some(|_res: *mut i32| -> bool { true } as fn(_) -> bool);
563
564        {
565            let _resource = ManagedCResource::new(
566                |res: *mut *mut i32| {
567                    unsafe {
568                        *res = resource_ptr;
569                    }
570                    0
571                },
572                cleanup,
573                false,
574                check_fn,
575            );
576            assert!(_resource.is_ok());
577        }
578        assert!(!flag.load(Ordering::SeqCst));
579        unsafe {
580            reclaim_resource(resource_ptr);
581        }
582    }
583
584    #[test]
585    fn test_drop_does_call_cleanup_if_check_for_is_closed_returns_false() {
586        let flag = Arc::new(AtomicBool::new(false));
587        let flag_clone = flag.clone();
588        let resource_ptr = make_resource(60);
589
590        let cleanup = Some(Box::new(move |res: *mut *mut i32| -> i32 {
591            flag_clone.store(true, Ordering::SeqCst);
592            unsafe {
593                reclaim_resource(*res);
594                *res = std::ptr::null_mut();
595            }
596            0
597        }) as Box<dyn FnMut(*mut *mut i32) -> i32>);
598
599        let check_fn = Some(|_res: *mut i32| -> bool { false } as fn(*mut i32) -> bool);
600
601        {
602            let _resource = ManagedCResource::new(
603                |res: *mut *mut i32| {
604                    unsafe {
605                        *res = resource_ptr;
606                    }
607                    0
608                },
609                cleanup,
610                false,
611                check_fn,
612            );
613            assert!(_resource.is_ok())
614        }
615        assert!(flag.load(Ordering::SeqCst));
616    }
617}