Skip to content

ctest: Add roundtrip test. #4560

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

Merged
merged 1 commit into from
Aug 1, 2025
Merged
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
2 changes: 1 addition & 1 deletion ctest-next/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub struct TestGenerator {
pub(crate) volatile_items: Vec<VolatileItem>,
array_arg: Option<ArrayArg>,
skip_private: bool,
skip_roundtrip: Option<SkipTest>,
pub(crate) skip_roundtrip: Option<SkipTest>,
pub(crate) skip_signededness: Option<SkipTest>,
}

Expand Down
64 changes: 64 additions & 0 deletions ctest-next/src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl CTestTemplate {
pub(crate) struct TestTemplate {
pub field_ptr_tests: Vec<TestFieldPtr>,
pub field_size_offset_tests: Vec<TestFieldSizeOffset>,
pub roundtrip_tests: Vec<TestRoundtrip>,
pub signededness_tests: Vec<TestSignededness>,
pub size_align_tests: Vec<TestSizeAlign>,
pub const_cstr_tests: Vec<TestCStr>,
Expand All @@ -76,6 +77,7 @@ impl TestTemplate {
template.populate_signededness_tests(&helper)?;
template.populate_field_size_offset_tests(&helper)?;
template.populate_field_ptr_tests(&helper)?;
template.populate_roundtrip_tests(&helper)?;

Ok(template)
}
Expand Down Expand Up @@ -245,6 +247,55 @@ impl TestTemplate {
Ok(())
}

/// Populates roundtrip tests for aliases/structs/unions.
///
/// It also keeps track of the names of each test.
fn populate_roundtrip_tests(
&mut self,
helper: &TranslateHelper,
) -> Result<(), TranslationError> {
for alias in helper.ffi_items.aliases() {
let c_ty = helper.c_type(alias)?;
self.add_roundtrip_test(helper, alias.ident(), &[], &c_ty, true);
}
for struct_ in helper.ffi_items.structs() {
let c_ty = helper.c_type(struct_)?;
self.add_roundtrip_test(helper, struct_.ident(), &struct_.fields, &c_ty, false);
}
for union_ in helper.ffi_items.unions() {
let c_ty = helper.c_type(union_)?;
self.add_roundtrip_test(helper, union_.ident(), &union_.fields, &c_ty, false);
}

Ok(())
}

fn add_roundtrip_test(
&mut self,
helper: &TranslateHelper,
ident: &str,
fields: &[Field],
c_ty: &str,
is_alias: bool,
) {
let should_skip_roundtrip_test = helper
.generator
.skip_roundtrip
.as_ref()
.is_some_and(|skip| skip(ident));
if !should_skip_roundtrip_test {
let item = TestRoundtrip {
test_name: roundtrip_test_ident(ident),
id: ident.into(),
fields: fields.iter().filter(|f| f.public).cloned().collect(),
c_ty: c_ty.into(),
is_alias,
};
self.roundtrip_tests.push(item.clone());
self.test_idents.push(item.test_name);
}
}

/// Populates field tests for structs/unions.
///
/// It also keeps track of the names of each test.
Expand Down Expand Up @@ -393,6 +444,15 @@ pub(crate) struct TestFieldSizeOffset {
pub c_ty: BoxStr,
}

#[derive(Clone, Debug)]
pub(crate) struct TestRoundtrip {
pub test_name: BoxStr,
pub id: BoxStr,
pub fields: Vec<Field>,
pub c_ty: BoxStr,
pub is_alias: bool,
}

fn signededness_test_ident(ident: &str) -> BoxStr {
format!("ctest_signededness_{ident}").into()
}
Expand All @@ -417,6 +477,10 @@ fn field_size_offset_test_ident(ident: &str, field_ident: &str) -> BoxStr {
format!("ctest_field_size_offset_{ident}_{field_ident}").into()
}

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

/// Wrap methods that depend on both ffi items and the generator.
pub(crate) struct TranslateHelper<'a> {
ffi_items: &'a FfiItems,
Expand Down
40 changes: 40 additions & 0 deletions ctest-next/templates/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,43 @@ ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}({{ item.c_ty }} *b) {
return &b->{{ item.c_field }};
}
{%- endfor +%}

#ifdef _MSC_VER
// Disable signed/unsigned conversion warnings on MSVC.
// These trigger even if the conversion is explicit.
# pragma warning(disable:4365)
#endif
{%- for item in ctx.roundtrip_tests +%}

// Tests whether the struct/union/alias `x` when passed by value to C and back to Rust
// remains unchanged.
// It checks if the size is the same as well as if the padding bytes are all in the correct place.
{{ item.c_ty }} ctest_roundtrip__{{ item.id }}(
{{ item.c_ty }} value,
const uint8_t is_padding_byte[sizeof({{ item.c_ty }})],
uint8_t value_bytes[sizeof({{ item.c_ty }})]
) {
int size = (int)sizeof({{ item.c_ty }});
// Mark `p` as volatile so that the C compiler does not optimize away the pattern we create.
// Otherwise the Rust side would not be able to see it.
volatile uint8_t* p = (volatile uint8_t*)&value;
int i = 0;
for (i = 0; i < size; ++i) {
// We skip padding bytes in both Rust and C because writing to it is undefined.
// Instead we just make sure the the placement of the padding bytes remains the same.
if (is_padding_byte[i]) { continue; }
value_bytes[i] = p[i];
// After we check that the pattern remained unchanged from Rust to C, we invert the pattern
// and send it back to Rust to make sure that it remains unchanged from C to Rust.
uint8_t d = (uint8_t)(255) - (uint8_t)(i % 256);
d = d == 0 ? 42: d;
p[i] = d;
}
return value;
}

{%- endfor +%}

#ifdef _MSC_VER
# pragma warning(default:4365)
#endif
126 changes: 125 additions & 1 deletion ctest-next/templates/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
mod generated_tests {
#![allow(non_snake_case)]
#![deny(improper_ctypes_definitions)]
use std::ffi::{CStr, c_char};
#[allow(unused_imports)]
use std::ffi::{CStr, c_int, c_char};
use std::fmt::{Debug, LowerHex};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[allow(unused_imports)]
Expand Down Expand Up @@ -189,6 +190,129 @@ mod generated_tests {
check_same(field_ptr.cast(), ctest_field_ptr,
"field type {{ item.field.ident() }} of {{ item.id }}");
}

{%- endfor +%}

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

/// Generates a padding map for a specific type.
///
/// Essentially, it returns a list of bytes, whose length is equal to the size of the type in
/// bytes. Each element corresponds to a byte and has two values. `true` if the byte is padding,
/// and `false` if the byte is not padding.
///
/// For aliases we assume that there are no padding bytes, for structs and unions,
/// if there are no fields, then everything is padding, if there are fields, then we have to
/// go through each field and figure out the padding.
fn roundtrip_padding__{{ item.id }}() -> Vec<bool> {
if {{ item.fields.len() }} == 0 {
// FIXME(ctest): What if it's an alias to a struct/union?
return vec![!{{ item.is_alias }}; size_of::<{{ item.id }}>()]
}

// If there are no fields, v and bar become unused.
#[allow(unused_mut)]
let mut v = Vec::<(usize, usize)>::new();
#[allow(unused_variables)]
let bar = MaybeUninit::<{{ item.id }}>::zeroed();
#[allow(unused_variables)]
let bar = bar.as_ptr();
{%- for field in item.fields +%}

let ty_ptr = unsafe { &raw const ((*bar).{{ field.ident() }}) };
let val = unsafe { ty_ptr.read_unaligned() };

let size = size_of_val(&val);
let off = offset_of!({{ item.id }}, {{ field.ident() }});
v.push((off, size));
{%- endfor +%}
// This vector contains `true` if the byte is padding and `false` if the byte is not
// padding. Initialize all bytes as:
// - padding if we have fields, this means that only the fields will be checked
// - no-padding if we have a type alias: if this causes problems the type alias should
// be skipped
let mut is_padding_byte = vec![true; size_of::<{{ item.id }}>()];
for (off, size) in &v {
for i in 0..*size {
is_padding_byte[off + i] = false;
}
}
is_padding_byte
}

/// Tests whether a type alias when passed to C and back to Rust remains unchanged.
///
/// It checks if the size is the same as well as if the padding bytes are all in the
/// correct place. For this test to be sound, `T` must be valid for any bitpattern.
pub fn {{ item.test_name }}() {
type U = {{ item.id }};
extern "C" {
fn ctest_size_of__{{ item.id }}() -> u64;
fn ctest_roundtrip__{{ item.id }}(
input: MaybeUninit<U>, is_padding_byte: *const bool, value_bytes: *mut u8
) -> U;
}

const SIZE: usize = size_of::<U>();

let is_padding_byte = roundtrip_padding__{{ item.id }}();
let mut expected = vec![0u8; SIZE];
let mut input = MaybeUninit::<U>::zeroed();

let input_ptr = input.as_mut_ptr().cast::<u8>();

// Fill the unitialized memory with a deterministic pattern.
// From Rust to C: every byte will be labelled from 1 to 255, with 0 turning into 42.
// From C to Rust: every byte will be inverted from before (254 -> 1), but 0 is still 42.
for i in 0..SIZE {
let c: u8 = (i % 256) as u8;
let c = if c == 0 { 42 } else { c };
let d: u8 = 255_u8 - (i % 256) as u8;
let d = if d == 0 { 42 } else { d };
unsafe {
input_ptr.add(i).write_volatile(c);
expected[i] = d;
}
}

let c_size = unsafe { ctest_size_of__{{ item.id }}() } as usize;
if SIZE != c_size {
FAILED.store(true, Ordering::Relaxed);
eprintln!(
"size of {{ item.c_ty }} is {c_size} in C and {SIZE} in Rust\n",
);
return;
}

let mut c_value_bytes = vec![0; size_of::<{{ item.id }}>()];
let r: U = unsafe {
ctest_roundtrip__{{ item.id }}(input, is_padding_byte.as_ptr(), c_value_bytes.as_mut_ptr())
};

// Check that the value bytes as read from C match the byte we sent from Rust.
for (i, is_padding_byte) in is_padding_byte.iter().enumerate() {
if *is_padding_byte { continue; }
let rust = unsafe { *input_ptr.add(i) };
let c = c_value_bytes[i];
if rust != c {
eprintln!("rust[{}] = {} != {} (C): Rust \"{{ item.id }}\" -> C", i, rust, c);
FAILED.store(true, Ordering::Relaxed);
}
}

// Check that value returned from C contains the bytes we expect.
for (i, is_padding_byte) in is_padding_byte.iter().enumerate() {
if *is_padding_byte { continue; }
let rust = expected[i] as usize;
let c = unsafe { (&raw const r).cast::<u8>().add(i).read_volatile() as usize };
if rust != c {
eprintln!(
"rust [{i}] = {rust} != {c} (C): C \"{{ item.id }}\" -> Rust",
);
FAILED.store(true, Ordering::Relaxed);
}
}
}
{%- endfor +%}
}

Expand Down
37 changes: 37 additions & 0 deletions ctest-next/tests/input/hierarchy.out.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,40 @@ uint32_t ctest_signededness_of__in6_addr(void) {
in6_addr all_ones = (in6_addr) -1;
return all_ones < 0;
}

#ifdef _MSC_VER
// Disable signed/unsigned conversion warnings on MSVC.
// These trigger even if the conversion is explicit.
# pragma warning(disable:4365)
#endif

// Tests whether the struct/union/alias `x` when passed by value to C and back to Rust
// remains unchanged.
// It checks if the size is the same as well as if the padding bytes are all in the correct place.
in6_addr ctest_roundtrip__in6_addr(
in6_addr value,
const uint8_t is_padding_byte[sizeof(in6_addr)],
uint8_t value_bytes[sizeof(in6_addr)]
) {
int size = (int)sizeof(in6_addr);
// Mark `p` as volatile so that the C compiler does not optimize away the pattern we create.
// Otherwise the Rust side would not be able to see it.
volatile uint8_t* p = (volatile uint8_t*)&value;
int i = 0;
for (i = 0; i < size; ++i) {
// We skip padding bytes in both Rust and C because writing to it is undefined.
// Instead we just make sure the the placement of the padding bytes remains the same.
if (is_padding_byte[i]) { continue; }
value_bytes[i] = p[i];
// After we check that the pattern remained unchanged from Rust to C, we invert the pattern
// and send it back to Rust to make sure that it remains unchanged from C to Rust.
uint8_t d = (uint8_t)(255) - (uint8_t)(i % 256);
d = d == 0 ? 42: d;
p[i] = d;
}
return value;
}

#ifdef _MSC_VER
# pragma warning(default:4365)
#endif
Loading