From b1f824e8f3af4b4477e8f18bd49878c29b0b8db7 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sun, 19 May 2024 01:31:20 -0400 Subject: [PATCH] Include helpers in bindgen-tests crate Put test generation behind a feature so we don't have to compile all that for the 3ds target, just the host as a build depdendency. --- bindgen-tests/Cargo.toml | 20 ++- bindgen-tests/src/lib.rs | 288 +++++++++------------------------- bindgen-tests/src/test_gen.rs | 226 ++++++++++++++++++++++++++ ctru-sys/Cargo.toml | 3 +- ctru-sys/src/lib.rs | 5 - ctru-sys/tests/layout_test.rs | 90 +---------- test-runner/Cargo.toml | 1 + test-runner/src/lib.rs | 1 + 8 files changed, 324 insertions(+), 310 deletions(-) create mode 100644 bindgen-tests/src/test_gen.rs diff --git a/bindgen-tests/Cargo.toml b/bindgen-tests/Cargo.toml index 3d6e7873..e99c0a74 100644 --- a/bindgen-tests/Cargo.toml +++ b/bindgen-tests/Cargo.toml @@ -3,9 +3,19 @@ name = "bindgen-tests" version = "0.1.0" edition = "2021" +[features] +default = [] +generate = [ + "dep:bindgen", + "dep:proc-macro2", + "dep:quote", + "dep:regex", + "dep:rust-format", +] + [dependencies] -bindgen = { workspace = true, features = ["experimental"] } -proc-macro2 = { version = "1.0.81" } -quote = { version = "1.0.36" } -regex = { version = "1.10.4" } -rust-format = { version = "0.3.4", features = ["token_stream"] } +bindgen = { workspace = true, features = ["experimental"], optional = true } +proc-macro2 = { version = "1.0.81", optional = true } +quote = { version = "1.0.36", optional = true } +regex = { version = "1.10.4", optional = true } +rust-format = { version = "0.3.4", features = ["token_stream"], optional = true } diff --git a/bindgen-tests/src/lib.rs b/bindgen-tests/src/lib.rs index eb907cb0..fc2aa2c5 100644 --- a/bindgen-tests/src/lib.rs +++ b/bindgen-tests/src/lib.rs @@ -1,225 +1,91 @@ -//! This module contains the code to generate layout tests comparing generated -//! Rust bindings to the actual C types defined in libctru. We use [`cpp_build`] -//! to compile helper functions that return the real `sizeof`/`alignof` those types -//! and compare them to the ones generated by `bindgen`. +#[cfg(feature = "generate")] +pub mod test_gen; +#[cfg(feature = "generate")] +pub use test_gen::*; -use std::cell::RefCell; -use std::collections::{BTreeMap, BTreeSet}; -use std::error::Error; -use std::fs::File; -use std::io::Write; -use std::path::Path; -use std::rc::Rc; +pub use std::mem::offset_of; -use bindgen::callbacks::{ - DeriveInfo, DeriveTrait, FieldInfo, ImplementsTrait, ParseCallbacks, TypeKind, -}; -use bindgen::FieldVisibilityKind; -use proc_macro2::TokenStream; -use quote::{format_ident, quote, TokenStreamExt}; -use regex::Regex; -use rust_format::{Formatter, RustFmt}; - -#[derive(Debug)] -pub struct LayoutTestCallbacks(Rc); - -impl LayoutTestCallbacks { - pub fn new() -> (Self, Rc) { - let generator = Rc::new(LayoutTestGenerator::new()); - (Self(Rc::clone(&generator)), generator) - } +pub fn size_of_ret(_f: impl Fn(U) -> T) -> usize { + ::std::mem::size_of::() } -impl ParseCallbacks for LayoutTestCallbacks { - fn header_file(&self, filename: &str) { - self.0.headers.borrow_mut().push(filename.to_string()); - } - - fn add_derives(&self, info: &DeriveInfo<'_>) -> Vec { - if let TypeKind::Union = info.kind { - // layout tests don't handle unions for now, just skip it - println!( - "cargo:warning=Skipping layout tests for union {}", - info.name, - ); - self.0.blocklist_type(info.name); - } - - Vec::new() - } - - fn blocklisted_type_implements_trait( - &self, - name: &str, - _derive_trait: DeriveTrait, - ) -> Option { - self.0.blocklist_type(name); - None - } - - fn field_visibility(&self, info: FieldInfo<'_>) -> Option { - self.0 - .struct_fields - .borrow_mut() - .entry(info.type_name.to_string()) - .or_default() - .insert(info.field_name.to_string()); - - None - } +#[macro_export] +macro_rules! size_of { + ($ty:ident::$field:ident) => {{ + $crate::size_of_ret(|x: $ty| x.$field) + }}; + ($ty:ty) => { + ::std::mem::size_of::<$ty>() + }; + ($expr:expr) => { + ::std::mem::size_of_val(&$expr) + }; } -#[derive(Debug)] -pub struct LayoutTestGenerator { - blocklist: RefCell)>>, - headers: RefCell>, - renames: RefCell>, - struct_fields: RefCell>>, +pub fn align_of_ret(_f: impl Fn(U) -> T) -> usize { + ::std::mem::align_of::() } -impl LayoutTestGenerator { - fn new() -> Self { - Self { - blocklist: RefCell::default(), - headers: RefCell::default(), - renames: RefCell::default(), - struct_fields: RefCell::default(), - } - } - - pub fn blocklist_type(&self, pattern: &str) -> &Self { - self.blocklist - .borrow_mut() - .push((Regex::new(&format!("^({pattern})$")).unwrap(), None)); - self - } - - pub fn blocklist_field(&self, struct_pattern: &str, field_pattern: &str) -> &Self { - self.blocklist.borrow_mut().push(( - Regex::new(&format!("^({struct_pattern})$")).unwrap(), - Some(Regex::new(&format!("^({field_pattern})$")).unwrap()), - )); - self - } - - pub fn rename_field(&self, cpp_name: &str, rust_name: &str) -> &Self { - self.renames - .borrow_mut() - .insert(rust_name.to_string(), cpp_name.to_string()); - self - } - - pub fn generate_layout_tests( - &self, - output_path: impl AsRef, - ) -> Result<(), Box> { - let mut file = File::create(output_path)?; - - // Since quote! tokenizes its input, it would result in invalid C++ for - // the `#include` directives (they would be missing whitespace/newlines), - // so we basically need to drop in the include headers here "manually" by - // writing them into the cpp! macro invocation. - file.write_all("cpp! {{\n".as_bytes())?; - for included_file in self.headers.borrow().iter() { - writeln!(file, " #include \"{included_file}\"")?; - } - file.write_all("}}\n".as_bytes())?; - - let test_tokens = RustFmt::default().format_tokens(self.build_tests())?; - file.write_all(test_tokens.as_bytes())?; - - Ok(()) - } - - fn build_tests(&self) -> TokenStream { - let mut output = TokenStream::new(); - - for struct_name in self.struct_fields.borrow().keys() { - if self - .blocklist - .borrow() - .iter() - .any(|(pat, field)| field.is_none() && pat.is_match(struct_name)) - { - println!("cargo:warning=Skipping layout tests for {struct_name}",); - continue; - } - - output.append_all(self.build_struct_test(struct_name)); - } - - output - } - - fn build_struct_test(&self, struct_name: &str) -> proc_macro2::TokenStream { - let name = format_ident!("{struct_name}"); - - let mut field_tests = Vec::new(); - field_tests.push(build_assert_eq( - "e!(size_of!(#name)), - "e!(sizeof(#name)), - )); - field_tests.push(build_assert_eq( - "e!(align_of!(#name)), - "e!(alignof(#name)), - )); - - let struct_fields = self.struct_fields.borrow(); - if let Some(fields) = struct_fields.get(struct_name) { - for field in fields { - if self - .blocklist - .borrow() - .iter() - .any(|(struct_pat, field_pat)| match field_pat { - Some(field_pat) => { - struct_pat.is_match(struct_name) && field_pat.is_match(field) - } - None => false, - }) - { - println!("cargo:warning=Skipping layout tests for {struct_name}::{field}",); - continue; - } - - let rust_field = format_ident!("{field}"); - let cpp_field = - format_ident!("{}", self.renames.borrow().get(field).unwrap_or(field)); - - field_tests.push(build_assert_eq( - "e!(size_of!(#name::#rust_field)), - "e!(sizeof(#name::#cpp_field)), - )); - - field_tests.push(build_assert_eq( - "e!(align_of!(#name::#rust_field)), - "e!(alignof(#name::#cpp_field)), - )); - - field_tests.push(build_assert_eq( - "e!(offset_of!(#name, #rust_field)), - "e!(offsetof(#name, #cpp_field)), - )); - } - } +#[macro_export] +macro_rules! align_of { + ($ty:ident::$field:ident) => {{ + // This matches the semantics of C++ alignof when it is applied to a struct + // member. Packed structs may under-align fields, so we take the minimum + // of the align of the struct and the type of the field itself. + $crate::align_of_ret(|x: $ty| x.$field).min(align_of!($ty)) + }}; + ($ty:ty) => { + ::std::mem::align_of::<$ty>() + }; + ($expr:expr) => { + ::std::mem::align_of_val(&$expr) + }; +} - quote! { - #[test] - fn #name() { - #(#field_tests);* +#[cfg(test)] +mod tests { + macro_rules! packed_struct { + ($name:ident, $size:literal) => { + #[repr(C, packed($size))] + struct $name { + a: u8, + b: u16, + c: u32, + d: u64, } - } + }; } -} -fn build_assert_eq(rust_lhs: &TokenStream, cpp_rhs: &TokenStream) -> TokenStream { - quote! { - assert_eq!( - #rust_lhs, - cpp!(unsafe [] -> usize as "size_t" { return #cpp_rhs; }), - "{} != {}", - stringify!(#rust_lhs), - stringify!(#cpp_rhs), - ); + packed_struct!(PackedStruct1, 1); + packed_struct!(PackedStruct2, 2); + packed_struct!(PackedStruct4, 4); + packed_struct!(PackedStruct8, 8); + + #[test] + fn align_of_matches_cpp() { + // Expected values are based on C++: https://godbolt.org/z/dPnP7nEse + assert_eq!(align_of!(PackedStruct1), 1); + assert_eq!(align_of!(PackedStruct1::a), 1); + assert_eq!(align_of!(PackedStruct1::b), 1); + assert_eq!(align_of!(PackedStruct1::c), 1); + assert_eq!(align_of!(PackedStruct1::d), 1); + + assert_eq!(align_of!(PackedStruct2), 2); + assert_eq!(align_of!(PackedStruct2::a), 1); + assert_eq!(align_of!(PackedStruct2::b), 2); + assert_eq!(align_of!(PackedStruct2::c), 2); + assert_eq!(align_of!(PackedStruct2::d), 2); + + assert_eq!(align_of!(PackedStruct4), 4); + assert_eq!(align_of!(PackedStruct4::a), 1); + assert_eq!(align_of!(PackedStruct4::b), 2); + assert_eq!(align_of!(PackedStruct4::c), 4); + assert_eq!(align_of!(PackedStruct4::d), 4); + + assert_eq!(align_of!(PackedStruct8), 8); + assert_eq!(align_of!(PackedStruct8::a), 1); + assert_eq!(align_of!(PackedStruct8::b), 2); + assert_eq!(align_of!(PackedStruct8::c), 4); + assert_eq!(align_of!(PackedStruct8::d), 8); } } diff --git a/bindgen-tests/src/test_gen.rs b/bindgen-tests/src/test_gen.rs new file mode 100644 index 00000000..c0c674cc --- /dev/null +++ b/bindgen-tests/src/test_gen.rs @@ -0,0 +1,226 @@ +//! This module contains the code to generate layout tests comparing generated +//! Rust bindings to the actual C types defined in libctru. We use [`cpp_build`] +//! to compile helper functions that return the real `sizeof`/`alignof` those types +//! and compare them to the ones generated by `bindgen`. + +use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet}; +use std::error::Error; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use std::rc::Rc; + +use bindgen::callbacks::{ + DeriveInfo, DeriveTrait, FieldInfo, ImplementsTrait, ParseCallbacks, TypeKind, +}; +use bindgen::FieldVisibilityKind; +use proc_macro2::TokenStream; +use quote::{format_ident, quote, TokenStreamExt}; +use regex::Regex; +use rust_format::{Formatter, RustFmt}; + +#[derive(Debug)] +pub struct LayoutTestCallbacks(Rc); + +impl LayoutTestCallbacks { + pub fn new() -> (Self, Rc) { + let generator = Rc::new(LayoutTestGenerator::new()); + (Self(Rc::clone(&generator)), generator) + } +} + +impl ParseCallbacks for LayoutTestCallbacks { + fn header_file(&self, filename: &str) { + self.0.headers.borrow_mut().push(filename.to_string()); + } + + fn add_derives(&self, info: &DeriveInfo<'_>) -> Vec { + if let TypeKind::Union = info.kind { + // layout tests don't handle unions for now, just skip it + println!( + "cargo:warning=Skipping layout tests for union {}", + info.name, + ); + self.0.blocklist_type(info.name); + } + + Vec::new() + } + + fn blocklisted_type_implements_trait( + &self, + name: &str, + _derive_trait: DeriveTrait, + ) -> Option { + self.0.blocklist_type(name); + None + } + + fn field_visibility(&self, info: FieldInfo<'_>) -> Option { + self.0 + .struct_fields + .borrow_mut() + .entry(info.type_name.to_string()) + .or_default() + .insert(info.field_name.to_string()); + + None + } +} + +#[derive(Debug)] +pub struct LayoutTestGenerator { + blocklist: RefCell)>>, + headers: RefCell>, + renames: RefCell>, + struct_fields: RefCell>>, +} + +impl LayoutTestGenerator { + fn new() -> Self { + Self { + blocklist: RefCell::default(), + headers: RefCell::default(), + renames: RefCell::default(), + struct_fields: RefCell::default(), + } + } + + pub fn blocklist_type(&self, pattern: &str) -> &Self { + self.blocklist + .borrow_mut() + .push((Regex::new(&format!("^({pattern})$")).unwrap(), None)); + self + } + + pub fn blocklist_field(&self, struct_pattern: &str, field_pattern: &str) -> &Self { + self.blocklist.borrow_mut().push(( + Regex::new(&format!("^({struct_pattern})$")).unwrap(), + Some(Regex::new(&format!("^({field_pattern})$")).unwrap()), + )); + self + } + + pub fn rename_field(&self, cpp_name: &str, rust_name: &str) -> &Self { + self.renames + .borrow_mut() + .insert(rust_name.to_string(), cpp_name.to_string()); + self + } + + pub fn generate_layout_tests( + &self, + output_path: impl AsRef, + ) -> Result<(), Box> { + let mut file = File::create(output_path)?; + + // Since quote! tokenizes its input, it would result in invalid C++ for + // the `#include` directives (they would be missing whitespace/newlines), + // so we basically need to drop in the include headers here "manually" by + // writing them into the cpp! macro invocation. + file.write_all("cpp! {{\n".as_bytes())?; + for included_file in self.headers.borrow().iter() { + writeln!(file, " #include \"{included_file}\"")?; + } + file.write_all("}}\n".as_bytes())?; + + let test_tokens = RustFmt::default().format_tokens(self.build_tests())?; + file.write_all(test_tokens.as_bytes())?; + + Ok(()) + } + + fn build_tests(&self) -> TokenStream { + let mut output = TokenStream::new(); + + for struct_name in self.struct_fields.borrow().keys() { + if self + .blocklist + .borrow() + .iter() + .any(|(pat, field)| field.is_none() && pat.is_match(struct_name)) + { + println!("cargo:warning=Skipping layout tests for {struct_name}",); + continue; + } + + output.append_all(self.build_struct_test(struct_name)); + } + + output + } + + fn build_struct_test(&self, struct_name: &str) -> proc_macro2::TokenStream { + let name = format_ident!("{struct_name}"); + + let mut field_tests = Vec::new(); + field_tests.push(build_assert_eq( + "e!(size_of!(#name)), + "e!(sizeof(#name)), + )); + field_tests.push(build_assert_eq( + "e!(align_of!(#name)), + "e!(alignof(#name)), + )); + + let struct_fields = self.struct_fields.borrow(); + if let Some(fields) = struct_fields.get(struct_name) { + for field in fields { + if self + .blocklist + .borrow() + .iter() + .any(|(struct_pat, field_pat)| match field_pat { + Some(field_pat) => { + struct_pat.is_match(struct_name) && field_pat.is_match(field) + } + None => false, + }) + { + println!("cargo:warning=Skipping layout tests for {struct_name}::{field}",); + continue; + } + + let rust_field = format_ident!("{field}"); + let cpp_field = + format_ident!("{}", self.renames.borrow().get(field).unwrap_or(field)); + + field_tests.push(build_assert_eq( + "e!(size_of!(#name::#rust_field)), + "e!(sizeof(#name::#cpp_field)), + )); + + field_tests.push(build_assert_eq( + "e!(align_of!(#name::#rust_field)), + "e!(alignof(#name::#cpp_field)), + )); + + field_tests.push(build_assert_eq( + "e!(offset_of!(#name, #rust_field)), + "e!(offsetof(#name, #cpp_field)), + )); + } + } + + let test_name = format_ident!("layout_test_{struct_name}"); + quote! { + #[test] + fn #test_name() { + #(#field_tests);* + } + } + } +} + +fn build_assert_eq(rust_lhs: &TokenStream, cpp_rhs: &TokenStream) -> TokenStream { + quote! { + assert_eq!( + #rust_lhs, + cpp!(unsafe [] -> usize as "size_t" { return #cpp_rhs; }), + "{} != {}", + stringify!(#rust_lhs), + stringify!(#cpp_rhs), + ); + } +} diff --git a/ctru-sys/Cargo.toml b/ctru-sys/Cargo.toml index d63e1197..4e106353 100644 --- a/ctru-sys/Cargo.toml +++ b/ctru-sys/Cargo.toml @@ -31,7 +31,7 @@ libc = { workspace = true } [build-dependencies] bindgen = { workspace = true, features = ["experimental"] } -bindgen-tests = { git = "https://github.com/rust3ds/ctru-rs.git", optional = true } +bindgen-tests = { git = "https://github.com/rust3ds/ctru-rs.git", optional = true, features = ["generate"] } cc = "1.0" # Use git dependency so we can use https://github.com/mystor/rust-cpp/pull/111 cpp_build = { optional = true, git = "https://github.com/mystor/rust-cpp.git" } @@ -42,6 +42,7 @@ which = "4.4.0" [dev-dependencies] shim-3ds = { workspace = true } test-runner = { workspace = true } +bindgen-tests = { git = "https://github.com/rust3ds/ctru-rs.git" } cpp = "0.5.9" [package.metadata.docs.rs] diff --git a/ctru-sys/src/lib.rs b/ctru-sys/src/lib.rs index 4e8e82f3..446091b2 100644 --- a/ctru-sys/src/lib.rs +++ b/ctru-sys/src/lib.rs @@ -14,11 +14,6 @@ )] #![doc(html_root_url = "https://rust3ds.github.io/ctru-rs/crates")] -// Prevent linking errors from the standard `test` library when running `cargo 3ds test --lib`. -// See https://github.com/rust-lang/rust-analyzer/issues/14167 for why we use `not(rust_analyzer)` -#[cfg(all(test, not(rust_analyzer)))] -extern crate shim_3ds; - pub mod result; pub use result::*; diff --git a/ctru-sys/tests/layout_test.rs b/ctru-sys/tests/layout_test.rs index a92cec4b..17611687 100644 --- a/ctru-sys/tests/layout_test.rs +++ b/ctru-sys/tests/layout_test.rs @@ -7,101 +7,15 @@ //! thinks they should be at bindgen time. #![feature(custom_test_frameworks)] -#![test_runner(test_runner::run_gdb)] - -extern crate shim_3ds; - -use std::mem::offset_of; - -fn size_of_ret(_f: impl Fn(U) -> T) -> usize { - ::std::mem::size_of::() -} - -macro_rules! size_of { - ($ty:ident::$field:ident) => {{ - $crate::size_of_ret(|x: $ty| x.$field) - }}; - ($ty:ty) => { - ::std::mem::size_of::<$ty>() - }; - ($expr:expr) => { - ::std::mem::size_of_val(&$expr) - }; -} - -fn align_of_ret(_f: impl Fn(U) -> T) -> usize { - ::std::mem::align_of::() -} - -macro_rules! align_of { - ($ty:ident::$field:ident) => {{ - // This matches the semantics of C++ alignof when it is applied to a struct - // member. Packed structs may under-align fields, so we take the minimum - // of the align of the struct and the type of the field itself. - $crate::align_of_ret(|x: $ty| x.$field).min(align_of!($ty)) - }}; - ($ty:ty) => { - ::std::mem::align_of::<$ty>() - }; - ($expr:expr) => { - ::std::mem::align_of_val(&$expr) - }; -} +#![test_runner(test_runner::run_console)] #[allow(non_snake_case)] #[allow(non_upper_case_globals)] mod generated_tests { - use super::*; + use bindgen_tests::{align_of, offset_of, size_of}; use cpp::cpp; use ctru_sys::*; include!(concat!(env!("OUT_DIR"), "/generated_layout_test.rs")); } - -mod helper_tests { - macro_rules! packed_struct { - ($name:ident, $size:literal) => { - #[repr(C, packed($size))] - struct $name { - a: u8, - b: u16, - c: u32, - d: u64, - } - }; - } - - packed_struct!(PackedStruct1, 1); - packed_struct!(PackedStruct2, 2); - packed_struct!(PackedStruct4, 4); - packed_struct!(PackedStruct8, 8); - - #[test] - fn align_of_matches_cpp() { - // Expected values are based on C++: https://godbolt.org/z/dPnP7nEse - assert_eq!(align_of!(PackedStruct1), 1); - assert_eq!(align_of!(PackedStruct1::a), 1); - assert_eq!(align_of!(PackedStruct1::b), 1); - assert_eq!(align_of!(PackedStruct1::c), 1); - assert_eq!(align_of!(PackedStruct1::d), 1); - - assert_eq!(align_of!(PackedStruct2), 2); - assert_eq!(align_of!(PackedStruct2::a), 1); - assert_eq!(align_of!(PackedStruct2::b), 2); - assert_eq!(align_of!(PackedStruct2::c), 2); - assert_eq!(align_of!(PackedStruct2::d), 2); - - assert_eq!(align_of!(PackedStruct4), 4); - assert_eq!(align_of!(PackedStruct4::a), 1); - assert_eq!(align_of!(PackedStruct4::b), 2); - assert_eq!(align_of!(PackedStruct4::c), 4); - assert_eq!(align_of!(PackedStruct4::d), 4); - - assert_eq!(align_of!(PackedStruct8), 8); - assert_eq!(align_of!(PackedStruct8::a), 1); - assert_eq!(align_of!(PackedStruct8::b), 2); - assert_eq!(align_of!(PackedStruct8::c), 4); - assert_eq!(align_of!(PackedStruct8::d), 8); - } -} diff --git a/test-runner/Cargo.toml b/test-runner/Cargo.toml index c5d02d24..9cc9aa25 100644 --- a/test-runner/Cargo.toml +++ b/test-runner/Cargo.toml @@ -12,3 +12,4 @@ socket = [] ctru-rs = { git = "https://github.com/rust3ds/ctru-rs" } ctru-sys = { git = "https://github.com/rust3ds/ctru-rs" } libc = { workspace = true } +shim-3ds = { git = "https://github.com/rust3ds/shim-3ds.git" } diff --git a/test-runner/src/lib.rs b/test-runner/src/lib.rs index b4a3fcfe..cf642fd9 100644 --- a/test-runner/src/lib.rs +++ b/test-runner/src/lib.rs @@ -9,6 +9,7 @@ #![feature(exitcode_exit_method)] #![test_runner(run_gdb)] +extern crate shim_3ds; extern crate test; mod console;