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