Skip to content

Commit

Permalink
Include helpers in bindgen-tests crate
Browse files Browse the repository at this point in the history
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
ian-h-chamberlain committed May 19, 2024
1 parent 3953844 commit b1f824e
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 310 deletions.
20 changes: 15 additions & 5 deletions bindgen-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
288 changes: 77 additions & 211 deletions bindgen-tests/src/lib.rs
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(
&quote!(size_of!(#name)),
&quote!(sizeof(#name)),
));
field_tests.push(build_assert_eq(
&quote!(align_of!(#name)),
&quote!(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(
&quote!(size_of!(#name::#rust_field)),
&quote!(sizeof(#name::#cpp_field)),
));

field_tests.push(build_assert_eq(
&quote!(align_of!(#name::#rust_field)),
&quote!(alignof(#name::#cpp_field)),
));

field_tests.push(build_assert_eq(
&quote!(offset_of!(#name, #rust_field)),
&quote!(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);
}
}
Loading

0 comments on commit b1f824e

Please sign in to comment.