Skip to content

Commit 197138e

Browse files
committed
ctest: add test for roundtripping structs/aliases/unions.
1 parent add5b99 commit 197138e

15 files changed

+1358
-27
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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,47 @@ 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+
int* error,
70+
uint8_t pad[sizeof({{ item.c_ty }})],
71+
uint8_t expected_pad[sizeof({{ item.c_ty }})]
72+
) {
73+
int size = (int)sizeof({{ item.c_ty }});
74+
// Mark `p` as volatile so that the C compiler does not optimize away the pattern we create.
75+
// Otherwise the Rust side would not be able to see it.
76+
volatile uint8_t* p = (volatile uint8_t*)&value;
77+
int i = 0;
78+
for (i = 0; i < size; ++i) {
79+
// We skip padding bytes in both Rust and C because writing to it is undefined.
80+
// Instead we just make sure the the placement of the padding bytes remains the same.
81+
if (pad[i]) { continue; }
82+
uint8_t expected = (uint8_t)(i % 256);
83+
expected = expected == 0? 42 : expected;
84+
expected_pad[i] = expected;
85+
// After we check that the pattern remained unchanged from Rust to C, we invert the pattern
86+
// and send it back to Rust to make sure that it remains unchanged from C to Rust.
87+
uint8_t d
88+
= (uint8_t)(255) - (uint8_t)(i % 256);
89+
d = d == 0? 42: d;
90+
p[i] = d;
91+
}
92+
return value;
93+
}
94+
95+
{%- endfor +%}
96+
97+
#ifdef _MSC_VER
98+
# pragma warning(default:4365)
99+
#endif

ctest-next/templates/test.rs

Lines changed: 120 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,122 @@ 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+
vec![!{{ item.is_alias }}; size_of::<{{ item.id }}>()]
160+
{%- else +%}
161+
let mut v = Vec::<(usize, usize)>::new();
162+
let bar = MaybeUninit::<{{ item.id }}>::zeroed();
163+
let bar = bar.as_ptr();
164+
{%- for field in item.fields +%}
165+
166+
let ty_ptr = unsafe { &raw const ((*bar).{{ field.ident() }}) };
167+
let val = unsafe { ty_ptr.read_unaligned() };
168+
169+
let size = size_of_val(&val);
170+
let off = offset_of!({{ item.id }}, {{ field.ident() }});
171+
v.push((off, size));
172+
{%- endfor +%}
173+
// This vector contains `true` if the byte is padding and `false` if the byte is not
174+
// padding. Initialize all bytes as:
175+
// - padding if we have fields, this means that only the fields will be checked
176+
// - no-padding if we have a type alias: if this causes problems the type alias should
177+
// be skipped
178+
let mut pad = vec![true; size_of::<{{ item.id }}>()];
179+
for (off, size) in &v {
180+
for i in 0..*size {
181+
pad[off + i] = false;
182+
}
183+
}
184+
pad
185+
{%- endif +%}
186+
}
187+
188+
/// Tests whether the type alias `x` when passed to C and back to Rust remains unchanged.
189+
///
190+
/// It checks if the size is the same as well as if the padding bytes are all in the
191+
/// correct place.
192+
pub fn {{ item.test_name }}() {
193+
type U = {{ item.id }};
194+
extern "C" {
195+
fn ctest_size_of__{{ item.id }}() -> u64;
196+
fn ctest_roundtrip__{{ item.id }}(
197+
x: MaybeUninit<U>, e: *mut c_int, pad: *const bool, expected: *mut u8
198+
) -> U;
199+
}
200+
let pad = roundtrip_padding__{{ item.id }}();
201+
let mut error: c_int = 0;
202+
let mut y = MaybeUninit::<U>::zeroed();
203+
let mut x = MaybeUninit::<U>::zeroed();
204+
205+
let x_ptr = x.as_mut_ptr().cast::<u8>();
206+
let y_ptr = y.as_mut_ptr().cast::<u8>();
207+
let size = size_of::<U>();
208+
209+
// Fill the unitialized memory with a deterministic pattern.
210+
// From Rust to C: every byte will be labelled from 1 to 255, with 0 turning into 42.
211+
// From C to Rust: every byte will be inverted from before (254 -> 1), but 0 is still 42.
212+
for i in 0..size {
213+
let c: u8 = (i % 256) as u8;
214+
let c = if c == 0 { 42 } else { c };
215+
let d: u8 = 255_u8 - (i % 256) as u8;
216+
let d = if d == 0 { 42 } else { d };
217+
unsafe {
218+
x_ptr.add(i).write_volatile(c);
219+
y_ptr.add(i).write_volatile(d);
220+
}
221+
}
222+
223+
let c_size = unsafe { ctest_size_of__{{ item.id }}() } as usize;
224+
if size != c_size {
225+
FAILED.store(true, Ordering::Relaxed);
226+
eprintln!(
227+
"size of {{ item.c_ty }} is {c_size} in C and {size} in Rust\n",
228+
);
229+
return;
230+
}
231+
232+
let mut expected = vec![0; size_of::<{{ item.id }}>()];
233+
let r: U = unsafe {
234+
ctest_roundtrip__{{ item.id }}(x, &mut error, pad.as_ptr(), expected.as_mut_ptr())
235+
};
236+
237+
for (i, elem) in pad.iter().enumerate().take(size) {
238+
if *elem { continue; }
239+
let rust = unsafe { *x_ptr.add(i) };
240+
let c = expected[i];
241+
if rust != c {
242+
eprintln!("rust[{}] = {} != {} (C): Rust \"{{ item.id }}\" -> C", i, rust, c);
243+
FAILED.store(true, Ordering::Relaxed);
244+
}
245+
}
246+
247+
for (i, elem) in pad.iter().enumerate().take(size) {
248+
if *elem { continue; }
249+
let rust = unsafe { (*y_ptr.add(i)) as usize };
250+
let c = unsafe { (&raw const r).cast::<u8>().add(i).read_volatile() as usize };
251+
if rust != c {
252+
eprintln!(
253+
"rust [{i}] = {rust} != {c} (C): C \"{{ item.id }}\" -> Rust",
254+
);
255+
FAILED.store(true, Ordering::Relaxed);
256+
}
257+
}
258+
}
259+
{%- endfor +%}
141260
}
142261

