Skip to content

ctest: add tests for size and alignment of structs, unions, and aliases, and signededness of aliases. #4556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion ctest-next/src/ast/type_alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use crate::BoxStr;
pub struct Type {
pub(crate) public: bool,
pub(crate) ident: BoxStr,
#[expect(unused)]
pub(crate) ty: syn::Type,
}

Expand Down
1 change: 0 additions & 1 deletion ctest-next/src/ffi_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ impl FfiItems {
}

/// Return a list of all type aliases found.
#[cfg_attr(not(test), expect(unused))]
pub(crate) fn aliases(&self) -> &Vec<Type> {
&self.aliases
}
Expand Down
23 changes: 23 additions & 0 deletions ctest-next/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub struct TestGenerator {
array_arg: Option<ArrayArg>,
skip_private: bool,
skip_roundtrip: Option<SkipTest>,
pub(crate) skip_signededness: Option<SkipTest>,
}

#[derive(Debug, Error)]
Expand Down Expand Up @@ -832,6 +833,28 @@ impl TestGenerator {
self
}

/// Configures whether a type's signededness is tested or not.
///
/// The closure is given the name of a Rust type, and returns whether the
/// type should be tested as having the right sign (positive or negative).
///
/// By default all signededness checks are performed.
///
/// # Examples
///
/// ```no_run
/// use ctest_next::TestGenerator;
///
/// let mut cfg = TestGenerator::new();
/// cfg.skip_signededness(|s| {
/// s.starts_with("foo_")
/// });
/// ```
pub fn skip_signededness(&mut self, f: impl Fn(&str) -> bool + 'static) -> &mut Self {
self.skip_signededness = Some(Box::new(f));
self
}

