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 !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"), }
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 fn running_under_valgrind() -> bool {
106 std::env::var_os("RUSTERON_VALGRIND").is_some()
107 }
108
109 #[test]
110 #[cfg(not(target_os = "windows"))] 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 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"))] 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 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"))] 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 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 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 let bindings_copy = bindings.clone();
297 for handler in bindings.handlers.iter_mut() {
298 let _ = crate::generate_handlers(handler, &bindings_copy);
299 }
300
301 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, 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 let norm = |s: &str| s.trim().to_string();
334 let (generated, committed) = (norm(&generated), norm(&committed));
335 if generated != committed {
336 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 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 #[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 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 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}