-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
3953844
commit b1f824e
Showing
8 changed files
with
324 additions
and
310 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<LayoutTestGenerator>); | ||
|
||
impl LayoutTestCallbacks { | ||
pub fn new() -> (Self, Rc<LayoutTestGenerator>) { | ||
let generator = Rc::new(LayoutTestGenerator::new()); | ||
(Self(Rc::clone(&generator)), generator) | ||
} | ||
pub fn size_of_ret<T, U>(_f: impl Fn(U) -> T) -> usize { | ||
::std::mem::size_of::<T>() | ||
} | ||
|
||
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<String> { | ||
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<ImplementsTrait> { | ||
self.0.blocklist_type(name); | ||
None | ||
} | ||
|
||
fn field_visibility(&self, info: FieldInfo<'_>) -> Option<FieldVisibilityKind> { | ||
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<Vec<(Regex, Option<Regex>)>>, | ||
headers: RefCell<Vec<String>>, | ||
renames: RefCell<BTreeMap<String, String>>, | ||
struct_fields: RefCell<BTreeMap<String, BTreeSet<String>>>, | ||
pub fn align_of_ret<T, U>(_f: impl Fn(U) -> T) -> usize { | ||
::std::mem::align_of::<T>() | ||
} | ||
|
||
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<Path>, | ||
) -> Result<(), Box<dyn Error>> { | ||
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); | ||
} | ||
} |
Oops, something went wrong.