Skip to content

Commit aa99092

Browse files
mbyxtgross35
authored andcommitted
ctest: add roundtrip test
1 parent cf82fdf commit aa99092

File tree

12 files changed

+1618
-6
lines changed

12 files changed

+1618
-6
lines changed

ctest-next/src/generator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub struct TestGenerator {
4343
pub(crate) volatile_items: Vec<VolatileItem>,
4444
array_arg: Option<ArrayArg>,
4545
skip_private: bool,
46-
skip_roundtrip: Option<SkipTest>,
46+
pub(crate) skip_roundtrip: Option<SkipTest>,
4747
pub(crate) skip_signededness: Option<SkipTest>,
4848
}
4949

ctest-next/src/template.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ impl CTestTemplate {
5151
pub(crate) struct TestTemplate {
5252
pub field_ptr_tests: Vec<TestFieldPtr>,
5353
pub field_size_offset_tests: Vec<TestFieldSizeOffset>,
54+
pub roundtrip_tests: Vec<TestRoundtrip>,
5455
pub signededness_tests: Vec<TestSignededness>,
5556
pub size_align_tests: Vec<TestSizeAlign>,
5657
pub const_cstr_tests: Vec<TestCStr>,
@@ -76,6 +77,7 @@ impl TestTemplate {
7677
template.populate_signededness_tests(&helper)?;
7778
template.populate_field_size_offset_tests(&helper)?;
7879
template.populate_field_ptr_tests(&helper)?;
80+
template.populate_roundtrip_tests(&helper)?;
7981

8082
Ok(template)
8183
}
@@ -245,6 +247,55 @@ impl TestTemplate {
245247
Ok(())
246248
}
247249

250+
/// Populates roundtrip tests for aliases/structs/unions.
251+
///
252+
/// It also keeps track of the names of each test.
253+
fn populate_roundtrip_tests(
254+
&mut self,
255+
helper: &TranslateHelper,
256+
) -> Result<(), TranslationError> {
257+
for alias in helper.ffi_items.aliases() {
258+
let c_ty = helper.c_type(alias)?;
259+
self.add_roundtrip_test(helper, alias.ident(), &[], &c_ty, true);
260+
}
261+
for struct_ in helper.ffi_items.structs() {
262+
let c_ty = helper.c_type(struct_)?;
263+
self.add_roundtrip_test(helper, struct_.ident(), &struct_.fields, &c_ty, false);
264+
}
265+
for union_ in helper.ffi_items.unions() {
266+
let c_ty = helper.c_type(union_)?;
267+
self.add_roundtrip_test(helper, union_.ident(), &union_.fields, &c_ty, false);
268+
}
269+
270+
Ok(())
271+
}
272+
273+
fn add_roundtrip_test(
274+
&mut self,
275+
helper: &TranslateHelper,
276+
ident: &str,
277+
fields: &[Field],
278+
c_ty: &str,
279+
is_alias: bool,
280+
) {
281+
let should_skip_roundtrip_test = helper
282+
.generator
283+
.skip_roundtrip
284+
.as_ref()
285+
.is_some_and(|skip| skip(ident));
286+
if !should_skip_roundtrip_test {
287+
let item = TestRoundtrip {
288+
test_name: roundtrip_test_ident(ident),
289+
id: ident.into(),
290+
fields: fields.iter().filter(|f| f.public).cloned().collect(),
291+
c_ty: c_ty.into(),
292+
is_alias,
293+
};
294+
self.roundtrip_tests.push(item.clone());
295+
self.test_idents.push(item.test_name);
296+
}
297+
}
298+
248299
/// Populates field tests for structs/unions.
249300
///
250301
/// It also keeps track of the names of each test.
@@ -393,6 +444,15 @@ pub(crate) struct TestFieldSizeOffset {
393444
pub c_ty: BoxStr,
394445
}
395446

447+
#[derive(Clone, Debug)]
448+
pub(crate) struct TestRoundtrip {
449+
pub test_name: BoxStr,
450+
pub id: BoxStr,
451+
pub fields: Vec<Field>,
452+
pub c_ty: BoxStr,
453+
pub is_alias: bool,
454+
}
455+
396456
fn signededness_test_ident(ident: &str) -> BoxStr {
397457
format!("ctest_signededness_{ident}").into()
398458
}
@@ -417,6 +477,10 @@ fn field_size_offset_test_ident(ident: &str, field_ident: &str) -> BoxStr {
417477
format!("ctest_field_size_offset_{ident}_{field_ident}").into()
418478
}
419479

480+
fn roundtrip_test_ident(ident: &str) -> BoxStr {
481+
format!("ctest_roundtrip_{ident}").into()
482+
}
483+
420484
/// Wrap methods that depend on both ffi items and the generator.
421485
pub(crate) struct TranslateHelper<'a> {
422486
ffi_items: &'a FfiItems,

ctest-next/templates/test.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,43 @@ ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}({{ item.c_ty }} *b) {
7878
return &b->{{ item.c_field }};
7979
}
8080
{%- endfor +%}
81+
82+
#ifdef _MSC_VER
83+
// Disable signed/unsigned conversion warnings on MSVC.
84+
// These trigger even if the conversion is explicit.
85+
# pragma warning(disable:4365)
86+
#endif
87+
{%- for item in ctx.roundtrip_tests +%}
88+
89+
// Tests whether the struct/union/alias `x` when passed by value to C and back to Rust
90+
// remains unchanged.
91+
// It checks if the size is the same as well as if the padding bytes are all in the correct place.
92+
{{ item.c_ty }} ctest_roundtrip__{{ item.id }}(
93+
{{ item.c_ty }} value,
94+
const uint8_t is_padding_byte[sizeof({{ item.c_ty }})],
95+
uint8_t value_bytes[sizeof({{ item.c_ty }})]
96+
) {
97+
int size = (int)sizeof({{ item.c_ty }});
98+
// Mark `p` as volatile so that the C compiler does not optimize away the pattern we create.
99+
// Otherwise the Rust side would not be able to see it.
100+
volatile uint8_t* p = (volatile uint8_t*)&value;
101+
int i = 0;
102+
for (i = 0; i < size; ++i) {
103+
// We skip padding bytes in both Rust and C because writing to it is undefined.
104+
// Instead we just make sure the the placement of the padding bytes remains the same.
105+
if (is_padding_byte[i]) { continue; }
106+
value_bytes[i] = p[i];
107+
// After we check that the pattern remained unchanged from Rust to C, we invert the pattern
108+
// and send it back to Rust to make sure that it remains unchanged from C to Rust.
109+
uint8_t d = (uint8_t)(255) - (uint8_t)(i % 256);
110+
d = d == 0 ? 42: d;
111+
p[i] = d;
112+
}
113+
return value;
114+
}
115+
116+
{%- endfor +%}
117+
118+
#ifdef _MSC_VER
119+
# pragma warning(default:4365)
120+
#endif

ctest-next/templates/test.rs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
mod generated_tests {
1010
#![allow(non_snake_case)]
1111
#![deny(improper_ctypes_definitions)]
12-
use std::ffi::{CStr, c_char};
12+
#[allow(unused_imports)]
13+
use std::ffi::{CStr, c_int, c_char};
1314
use std::fmt::{Debug, LowerHex};
1415
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1516
#[allow(unused_imports)]
@@ -189,6 +190,129 @@ mod generated_tests {
189190
check_same(field_ptr.cast(), ctest_field_ptr,
190191
"field type {{ item.field.ident() }} of {{ item.id }}");
191192
}
193+
194+
{%- endfor +%}
195+
196+
{%- for item in ctx.roundtrip_tests +%}
197+
198+
/// Generates a padding map for a specific type.
199+
///
200+
/// Essentially, it returns a list of bytes, whose length is equal to the size of the type in
201+
/// bytes. Each element corresponds to a byte and has two values. `true` if the byte is padding,
202+
/// and `false` if the byte is not padding.
203+
///
204+
/// For aliases we assume that there are no padding bytes, for structs and unions,
205+
/// if there are no fields, then everything is padding, if there are fields, then we have to
206+
/// go through each field and figure out the padding.
207+
fn roundtrip_padding__{{ item.id }}() -> Vec<bool> {
208+
if {{ item.fields.len() }} == 0 {
209+
// FIXME(ctest): What if it's an alias to a struct/union?
210+
return vec![!{{ item.is_alias }}; size_of::<{{ item.id }}>()]
211+
}
212+
213+
// If there are no fields, v and bar become unused.
214+
#[allow(unused_mut)]
215+
let mut v = Vec::<(usize, usize)>::new();
216+
#[allow(unused_variables)]
217+
let bar = MaybeUninit::<{{ item.id }}>::zeroed();
218+
#[allow(unused_variables)]
219+
let bar = bar.as_ptr();
220+
{%- for field in item.fields +%}
221+
222+
let ty_ptr = unsafe { &raw const ((*bar).{{ field.ident() }}) };
223+
let val = unsafe { ty_ptr.read_unaligned() };
224+
225+
let size = size_of_val(&val);
226+
let off = offset_of!({{ item.id }}, {{ field.ident() }});
227+
v.push((off, size));
228+
{%- endfor +%}
229+
// This vector contains `true` if the byte is padding and `false` if the byte is not
230+
// padding. Initialize all bytes as:
231+
// - padding if we have fields, this means that only the fields will be checked
232+
// - no-padding if we have a type alias: if this causes problems the type alias should
233+
// be skipped
234+
let mut is_padding_byte = vec![true; size_of::<{{ item.id }}>()];
235+
for (off, size) in &v {
236+
for i in 0..*size {
237+
is_padding_byte[off + i] = false;
238+
}
239+
}
240+
is_padding_byte
241+
}
242+
243+
/// Tests whether a type alias when passed to C and back to Rust remains unchanged.
244+
///
245+
/// It checks if the size is the same as well as if the padding bytes are all in the
246+
/// correct place. For this test to be sound, `T` must be valid for any bitpattern.
247+
pub fn {{ item.test_name }}() {
248+
type U = {{ item.id }};
249+
extern "C" {
250+
fn ctest_size_of__{{ item.id }}() -> u64;
251+
fn ctest_roundtrip__{{ item.id }}(
252+
input: MaybeUninit<U>, is_padding_byte: *const bool, value_bytes: *mut u8
253+
) -> U;
254+
}
255+
256+
const SIZE: usize = size_of::<U>();
257+
258+
let is_padding_byte = roundtrip_padding__{{ item.id }}();
259+
let mut expected = vec![0u8; SIZE];
260+
let mut input = MaybeUninit::<U>::zeroed();
261+
262+
let input_ptr = input.as_mut_ptr().cast::<u8>();
263+
264+
// Fill the unitialized memory with a deterministic pattern.
265+
// From Rust to C: every byte will be labelled from 1 to 255, with 0 turning into 42.
266+
// From C to Rust: every byte will be inverted from before (254 -> 1), but 0 is still 42.
267+
for i in 0..SIZE {
268+
let c: u8 = (i % 256) as u8;
269+
let c = if c == 0 { 42 } else { c };
270+
let d: u8 = 255_u8 - (i % 256) as u8;
271+
let d = if d == 0 { 42 } else { d };
272+
unsafe {
273+
input_ptr.add(i).write_volatile(c);
274+
expected[i] = d;
275+
}
276+
}
277+
278+
let c_size = unsafe { ctest_size_of__{{ item.id }}() } as usize;
279+
if SIZE != c_size {
280+
FAILED.store(true, Ordering::Relaxed);
281+
eprintln!(
282+
"size of {{ item.c_ty }} is {c_size} in C and {SIZE} in Rust\n",
283+
);
284+
return;
285+
}
286+
287+
let mut c_value_bytes = vec![0; size_of::<{{ item.id }}>()];
288+
let r: U = unsafe {
289+
ctest_roundtrip__{{ item.id }}(input, is_padding_byte.as_ptr(), c_value_bytes.as_mut_ptr())
290+
};
291+
292+
// Check that the value bytes as read from C match the byte we sent from Rust.
293+
for (i, is_padding_byte) in is_padding_byte.iter().enumerate() {
294+
if *is_padding_byte { continue; }
295+
let rust = unsafe { *input_ptr.add(i) };
296+
let c = c_value_bytes[i];
297+
if rust != c {
298+
eprintln!("rust[{}] = {} != {} (C): Rust \"{{ item.id }}\" -> C", i, rust, c);
299+
FAILED.store(true, Ordering::Relaxed);
300+
}
301+
}
302+
303+
// Check that value returned from C contains the bytes we expect.
304+
for (i, is_padding_byte) in is_padding_byte.iter().enumerate() {
305+
if *is_padding_byte { continue; }
306+
let rust = expected[i] as usize;
307+
let c = unsafe { (&raw const r).cast::<u8>().add(i).read_volatile() as usize };
308+
if rust != c {
309+
eprintln!(
310+
"rust [{i}] = {rust} != {c} (C): C \"{{ item.id }}\" -> Rust",
311+
);
312+
FAILED.store(true, Ordering::Relaxed);
313+
}
314+
}
315+
}
192316
{%- endfor +%}
193317
}
194318

ctest-next/tests/input/hierarchy.out.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,40 @@ uint32_t ctest_signededness_of__in6_addr(void) {
2727
in6_addr all_ones = (in6_addr) -1;
2828
return all_ones < 0;
2929
}
30+
31+
#ifdef _MSC_VER
32+
// Disable signed/unsigned conversion warnings on MSVC.
33+
// These trigger even if the conversion is explicit.
34+
# pragma warning(disable:4365)
35+
#endif
36+
37+
// Tests whether the struct/union/alias `x` when passed by value to C and back to Rust
38+
// remains unchanged.
39+
// It checks if the size is the same as well as if the padding bytes are all in the correct place.
40+
in6_addr ctest_roundtrip__in6_addr(
41+
in6_addr value,
42+
const uint8_t is_padding_byte[sizeof(in6_addr)],
43+
uint8_t value_bytes[sizeof(in6_addr)]
44+
) {
45+
int size = (int)sizeof(in6_addr);
46+
// Mark `p` as volatile so that the C compiler does not optimize away the pattern we create.
47+
// Otherwise the Rust side would not be able to see it.
48+
volatile uint8_t* p = (volatile uint8_t*)&value;
49+
int i = 0;
50+
for (i = 0; i < size; ++i) {
51+
// We skip padding bytes in both Rust and C because writing to it is undefined.
52+
// Instead we just make sure the the placement of the padding bytes remains the same.
53+
if (is_padding_byte[i]) { continue; }
54+
value_bytes[i] = p[i];
55+
// After we check that the pattern remained unchanged from Rust to C, we invert the pattern
56+
// and send it back to Rust to make sure that it remains unchanged from C to Rust.
57+
uint8_t d = (uint8_t)(255) - (uint8_t)(i % 256);
58+
d = d == 0 ? 42: d;
59+
p[i] = d;
60+
}
61+
return value;
62+
}
63+
64+
#ifdef _MSC_VER
65+
# pragma warning(default:4365)
66+
#endif

0 commit comments

Comments
 (0)