Skip to content

Commit 3da3f28

Browse files
committed
ctest: add roundtrip test
1 parent b94681f commit 3da3f28

15 files changed

+1530
-10
lines changed

ctest-next/src/ast/field.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use crate::BoxStr;
33
/// Represents a field in a struct or union defined in Rust.
44
#[derive(Debug, Clone)]
55
pub struct Field {
6-
#[expect(unused)]
76
pub(crate) public: bool,
87
pub(crate) ident: BoxStr,
98
pub(crate) ty: syn::Type,

ctest-next/src/ast/structure.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use crate::{BoxStr, Field};
55
pub struct Struct {
66
pub(crate) public: bool,
77
pub(crate) ident: BoxStr,
8-
#[expect(unused)]
98
pub(crate) fields: Vec<Field>,
109
}
1110

ctest-next/src/ast/union.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ pub struct Union {
66
#[expect(unused)]
77
pub(crate) public: bool,
88
pub(crate) ident: BoxStr,
9-
#[expect(unused)]
109
pub(crate) fields: Vec<Field>,
1110
}
1211

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
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: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use quote::ToTokens;
33

44
use crate::ffi_items::FfiItems;
55
use crate::translator::Translator;
6-
use crate::{BoxStr, MapInput, Result, TestGenerator, TranslationError};
6+
use crate::{BoxStr, Field, MapInput, Result, TestGenerator, TranslationError};
77

88
/// Represents the Rust side of the generated testing suite.
99
#[derive(Template, Clone)]
@@ -46,6 +46,7 @@ impl CTestTemplate {
4646
/// Stores all information necessary for generation of tests for all items.
4747
#[derive(Clone, Debug, Default)]
4848
pub(crate) struct TestTemplate {
49+
pub roundtrip_tests: Vec<TestRoundtrip>,
4950
pub signededness_tests: Vec<TestSignededness>,
5051
pub size_align_tests: Vec<TestSizeAlign>,
5152
pub const_cstr_tests: Vec<TestCStr>,
@@ -69,6 +70,7 @@ impl TestTemplate {
6970
template.populate_const_and_cstr_tests(&helper)?;
7071
template.populate_size_align_tests(&helper)?;
7172
template.populate_signededness_tests(&helper)?;
73+
template.populate_roundtrip_tests(&helper)?;
7274

7375
Ok(template)
7476
}
@@ -180,6 +182,55 @@ impl TestTemplate {
180182

181183
Ok(())
182184
}
185+
186+
/// Populates roundtrip tests for aliases/structs/unions.
187+
///
188+
/// It also keeps track of the names of each test.
189+
fn populate_roundtrip_tests(
190+
&mut self,
191+
helper: &TranslateHelper,
192+
) -> Result<(), TranslationError> {
193+
for alias in helper.ffi_items.aliases() {
194+
let c_ty = helper.c_type(alias)?;
195+
self.add_roundtrip_test(helper, alias.ident(), &[], &c_ty, true);
196+
}
197+
for struct_ in helper.ffi_items.structs() {
198+
let c_ty = helper.c_type(struct_)?;
199+
self.add_roundtrip_test(helper, struct_.ident(), &struct_.fields, &c_ty, false);
200+
}
201+
for union_ in helper.ffi_items.unions() {
202+
let c_ty = helper.c_type(union_)?;
203+
self.add_roundtrip_test(helper, union_.ident(), &union_.fields, &c_ty, false);
204+
}
205+
206+
Ok(())
207+
}
208+
209+
fn add_roundtrip_test(
210+
&mut self,
211+
helper: &TranslateHelper,
212+
ident: &str,
213+
fields: &[Field],
214+
c_ty: &str,
215+
is_alias: bool,
216+
) {
217+
let should_skip_roundtrip_test = helper
218+
.generator
219+
.skip_roundtrip
220+
.as_ref()
221+
.is_some_and(|skip| skip(ident));
222+
if !should_skip_roundtrip_test {
223+
let item = TestRoundtrip {
224+
test_name: roundtrip_test_ident(ident),
225+
id: ident.into(),
226+
fields: fields.iter().filter(|f| f.public).cloned().collect(),
227+
c_ty: c_ty.into(),
228+
is_alias,
229+
};
230+
self.roundtrip_tests.push(item.clone());
231+
self.test_idents.push(item.test_name);
232+
}
233+
}
183234
}
184235

185236
/* Many test structures have the following fields:
@@ -228,6 +279,15 @@ pub(crate) struct TestConst {
228279
pub c_ty: BoxStr,
229280
}
230281

282+
#[derive(Clone, Debug)]
283+
pub(crate) struct TestRoundtrip {
284+
pub test_name: BoxStr,
285+
pub id: BoxStr,
286+
pub fields: Vec<Field>,
287+
pub c_ty: BoxStr,
288+
pub is_alias: bool,
289+
}
290+
231291
fn signededness_test_ident(ident: &str) -> BoxStr {
232292
format!("ctest_signededness_{ident}").into()
233293
}
@@ -244,6 +304,10 @@ fn const_test_ident(ident: &str) -> BoxStr {
244304
format!("ctest_const_{ident}").into()
245305
}
246306

307+
fn roundtrip_test_ident(ident: &str) -> BoxStr {
308+
format!("ctest_roundtrip_{ident}").into()
309+
}
310+
247311
/// Wrap methods that depend on both ffi items and the generator.
248312
struct TranslateHelper<'a> {
249313
ffi_items: &'a FfiItems,

ctest-next/templates/test.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,43 @@ uint32_t ctest_signededness_of__{{ alias.id }}(void) {
5353
return all_ones < 0;
5454
}
5555
{%- endfor +%}
56+
57+
#ifdef _MSC_VER
58+
// Disable signed/unsigned conversion warnings on MSVC.
59+
// These trigger even if the conversion is explicit.
60+
# pragma warning(disable:4365)
61+
#endif
62+
{%- for item in ctx.roundtrip_tests +%}
63+
64+
// Tests whether the struct/union/alias `x` when passed by value to C and back to Rust
65+
// remains unchanged.
66+
// It checks if the size is the same as well as if the padding bytes are all in the correct place.
67+
{{ item.c_ty }} ctest_roundtrip__{{ item.id }}(
68+
{{ item.c_ty }} value,
69+
const uint8_t is_padding_byte[sizeof({{ item.c_ty }})],
70+
uint8_t value_bytes[sizeof({{ item.c_ty }})]
71+
) {
72+
int size = (int)sizeof({{ item.c_ty }});
73+
// Mark `p` as volatile so that the C compiler does not optimize away the pattern we create.
74+
// Otherwise the Rust side would not be able to see it.
75+
volatile uint8_t* p = (volatile uint8_t*)&value;
76+
int i = 0;
77+
for (i = 0; i < size; ++i) {
78+
// We skip padding bytes in both Rust and C because writing to it is undefined.
79+
// Instead we just make sure the the placement of the padding bytes remains the same.
80+
if (is_padding_byte[i]) { continue; }
81+
value_bytes[i] = p[i];
82+
// After we check that the pattern remained unchanged from Rust to C, we invert the pattern
83+
// and send it back to Rust to make sure that it remains unchanged from C to Rust.
84+
uint8_t d = (uint8_t)(255) - (uint8_t)(i % 256);
85+
d = d == 0 ? 42: d;
86+
p[i] = d;
87+
}
88+
return value;
89+
}
90+
91+
{%- endfor +%}
92+
93+
#ifdef _MSC_VER
94+
# pragma warning(default:4365)
95+
#endif

ctest-next/templates/test.rs

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
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)]
1617
use std::{mem, ptr, slice};
18+
#[allow(unused_imports)]
19+
use std::mem::{MaybeUninit, offset_of};
1720

1821
use super::*;
1922

@@ -138,6 +141,128 @@ mod generated_tests {
138141
check_same((all_ones < all_zeros) as u32, c_is_signed, "{{ alias.id }} signed");
139142
}
140143
{%- endfor +%}
144+
145+
{%- for item in ctx.roundtrip_tests +%}
146+
147+
/// Generates a padding map for a specific type.
148+
///
149+
/// Essentially, it returns a list of bytes, whose length is equal to the size of the type in
150+
/// bytes. Each element corresponds to a byte and has two values. `true` if the byte is padding,
151+
/// and `false` if the byte is not padding.
152+
///
153+
/// For aliases we assume that there are no padding bytes, for structs and unions,
154+
/// if there are no fields, then everything is padding, if there are fields, then we have to
155+
/// go through each field and figure out the padding.
156+
fn roundtrip_padding__{{ item.id }}() -> Vec<bool> {
157+
if {{ item.fields.len() }} == 0 {
158+
// FIXME(ctest): What if it's an alias to a struct/union?
159+
return vec![!{{ item.is_alias }}; size_of::<{{ item.id }}>()]
160+
}
161+
162+
// If there are no fields, v and bar become unused.
163+
#[allow(unused_mut)]
164+
let mut v = Vec::<(usize, usize)>::new();
165+
#[allow(unused_variables)]
166+
let bar = MaybeUninit::<{{ item.id }}>::zeroed();
167+
#[allow(unused_variables)]
168+
let bar = bar.as_ptr();
169+
{%- for field in item.fields +%}
170+
171+
let ty_ptr = unsafe { &raw const ((*bar).{{ field.ident() }}) };
172+
let val = unsafe { ty_ptr.read_unaligned() };
173+
174+
let size = size_of_val(&val);
175+
let off = offset_of!({{ item.id }}, {{ field.ident() }});
176+
v.push((off, size));
177+
{%- endfor +%}
178+
// This vector contains `true` if the byte is padding and `false` if the byte is not
179+
// padding. Initialize all bytes as:
180+
// - padding if we have fields, this means that only the fields will be checked
181+
// - no-padding if we have a type alias: if this causes problems the type alias should
182+
// be skipped
183+
let mut is_padding_byte = vec![true; size_of::<{{ item.id }}>()];
184+
for (off, size) in &v {
185+
for i in 0..*size {
186+
is_padding_byte[off + i] = false;
187+
}
188+
}
189+
is_padding_byte
190+
}
191+
192+
/// Tests whether a type alias when passed to C and back to Rust remains unchanged.
193+
///
194+
/// It checks if the size is the same as well as if the padding bytes are all in the
195+
/// correct place. For this test to be sound, `T` must be valid for any bitpattern.
196+
pub fn {{ item.test_name }}() {
197+
type U = {{ item.id }};
198+
extern "C" {
199+
fn ctest_size_of__{{ item.id }}() -> u64;
200+
fn ctest_roundtrip__{{ item.id }}(
201+
input: MaybeUninit<U>, is_padding_byte: *const bool, expected: *mut u8
202+
) -> U;
203+
}
204+
205+
const SIZE: usize = size_of::<U>();
206+
207+
let is_padding_byte = roundtrip_padding__{{ item.id }}();
208+
let mut expected = [0u8; SIZE];
209+
let mut input = MaybeUninit::<U>::zeroed();
210+
211+
let input_ptr = input.as_mut_ptr().cast::<u8>();
212+
213+
// Fill the unitialized memory with a deterministic pattern.
214+
// From Rust to C: every byte will be labelled from 1 to 255, with 0 turning into 42.
215+
// From C to Rust: every byte will be inverted from before (254 -> 1), but 0 is still 42.
216+
for i in 0..SIZE {
217+
let c: u8 = (i % 256) as u8;
218+
let c = if c == 0 { 42 } else { c };
219+
let d: u8 = 255_u8 - (i % 256) as u8;
220+
let d = if d == 0 { 42 } else { d };
221+
unsafe {
222+
input_ptr.add(i).write_volatile(c);
223+
expected[i] = d;
224+
}
225+
}
226+
227+
let c_size = unsafe { ctest_size_of__{{ item.id }}() } as usize;
228+
if SIZE != c_size {
229+
FAILED.store(true, Ordering::Relaxed);
230+
eprintln!(
231+
"size of {{ item.c_ty }} is {c_size} in C and {SIZE} in Rust\n",
232+
);
233+
return;
234+
}
235+
236+
let mut c_value_bytes = vec![0; size_of::<{{ item.id }}>()];
237+
let r: U = unsafe {
238+
ctest_roundtrip__{{ item.id }}(input, is_padding_byte.as_ptr(), c_value_bytes.as_mut_ptr())
239+
};
240+
241+
// Check that the value bytes as read from C match the byte we sent from Rust.
242+
for (i, is_padding_byte) in is_padding_byte.iter().enumerate().take(SIZE) {
243+
if *is_padding_byte { continue; }
244+
let rust = unsafe { *input_ptr.add(i) };
245+
let c = c_value_bytes[i];
246+
if rust != c {
247+
eprintln!("rust[{}] = {} != {} (C): Rust \"{{ item.id }}\" -> C", i, rust, c);
248+
FAILED.store(true, Ordering::Relaxed);
249+
}
250+
}
251+
252+
// Check that value returned from C contains the bytes we expect.
253+
for (i, is_padding_byte) in is_padding_byte.iter().enumerate().take(SIZE) {
254+
if *is_padding_byte { continue; }
255+
let rust = expected[i] as usize;
256+
let c = unsafe { (&raw const r).cast::<u8>().add(i).read_volatile() as usize };
257+
if rust != c {
258+
eprintln!(
259+
"rust [{i}] = {rust} != {c} (C): C \"{{ item.id }}\" -> Rust",
260+
);
261+
FAILED.store(true, Ordering::Relaxed);
262+
}
263+
}
264+
}
265+
{%- endfor +%}
141266
}
142267

143268
use generated_tests::*;

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)