143262
use generated_tests::*;

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

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ static bool ctest_const_ON_val_static = ON;
1111

1212
// Define a function that returns a pointer to the value of the constant to test.
1313
// This will later be called on the Rust side via FFI.
14-
bool *ctest_const__ON(void)
15-
{
14+
bool *ctest_const__ON(void) {
1615
return &ctest_const_ON_val_static;
1716
}
1817

@@ -24,8 +23,48 @@ uint64_t ctest_align_of__in6_addr(void) { return _Alignof(in6_addr); }
2423

2524
// Return `1` if the type is signed, otherwise return `0`.
2625
// Casting -1 to the aliased type if signed evaluates to `-1 < 0`, if unsigned to `MAX_VALUE < 0`
27-
uint32_t ctest_signededness_of__in6_addr(void)
28-
{
29-
in6_addr all_ones = (in6_addr)-1;
26+
uint32_t ctest_signededness_of__in6_addr(void) {
27+
in6_addr all_ones = (in6_addr) -1;
3028
return all_ones < 0;
3129
}
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+
int* error,
43+
uint8_t pad[sizeof(in6_addr)],
44+
uint8_t expected_pad[sizeof(in6_addr)]
45+
) {
46+
int size = (int)sizeof(in6_addr);
47+
// Mark `p` as volatile so that the C compiler does not optimize away the pattern we create.
48+
// Otherwise the Rust side would not be able to see it.
49+
volatile uint8_t* p = (volatile uint8_t*)&value;
50+
int i = 0;
51+
for (i = 0; i < size; ++i) {
52+
// We skip padding bytes in both Rust and C because writing to it is undefined.
53+
// Instead we just make sure the the placement of the padding bytes remains the same.
54+
if (pad[i]) { continue; }
55+
uint8_t expected = (uint8_t)(i % 256);
56+
expected = expected == 0? 42 : expected;
57+
expected_pad[i] = expected;
58+
// After we check that the pattern remained unchanged from Rust to C, we invert the pattern
59+
// and send it back to Rust to make sure that it remains unchanged from C to Rust.
60+
uint8_t d
61+
= (uint8_t)(255) - (uint8_t)(i % 256);
62+
d = d == 0? 42: d;
63+
p[i] = d;
64+
}
65+
return value;
66+
}
67+
68+
#ifdef _MSC_VER
69+
# pragma warning(default:4365)
70+
#endif

0 commit comments

Comments
 (0)