/// Generate the Rust and C testing files.
///
/// Returns the path to the generated file.
Expand Down
141 changes: 116 additions & 25 deletions ctest-next/src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ impl CTestTemplate {
/// Stores all information necessary for generation of tests for all items.
#[derive(Clone, Debug, Default)]
pub(crate) struct TestTemplate {
pub signededness_tests: Vec<TestSignededness>,
pub size_align_tests: Vec<TestSizeAlign>,
pub const_cstr_tests: Vec<TestCStr>,
pub const_tests: Vec<TestConst>,
pub test_idents: Vec<BoxStr>,
}

impl TestTemplate {
/// Populate all tests for all items depending on the configuration provided.
pub(crate) fn new(
ffi_items: &FfiItems,
generator: &TestGenerator,
Expand All @@ -62,15 +65,20 @@ impl TestTemplate {
translator: Translator::new(),
};

/* Figure out which tests are to be generated. */
// FIXME(ctest): Populate more test information, maybe extract into separate methods.
// The workflow would be to create a struct that stores information for the new test,
// and populating that struct here, so that the also things that have to be added to
// the test templates are the new tests parameterized by that struct.
let mut template = Self::default();
template.populate_const_and_cstr_tests(&helper)?;
template.populate_size_align_tests(&helper)?;
template.populate_signededness_tests(&helper)?;

let mut const_tests = vec![];
let mut const_cstr_tests = vec![];
for constant in ffi_items.constants() {
Ok(template)
}

/// Populates tests for constants and C-str constants, keeping track of the names of each test.
fn populate_const_and_cstr_tests(
&mut self,
helper: &TranslateHelper,
) -> Result<(), TranslationError> {
for constant in helper.ffi_items.constants() {
if let syn::Type::Ptr(ptr) = &constant.ty
&& let syn::Type::Path(path) = &*ptr.elem
&& path.path.segments.last().unwrap().ident == "c_char"
Expand All @@ -82,29 +90,95 @@ impl TestTemplate {
rust_val: constant.ident().into(),
c_val: helper.c_ident(constant).into(),
};
const_cstr_tests.push(item)
self.const_cstr_tests.push(item.clone());
self.test_idents.push(item.test_name);
} else {
let item = TestConst {
id: constant.ident().into(),
test_name: const_test_ident(constant.ident()),
rust_val: constant.ident.clone(),
rust_val: constant.ident().into(),
rust_ty: constant.ty.to_token_stream().to_string().into_boxed_str(),
c_val: helper.c_ident(constant).into(),
c_ty: helper.c_type(constant)?.into(),
};
const_tests.push(item)
self.const_tests.push(item.clone());
self.test_idents.push(item.test_name);
}
}

let mut test_idents = vec![];
test_idents.extend(const_cstr_tests.iter().map(|test| test.test_name.clone()));
test_idents.extend(const_tests.iter().map(|test| test.test_name.clone()));
Ok(())
}

Ok(Self {
const_cstr_tests,
const_tests,
test_idents,
})
/// Populates size and alignment tests for aliases, structs, and unions.
///
/// It also keeps track of the names of each test.
fn populate_size_align_tests(
&mut self,
helper: &TranslateHelper,
) -> Result<(), TranslationError> {
for alias in helper.ffi_items.aliases() {
let item = TestSizeAlign {
test_name: size_align_test_ident(alias.ident()),
id: alias.ident().into(),
rust_ty: alias.ident().into(),
c_ty: helper.c_type(alias)?.into(),
};
self.size_align_tests.push(item.clone());
self.test_idents.push(item.test_name);
}
for struct_ in helper.ffi_items.structs() {
let item = TestSizeAlign {
test_name: size_align_test_ident(struct_.ident()),
id: struct_.ident().into(),
rust_ty: struct_.ident().into(),
c_ty: helper.c_type(struct_)?.into(),
};
self.size_align_tests.push(item.clone());
self.test_idents.push(item.test_name);
}
for union_ in helper.ffi_items.unions() {
let item = TestSizeAlign {
test_name: size_align_test_ident(union_.ident()),
id: union_.ident().into(),
rust_ty: union_.ident().into(),
c_ty: helper.c_type(union_)?.into(),
};
self.size_align_tests.push(item.clone());
self.test_idents.push(item.test_name);
}

Ok(())
}

/// Populates signededness tests for aliases.
///
/// It also keeps track of the names of each test.
fn populate_signededness_tests(
&mut self,
helper: &TranslateHelper,
) -> Result<(), TranslationError> {
for alias in helper.ffi_items.aliases() {
let should_skip_signededness_test = helper
.generator
.skip_signededness
.as_ref()
.is_some_and(|skip| skip(alias.ident()));

if !helper.translator.is_signed(helper.ffi_items, &alias.ty)
|| should_skip_signededness_test
{
continue;
}
let item = TestSignededness {
test_name: signededness_test_ident(alias.ident()),
id: alias.ident().into(),
c_ty: helper.c_type(alias)?.into(),
};
self.signededness_tests.push(item.clone());
self.test_idents.push(item.test_name);
}

Ok(())
}
}

Expand All @@ -119,6 +193,21 @@ impl TestTemplate {
* - `c_ty`: The C type of the constant, qualified with `struct` or `union` if needed.
*/

#[derive(Clone, Debug)]
pub(crate) struct TestSignededness {
pub test_name: BoxStr,
pub id: BoxStr,
pub c_ty: BoxStr,
}

#[derive(Clone, Debug)]
pub(crate) struct TestSizeAlign {
pub test_name: BoxStr,
pub id: BoxStr,
pub rust_ty: BoxStr,
pub c_ty: BoxStr,
}

/// Information required to test a constant CStr.
#[derive(Clone, Debug)]
pub(crate) struct TestCStr {
Expand All @@ -139,16 +228,18 @@ pub(crate) struct TestConst {
pub c_ty: BoxStr,
}

/// The Rust name of the cstr test.
///
/// The C name of this same test is the same with `__` prepended.
fn signededness_test_ident(ident: &str) -> BoxStr {
format!("ctest_signededness_{ident}").into()
}

fn size_align_test_ident(ident: &str) -> BoxStr {
format!("ctest_size_align_{ident}").into()
}

fn cstr_test_ident(ident: &str) -> BoxStr {
format!("ctest_const_cstr_{ident}").into()
}

/// The Rust name of the const test.
///
/// The C name of this test is the same with `__` prepended.
fn const_test_ident(ident: &str) -> BoxStr {
format!("ctest_const_{ident}").into()
}
Expand Down
26 changes: 26 additions & 0 deletions ctest-next/src/translator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use syn::spanned::Spanned;
use thiserror::Error;

use crate::BoxStr;
use crate::ffi_items::FfiItems;

/// An error that occurs during translation, detailing cause and location.
#[derive(Debug, Error)]
Expand Down Expand Up @@ -314,6 +315,31 @@ impl Translator {
}
}
}

/// Determine whether a C type is a signed type.
///
/// For primitive types it checks against a known list of signed types, but for aliases
/// which are the only thing other than primitives that can be signed, it recursively checks
/// the underlying type of the alias.
pub(crate) fn is_signed(&self, ffi_items: &FfiItems, ty: &syn::Type) -> bool {
match ty {
syn::Type::Path(path) => {
let ident = path.path.segments.last().unwrap().ident.clone();
if let Some(aliased) = ffi_items.aliases().iter().find(|a| ident == a.ident()) {
return self.is_signed(ffi_items, &aliased.ty);
}
match self.translate_primitive_type(&ident).as_str() {
"char" | "short" | "long" | "long long" | "size_t" | "ssize_t" => true,
s => {
s.starts_with("int")
|| s.starts_with("uint") | s.starts_with("signed ")
|| s.starts_with("unsigned ")
}
}
}
_ => false,
}
}
}

/// Translate a simple Rust expression to C.
Expand Down
19 changes: 19 additions & 0 deletions ctest-next/templates/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,22 @@ static {{ constant.c_ty }} ctest_const_{{ constant.id }}_val_static = {{ constan
return &ctest_const_{{ constant.id }}_val_static;
}
{%- endfor +%}

{%- for item in ctx.size_align_tests +%}

// Return the size of a type.
uint64_t ctest_size_of__{{ item.id }}(void) { return sizeof({{ item.c_ty }}); }

// Return the alignment of a type.
uint64_t ctest_align_of__{{ item.id }}(void) { return _Alignof({{ item.c_ty }}); }
{%- endfor +%}

{%- for alias in ctx.signededness_tests +%}

// Return `1` if the type is signed, otherwise return `0`.
// Casting -1 to the aliased type if signed evaluates to `-1 < 0`, if unsigned to `MAX_VALUE < 0`
uint32_t ctest_signededness_of__{{ alias.id }}(void) {
{{ alias.c_ty }} all_ones = ({{ alias.c_ty }}) -1;
return all_ones < 0;
}
{%- endfor +%}
39 changes: 39 additions & 0 deletions ctest-next/templates/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,45 @@ mod generated_tests {
}
}
{%- endfor +%}

{%- for item in ctx.size_align_tests +%}

/// Compare the size and alignment of the type in Rust and C, making sure they are the same.
pub fn {{ item.test_name }}() {
extern "C" {
fn ctest_size_of__{{ item.id }}() -> u64;
fn ctest_align_of__{{ item.id }}() -> u64;
}

let rust_size = size_of::<{{ item.rust_ty }}>() as u64;
let c_size = unsafe { ctest_size_of__{{ item.id }}() };

let rust_align = align_of::<{{ item.rust_ty }}>() as u64;
let c_align = unsafe { ctest_align_of__{{ item.id }}() };

check_same(rust_size, c_size, "{{ item.id }} size");
check_same(rust_align, c_align, "{{ item.id }} align");
}
{%- endfor +%}

{%- for alias in ctx.signededness_tests +%}

/// Make sure that the signededness of a type alias in Rust and C is the same.
///
/// This is done by casting 0 to that type and flipping all of its bits. For unsigned types,
/// this would result in a value larger than zero. For signed types, this results in a value
/// smaller than 0.
pub fn {{ alias.test_name }}() {
extern "C" {
fn ctest_signededness_of__{{ alias.id }}() -> u32;
}
let all_ones = !(0 as {{ alias.id }});
let all_zeros = 0 as {{ alias.id }};
let c_is_signed = unsafe { ctest_signededness_of__{{ alias.id }}() };

check_same((all_ones < all_zeros) as u32, c_is_signed, "{{ alias.id }} signed");
}
{%- endfor +%}
}

use generated_tests::*;
Expand Down
13 changes: 13 additions & 0 deletions ctest-next/tests/input/hierarchy.out.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,16 @@ static bool ctest_const_ON_val_static = ON;
bool *ctest_const__ON(void) {
return &ctest_const_ON_val_static;
}

// Return the size of a type.
uint64_t ctest_size_of__in6_addr(void) { return sizeof(in6_addr); }

// Return the alignment of a type.
uint64_t ctest_align_of__in6_addr(void) { return _Alignof(in6_addr); }

// Return `1` if the type is signed, otherwise return `0`.
// Casting -1 to the aliased type if signed evaluates to `-1 < 0`, if unsigned to `MAX_VALUE < 0`
uint32_t ctest_signededness_of__in6_addr(void) {
in6_addr all_ones = (in6_addr) -1;
return all_ones < 0;
}
Loading
Loading