From 29214633f17ef3b876a0fc8e967a4fdf366c2d2f Mon Sep 17 00:00:00 2001 From: mbyx Date: Fri, 21 Nov 2025 17:49:47 +0500 Subject: [PATCH] ctest: allow different code generation according to edition --- ctest/src/generator.rs | 21 + ctest/src/template.rs | 17 + ctest/templates/test.rs | 19 +- ctest/tests/basic.rs | 14 + ctest/tests/input/hierarchy.out.rs | 2 +- ctest/tests/input/macro.out.edition-2024.c | 195 +++++++ ctest/tests/input/macro.out.edition-2024.rs | 558 +++++++++++++++++++ ctest/tests/input/simple.out.with-renames.rs | 4 +- ctest/tests/input/simple.out.with-skips.rs | 2 +- 9 files changed, 819 insertions(+), 13 deletions(-) create mode 100644 ctest/tests/input/macro.out.edition-2024.c create mode 100644 ctest/tests/input/macro.out.edition-2024.rs diff --git a/ctest/src/generator.rs b/ctest/src/generator.rs index 47a865c0f7020..bc12c7c4fe3ba 100644 --- a/ctest/src/generator.rs +++ b/ctest/src/generator.rs @@ -36,6 +36,9 @@ use crate::{ get_build_target, }; +/// The default Rust edition used to generate the code. +const DEFAULT_EDITION: u32 = 2021; + /// A function that takes a mappable input and returns its mapping as `Some`, otherwise /// use the default name if `None`. type MappedName = Box Option>; @@ -73,6 +76,8 @@ pub struct TestGenerator { pub(crate) skip_roundtrip: Option, pub(crate) skip_signededness: Option, pub(crate) skip_fn_ptrcheck: Option, + /// The Rust edition to generate code against. + pub(crate) edition: Option, } /// An error that occurs when generating the test files. @@ -239,6 +244,21 @@ impl TestGenerator { self } + /// Set the Rust edition that the code is generated for. + /// + /// # Examples + /// + /// ```no_run + /// use ctest::TestGenerator; + /// + /// let mut cfg = TestGenerator::new(); + /// cfg.edition(2024); + /// ``` + pub fn edition(&mut self, e: u32) -> &mut Self { + self.edition = Some(e); + self + } + /// Configures the output directory of the generated Rust and C code. /// /// # Examples @@ -1048,6 +1068,7 @@ impl TestGenerator { }; let mut rust_file = RustTestTemplate::new(&ffi_items, self)? + .edition(self.edition.unwrap_or(DEFAULT_EDITION)) .render() .map_err(GenerationError::RustTemplateRender)?; ensure_trailing_newline(&mut rust_file); diff --git a/ctest/src/template.rs b/ctest/src/template.rs index 0580c1ba8a39b..6431f3b41a800 100644 --- a/ctest/src/template.rs +++ b/ctest/src/template.rs @@ -27,6 +27,7 @@ use crate::{ #[template(path = "test.rs")] pub(crate) struct RustTestTemplate { pub template: TestTemplate, + pub extern_keyword: BoxStr, } impl RustTestTemplate { @@ -36,8 +37,24 @@ impl RustTestTemplate { ) -> Result { Ok(Self { template: TestTemplate::new(ffi_items, generator)?, + extern_keyword: "extern".into(), }) } + + /// Modify the generated template such that it supports edition 2024. + pub(crate) fn edition(&mut self, edition: u32) -> &Self { + match edition { + 2021 => { + self.extern_keyword = "extern".into(); + } + 2024 => { + // For now we only use this to convert extern to unsafe extern. + self.extern_keyword = "unsafe extern".into(); + } + _ => panic!("unsupported or invalid edition: {edition}"), + } + self + } } /// Represents the C side of the generated testing suite. diff --git a/ctest/templates/test.rs b/ctest/templates/test.rs index 7f313544ed3c3..8e6c5d99f48af 100644 --- a/ctest/templates/test.rs +++ b/ctest/templates/test.rs @@ -2,6 +2,7 @@ {#- ↑ Doesn't apply here, this is the template! +#} {%- let ctx = self.template +%} +{%- let ctest_extern = self.extern_keyword +%} /// As this file is sometimes built using rustc, crate level attributes /// are not allowed at the top-level, so we hack around this by keeping it @@ -73,7 +74,7 @@ mod generated_tests { // Test that the string constant is the same in both Rust and C. // While fat pointers can't be translated, we instead use * const c_char. pub fn {{ const_cstr.test_name }}() { - extern "C" { + {{ ctest_extern }} "C" { fn ctest_const_cstr__{{ const_cstr.id }}() -> *const c_char; } @@ -100,7 +101,7 @@ mod generated_tests { // This performs a byte by byte comparison of the constant value. pub fn {{ constant.test_name }}() { type T = {{ constant.rust_ty }}; - extern "C" { + {{ ctest_extern }} "C" { fn ctest_const__{{ constant.id }}() -> *const T; } @@ -125,7 +126,7 @@ mod generated_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" { + {{ ctest_extern }} "C" { fn ctest_size_of__{{ item.id }}() -> u64; fn ctest_align_of__{{ item.id }}() -> u64; } @@ -149,7 +150,7 @@ mod generated_tests { /// 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" { + {{ ctest_extern }} "C" { fn ctest_signededness_of__{{ alias.id }}() -> u32; } let all_ones = !(0 as {{ alias.id }}); @@ -164,7 +165,7 @@ mod generated_tests { /// Make sure that the offset and size of a field in a struct/union is the same. pub fn {{ item.test_name }}() { - extern "C" { + {{ ctest_extern }} "C" { fn ctest_offset_of__{{ item.id }}__{{ item.field.ident() }}() -> u64; fn ctest_size_of__{{ item.id }}__{{ item.field.ident() }}() -> u64; } @@ -193,7 +194,7 @@ mod generated_tests { /// Tests if the pointer to the field is the same in Rust and C. pub fn {{ item.test_name }}() { - extern "C" { + {{ ctest_extern }} "C" { fn ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}(a: *const {{ item.id }}) -> *mut u8; } @@ -264,7 +265,7 @@ mod generated_tests { /// 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" { + {{ ctest_extern }} "C" { fn ctest_size_of__{{ item.id }}() -> u64; fn ctest_roundtrip__{{ item.id }}( input: MaybeUninit, is_padding_byte: *const bool, value_bytes: *mut u8 @@ -337,7 +338,7 @@ mod generated_tests { /// Check if the Rust and C side function pointers point to the same underlying function. pub fn {{ item.test_name }}() { - extern "C" { + {{ ctest_extern }} "C" { fn ctest_foreign_fn__{{ item.id }}() -> unsafe extern "C" fn(); } let actual = unsafe { ctest_foreign_fn__{{ item.id }}() } as u64; @@ -350,7 +351,7 @@ mod generated_tests { // Tests if the pointer to the static variable matches in both Rust and C. pub fn {{ static_.test_name }}() { - extern "C" { + {{ ctest_extern }} "C" { fn ctest_static__{{ static_.id }}() -> *const {{ static_.rust_ty }}; } let actual = (&raw const {{ static_.id }}).addr(); diff --git a/ctest/tests/basic.rs b/ctest/tests/basic.rs index 6718456f32da9..23523cdc4dfbc 100644 --- a/ctest/tests/basic.rs +++ b/ctest/tests/basic.rs @@ -150,6 +150,20 @@ fn test_entrypoint_macro() { check_entrypoint(&mut gen_, out_dir, crate_path, library_path, include_path); } +/// Test if generated code for macro.rs passes requirements for edition 2024. +#[test] +fn test_edition_2024_macro() { + let include_path = PathBuf::from("tests/input"); + let crate_path = include_path.join("macro.rs"); + let library_path = "macro.out.edition-2024.a"; + + let (mut gen_, out_dir) = default_generator(1, None).unwrap(); + gen_.edition(2024) + .header_with_defines("macro.h", vec!["SUPPRESS_ERROR"]); + + check_entrypoint(&mut gen_, out_dir, crate_path, library_path, include_path); +} + /// Test if a file with invalid syntax fails to generate tests. #[test] fn test_entrypoint_invalid_syntax() { diff --git a/ctest/tests/input/hierarchy.out.rs b/ctest/tests/input/hierarchy.out.rs index dfb1c1f08ef84..8976956b93dbe 100644 --- a/ctest/tests/input/hierarchy.out.rs +++ b/ctest/tests/input/hierarchy.out.rs @@ -112,7 +112,7 @@ mod generated_tests { /// this would result in a value larger than zero. For signed types, this results in a value /// smaller than 0. pub fn ctest_signededness_in6_addr() { - extern "C" { + extern "C" { fn ctest_signededness_of__in6_addr() -> u32; } let all_ones = !(0 as in6_addr); diff --git a/ctest/tests/input/macro.out.edition-2024.c b/ctest/tests/input/macro.out.edition-2024.c new file mode 100644 index 0000000000000..975ba9a8e371d --- /dev/null +++ b/ctest/tests/input/macro.out.edition-2024.c @@ -0,0 +1,195 @@ +/* This file was autogenerated by ctest; do not modify directly */ + +#include +#include +#include +#include + +#define SUPPRESS_ERROR +#include +#undef SUPPRESS_ERROR + +#if defined(__cplusplus) + #define CTEST_ALIGNOF(T) alignof(T) + #define CTEST_EXTERN extern "C" +#else + #define CTEST_ALIGNOF(T) _Alignof(T) + #define CTEST_EXTERN +#endif + +typedef void (*ctest_void_func)(void); + +// Return the size of a type. +CTEST_EXTERN uint64_t ctest_size_of__VecU8(void) { return sizeof(struct VecU8); } + +// Return the alignment of a type. +CTEST_EXTERN uint64_t ctest_align_of__VecU8(void) { return CTEST_ALIGNOF(struct VecU8); } + +// Return the size of a type. +CTEST_EXTERN uint64_t ctest_size_of__VecU16(void) { return sizeof(struct VecU16); } + +// Return the alignment of a type. +CTEST_EXTERN uint64_t ctest_align_of__VecU16(void) { return CTEST_ALIGNOF(struct VecU16); } + +// Return the offset of a struct/union field. +CTEST_EXTERN uint64_t ctest_offset_of__VecU8__x(void) { + return offsetof(struct VecU8, x); +} + +// Return the size of a struct/union field. +CTEST_EXTERN uint64_t ctest_size_of__VecU8__x(void) { + return sizeof(((struct VecU8){}).x); +} + +// Return the offset of a struct/union field. +CTEST_EXTERN uint64_t ctest_offset_of__VecU8__y(void) { + return offsetof(struct VecU8, y); +} + +// Return the size of a struct/union field. +CTEST_EXTERN uint64_t ctest_size_of__VecU8__y(void) { + return sizeof(((struct VecU8){}).y); +} + +// Return the offset of a struct/union field. +CTEST_EXTERN uint64_t ctest_offset_of__VecU16__x(void) { + return offsetof(struct VecU16, x); +} + +// Return the size of a struct/union field. +CTEST_EXTERN uint64_t ctest_size_of__VecU16__x(void) { + return sizeof(((struct VecU16){}).x); +} + +// Return the offset of a struct/union field. +CTEST_EXTERN uint64_t ctest_offset_of__VecU16__y(void) { + return offsetof(struct VecU16, y); +} + +// Return the size of a struct/union field. +CTEST_EXTERN uint64_t ctest_size_of__VecU16__y(void) { + return sizeof(((struct VecU16){}).y); +} + +// Return a pointer to a struct/union field. +// This field can have a normal data type, or it could be a function pointer or an array, which +// have different syntax. A typedef is used for convenience, but the syntax must be precomputed. +typedef uint8_t *ctest_field_ty__VecU8__x; +CTEST_EXTERN ctest_field_ty__VecU8__x +ctest_field_ptr__VecU8__x(struct VecU8 *b) { + return &b->x; +} + +// Return a pointer to a struct/union field. +// This field can have a normal data type, or it could be a function pointer or an array, which +// have different syntax. A typedef is used for convenience, but the syntax must be precomputed. +typedef uint8_t *ctest_field_ty__VecU8__y; +CTEST_EXTERN ctest_field_ty__VecU8__y +ctest_field_ptr__VecU8__y(struct VecU8 *b) { + return &b->y; +} + +// Return a pointer to a struct/union field. +// This field can have a normal data type, or it could be a function pointer or an array, which +// have different syntax. A typedef is used for convenience, but the syntax must be precomputed. +typedef uint16_t *ctest_field_ty__VecU16__x; +CTEST_EXTERN ctest_field_ty__VecU16__x +ctest_field_ptr__VecU16__x(struct VecU16 *b) { + return &b->x; +} + +// Return a pointer to a struct/union field. +// This field can have a normal data type, or it could be a function pointer or an array, which +// have different syntax. A typedef is used for convenience, but the syntax must be precomputed. +typedef uint16_t *ctest_field_ty__VecU16__y; +CTEST_EXTERN ctest_field_ty__VecU16__y +ctest_field_ptr__VecU16__y(struct VecU16 *b) { + return &b->y; +} + +#ifdef _MSC_VER + // Disable signed/unsigned conversion warnings on MSVC. + // These trigger even if the conversion is explicit. + #pragma warning(disable:4365) +#endif + +#ifdef __GNUC__ + // GCC emits a warning with `-Wextra` if we return a typedef to a type marked `volatile`. + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wignored-qualifiers" +#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. +CTEST_EXTERN struct VecU8 ctest_roundtrip__VecU8( + struct VecU8 value, + const uint8_t is_padding_byte[sizeof(struct VecU8)], + uint8_t value_bytes[sizeof(struct VecU8)] +) { + int size = (int)sizeof(struct VecU8); + // 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; +} + +// 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. +CTEST_EXTERN struct VecU16 ctest_roundtrip__VecU16( + struct VecU16 value, + const uint8_t is_padding_byte[sizeof(struct VecU16)], + uint8_t value_bytes[sizeof(struct VecU16)] +) { + int size = (int)sizeof(struct VecU16); + // 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 __GNUC__ + // Pop allow for `-Wignored-qualifiers` + #pragma GCC diagnostic pop +#endif + +#ifdef _MSC_VER + // Pop allow for 4365 + #pragma warning(default:4365) +#endif + +#ifdef _MSC_VER + // Disable function pointer type conversion warnings on MSVC. + // The conversion may fail only if we call that function, however we only check its address. + #pragma warning(disable:4191) +#endif + +#ifdef _MSC_VER + // Pop allow for 4191 + #pragma warning(default:4191) +#endif diff --git a/ctest/tests/input/macro.out.edition-2024.rs b/ctest/tests/input/macro.out.edition-2024.rs new file mode 100644 index 0000000000000..125bd3b213aac --- /dev/null +++ b/ctest/tests/input/macro.out.edition-2024.rs @@ -0,0 +1,558 @@ +/* This file was autogenerated by ctest; do not modify directly */ + +/// As this file is sometimes built using rustc, crate level attributes +/// are not allowed at the top-level, so we hack around this by keeping it +/// inside of a module. +mod generated_tests { + #![allow(non_snake_case)] + #![deny(improper_ctypes_definitions)] + #[allow(unused_imports)] + use std::ffi::{CStr, c_int, c_char, c_uint}; + use std::fmt::{Debug, Write}; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + #[allow(unused_imports)] + use std::{mem, ptr, slice}; + #[allow(unused_imports)] + use std::mem::{MaybeUninit, offset_of}; + + use super::*; + + pub static FAILED: AtomicBool = AtomicBool::new(false); + pub static NTESTS: AtomicUsize = AtomicUsize::new(0); + + /// Check that the value returned from the Rust and C side in a certain test is equivalent. + /// + /// Internally it will remember which checks failed and how many tests have been run. + fn check_same(rust: T, c: T, attr: &str) { + if rust != c { + eprintln!("bad {attr}: rust: {rust:?} != c {c:?}"); + FAILED.store(true, Ordering::Relaxed); + } else { + NTESTS.fetch_add(1, Ordering::Relaxed); + } + } + + fn check_same_bytes(rust: &[u8], c: &[u8], attr: &str) { + if rust == c { + NTESTS.fetch_add(1, Ordering::Relaxed); + return; + } + + FAILED.store(true, Ordering::Relaxed); + // Buffer to a string so we don't write individual bytes to stdio + let mut s = String::new(); + if rust.len() == c.len() { + for (i, (&rb, &cb)) in rust.iter().zip(c.iter()).enumerate() { + if rb != cb { + writeln!( + s, "bad {attr} at byte {i}: rust: {rb:?} ({rb:#x}) != c {cb:?} ({cb:#x})" + ).unwrap(); + break; + } + } + } else { + writeln!(s, "bad {attr}: rust len {} != c len {}", rust.len(), c.len()).unwrap(); + } + + write!(s, " rust bytes:").unwrap(); + for b in rust { + write!(s, " {b:02x}").unwrap(); + } + write!(s, "\n c bytes: ").unwrap(); + for b in c { + write!(s, " {b:02x}").unwrap(); + } + eprintln!("{s}"); + } + + /// Compare the size and alignment of the type in Rust and C, making sure they are the same. + pub fn ctest_size_align_VecU8() { + unsafe extern "C" { + fn ctest_size_of__VecU8() -> u64; + fn ctest_align_of__VecU8() -> u64; + } + + let rust_size = size_of::() as u64; + let c_size = unsafe { ctest_size_of__VecU8() }; + + let rust_align = align_of::() as u64; + let c_align = unsafe { ctest_align_of__VecU8() }; + + check_same(rust_size, c_size, "`VecU8` size"); + check_same(rust_align, c_align, "`VecU8` align"); + } + + /// Compare the size and alignment of the type in Rust and C, making sure they are the same. + pub fn ctest_size_align_VecU16() { + unsafe extern "C" { + fn ctest_size_of__VecU16() -> u64; + fn ctest_align_of__VecU16() -> u64; + } + + let rust_size = size_of::() as u64; + let c_size = unsafe { ctest_size_of__VecU16() }; + + let rust_align = align_of::() as u64; + let c_align = unsafe { ctest_align_of__VecU16() }; + + check_same(rust_size, c_size, "`VecU16` size"); + check_same(rust_align, c_align, "`VecU16` align"); + } + + /// Make sure that the offset and size of a field in a struct/union is the same. + pub fn ctest_field_size_offset_VecU8_x() { + unsafe extern "C" { + fn ctest_offset_of__VecU8__x() -> u64; + fn ctest_size_of__VecU8__x() -> u64; + } + + let uninit_ty = MaybeUninit::::zeroed(); + let uninit_ty = uninit_ty.as_ptr(); + + // SAFETY: we assume the field access doesn't wrap + let ty_ptr = unsafe { &raw const (*uninit_ty).x }; + // SAFETY: we assume that all zeros is a valid bitpattern for `ty_ptr`, otherwise the + // test should be skipped. + let val = unsafe { ty_ptr.read_unaligned() }; + + // SAFETY: FFI call with no preconditions + let ctest_field_offset = unsafe { ctest_offset_of__VecU8__x() }; + check_same(offset_of!(VecU8, x) as u64, ctest_field_offset, + "field offset `x` of `VecU8`"); + // SAFETY: FFI call with no preconditions + let ctest_field_size = unsafe { ctest_size_of__VecU8__x() }; + check_same(size_of_val(&val) as u64, ctest_field_size, + "field size `x` of `VecU8`"); + } + + /// Make sure that the offset and size of a field in a struct/union is the same. + pub fn ctest_field_size_offset_VecU8_y() { + unsafe extern "C" { + fn ctest_offset_of__VecU8__y() -> u64; + fn ctest_size_of__VecU8__y() -> u64; + } + + let uninit_ty = MaybeUninit::::zeroed(); + let uninit_ty = uninit_ty.as_ptr(); + + // SAFETY: we assume the field access doesn't wrap + let ty_ptr = unsafe { &raw const (*uninit_ty).y }; + // SAFETY: we assume that all zeros is a valid bitpattern for `ty_ptr`, otherwise the + // test should be skipped. + let val = unsafe { ty_ptr.read_unaligned() }; + + // SAFETY: FFI call with no preconditions + let ctest_field_offset = unsafe { ctest_offset_of__VecU8__y() }; + check_same(offset_of!(VecU8, y) as u64, ctest_field_offset, + "field offset `y` of `VecU8`"); + // SAFETY: FFI call with no preconditions + let ctest_field_size = unsafe { ctest_size_of__VecU8__y() }; + check_same(size_of_val(&val) as u64, ctest_field_size, + "field size `y` of `VecU8`"); + } + + /// Make sure that the offset and size of a field in a struct/union is the same. + pub fn ctest_field_size_offset_VecU16_x() { + unsafe extern "C" { + fn ctest_offset_of__VecU16__x() -> u64; + fn ctest_size_of__VecU16__x() -> u64; + } + + let uninit_ty = MaybeUninit::::zeroed(); + let uninit_ty = uninit_ty.as_ptr(); + + // SAFETY: we assume the field access doesn't wrap + let ty_ptr = unsafe { &raw const (*uninit_ty).x }; + // SAFETY: we assume that all zeros is a valid bitpattern for `ty_ptr`, otherwise the + // test should be skipped. + let val = unsafe { ty_ptr.read_unaligned() }; + + // SAFETY: FFI call with no preconditions + let ctest_field_offset = unsafe { ctest_offset_of__VecU16__x() }; + check_same(offset_of!(VecU16, x) as u64, ctest_field_offset, + "field offset `x` of `VecU16`"); + // SAFETY: FFI call with no preconditions + let ctest_field_size = unsafe { ctest_size_of__VecU16__x() }; + check_same(size_of_val(&val) as u64, ctest_field_size, + "field size `x` of `VecU16`"); + } + + /// Make sure that the offset and size of a field in a struct/union is the same. + pub fn ctest_field_size_offset_VecU16_y() { + unsafe extern "C" { + fn ctest_offset_of__VecU16__y() -> u64; + fn ctest_size_of__VecU16__y() -> u64; + } + + let uninit_ty = MaybeUninit::::zeroed(); + let uninit_ty = uninit_ty.as_ptr(); + + // SAFETY: we assume the field access doesn't wrap + let ty_ptr = unsafe { &raw const (*uninit_ty).y }; + // SAFETY: we assume that all zeros is a valid bitpattern for `ty_ptr`, otherwise the + // test should be skipped. + let val = unsafe { ty_ptr.read_unaligned() }; + + // SAFETY: FFI call with no preconditions + let ctest_field_offset = unsafe { ctest_offset_of__VecU16__y() }; + check_same(offset_of!(VecU16, y) as u64, ctest_field_offset, + "field offset `y` of `VecU16`"); + // SAFETY: FFI call with no preconditions + let ctest_field_size = unsafe { ctest_size_of__VecU16__y() }; + check_same(size_of_val(&val) as u64, ctest_field_size, + "field size `y` of `VecU16`"); + } + + /// Tests if the pointer to the field is the same in Rust and C. + pub fn ctest_field_ptr_VecU8_x() { + unsafe extern "C" { + fn ctest_field_ptr__VecU8__x(a: *const VecU8) -> *mut u8; + } + + let uninit_ty = MaybeUninit::::zeroed(); + let ty_ptr = uninit_ty.as_ptr(); + // SAFETY: We don't read `field_ptr`, only compare the pointer itself. + // The assumption is made that this does not wrap the address space. + let field_ptr = unsafe { &raw const ((*ty_ptr).x) }; + + // SAFETY: FFI call with no preconditions + let ctest_field_ptr = unsafe { ctest_field_ptr__VecU8__x(ty_ptr) }; + check_same(field_ptr.cast(), ctest_field_ptr, + "field pointer access `x` of `VecU8`"); + } + + /// Tests if the pointer to the field is the same in Rust and C. + pub fn ctest_field_ptr_VecU8_y() { + unsafe extern "C" { + fn ctest_field_ptr__VecU8__y(a: *const VecU8) -> *mut u8; + } + + let uninit_ty = MaybeUninit::::zeroed(); + let ty_ptr = uninit_ty.as_ptr(); + // SAFETY: We don't read `field_ptr`, only compare the pointer itself. + // The assumption is made that this does not wrap the address space. + let field_ptr = unsafe { &raw const ((*ty_ptr).y) }; + + // SAFETY: FFI call with no preconditions + let ctest_field_ptr = unsafe { ctest_field_ptr__VecU8__y(ty_ptr) }; + check_same(field_ptr.cast(), ctest_field_ptr, + "field pointer access `y` of `VecU8`"); + } + + /// Tests if the pointer to the field is the same in Rust and C. + pub fn ctest_field_ptr_VecU16_x() { + unsafe extern "C" { + fn ctest_field_ptr__VecU16__x(a: *const VecU16) -> *mut u8; + } + + let uninit_ty = MaybeUninit::::zeroed(); + let ty_ptr = uninit_ty.as_ptr(); + // SAFETY: We don't read `field_ptr`, only compare the pointer itself. + // The assumption is made that this does not wrap the address space. + let field_ptr = unsafe { &raw const ((*ty_ptr).x) }; + + // SAFETY: FFI call with no preconditions + let ctest_field_ptr = unsafe { ctest_field_ptr__VecU16__x(ty_ptr) }; + check_same(field_ptr.cast(), ctest_field_ptr, + "field pointer access `x` of `VecU16`"); + } + + /// Tests if the pointer to the field is the same in Rust and C. + pub fn ctest_field_ptr_VecU16_y() { + unsafe extern "C" { + fn ctest_field_ptr__VecU16__y(a: *const VecU16) -> *mut u8; + } + + let uninit_ty = MaybeUninit::::zeroed(); + let ty_ptr = uninit_ty.as_ptr(); + // SAFETY: We don't read `field_ptr`, only compare the pointer itself. + // The assumption is made that this does not wrap the address space. + let field_ptr = unsafe { &raw const ((*ty_ptr).y) }; + + // SAFETY: FFI call with no preconditions + let ctest_field_ptr = unsafe { ctest_field_ptr__VecU16__y(ty_ptr) }; + check_same(field_ptr.cast(), ctest_field_ptr, + "field pointer access `y` of `VecU16`"); + } + + /// 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__VecU8() -> Vec { + if 2 == 0 { + // FIXME(ctest): What if it's an alias to a struct/union? + return vec![!false; size_of::()] + } + + // 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::::zeroed(); + #[allow(unused_variables)] + let bar = bar.as_ptr(); + + let ty_ptr = unsafe { &raw const ((*bar).x) }; + let val = unsafe { ty_ptr.read_unaligned() }; + + let size = size_of_val(&val); + let off = offset_of!(VecU8, x); + v.push((off, size)); + + let ty_ptr = unsafe { &raw const ((*bar).y) }; + let val = unsafe { ty_ptr.read_unaligned() }; + + let size = size_of_val(&val); + let off = offset_of!(VecU8, y); + v.push((off, size)); + // 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::()]; + 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 ctest_roundtrip_VecU8() { + type U = VecU8; + unsafe extern "C" { + fn ctest_size_of__VecU8() -> u64; + fn ctest_roundtrip__VecU8( + input: MaybeUninit, is_padding_byte: *const bool, value_bytes: *mut u8 + ) -> U; + } + + const SIZE: usize = size_of::(); + + let is_padding_byte = roundtrip_padding__VecU8(); + let mut expected = vec![0u8; SIZE]; + let mut input = MaybeUninit::::zeroed(); + + let input_ptr = input.as_mut_ptr().cast::(); + + // Fill the uninitialized 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__VecU8() } as usize; + if SIZE != c_size { + FAILED.store(true, Ordering::Relaxed); + eprintln!( + "size of `struct VecU8` is {c_size} in C and {SIZE} in Rust\n", + ); + return; + } + + let mut c_value_bytes = vec![0; size_of::()]; + let r: U = unsafe { + ctest_roundtrip__VecU8(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 `VecU8` -> 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::().add(i).read_volatile() as usize }; + if rust != c { + eprintln!( + "rust [{i}] = {rust} != {c} (C): C `VecU8` -> Rust", + ); + FAILED.store(true, Ordering::Relaxed); + } + } + } + + /// 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__VecU16() -> Vec { + if 2 == 0 { + // FIXME(ctest): What if it's an alias to a struct/union? + return vec![!false; size_of::()] + } + + // 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::::zeroed(); + #[allow(unused_variables)] + let bar = bar.as_ptr(); + + let ty_ptr = unsafe { &raw const ((*bar).x) }; + let val = unsafe { ty_ptr.read_unaligned() }; + + let size = size_of_val(&val); + let off = offset_of!(VecU16, x); + v.push((off, size)); + + let ty_ptr = unsafe { &raw const ((*bar).y) }; + let val = unsafe { ty_ptr.read_unaligned() }; + + let size = size_of_val(&val); + let off = offset_of!(VecU16, y); + v.push((off, size)); + // 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::()]; + 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 ctest_roundtrip_VecU16() { + type U = VecU16; + unsafe extern "C" { + fn ctest_size_of__VecU16() -> u64; + fn ctest_roundtrip__VecU16( + input: MaybeUninit, is_padding_byte: *const bool, value_bytes: *mut u8 + ) -> U; + } + + const SIZE: usize = size_of::(); + + let is_padding_byte = roundtrip_padding__VecU16(); + let mut expected = vec![0u8; SIZE]; + let mut input = MaybeUninit::::zeroed(); + + let input_ptr = input.as_mut_ptr().cast::(); + + // Fill the uninitialized 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__VecU16() } as usize; + if SIZE != c_size { + FAILED.store(true, Ordering::Relaxed); + eprintln!( + "size of `struct VecU16` is {c_size} in C and {SIZE} in Rust\n", + ); + return; + } + + let mut c_value_bytes = vec![0; size_of::()]; + let r: U = unsafe { + ctest_roundtrip__VecU16(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 `VecU16` -> 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::().add(i).read_volatile() as usize }; + if rust != c { + eprintln!( + "rust [{i}] = {rust} != {c} (C): C `VecU16` -> Rust", + ); + FAILED.store(true, Ordering::Relaxed); + } + } + } +} + +use generated_tests::*; + +fn main() { + println!("RUNNING ALL TESTS"); + run_all(); + if FAILED.load(std::sync::atomic::Ordering::Relaxed) { + panic!("some tests failed"); + } else { + println!( + "PASSED {} tests", + NTESTS.load(std::sync::atomic::Ordering::Relaxed) + ); + } +} + +// Run all tests by calling the functions that define them. +// FIXME(ctest): Maybe consider running the tests in parallel, since everything is independent +// and we already use atomics. +fn run_all() { + ctest_size_align_VecU8(); + ctest_size_align_VecU16(); + ctest_field_size_offset_VecU8_x(); + ctest_field_size_offset_VecU8_y(); + ctest_field_size_offset_VecU16_x(); + ctest_field_size_offset_VecU16_y(); + ctest_field_ptr_VecU8_x(); + ctest_field_ptr_VecU8_y(); + ctest_field_ptr_VecU16_x(); + ctest_field_ptr_VecU16_y(); + ctest_roundtrip_VecU8(); + ctest_roundtrip_VecU16(); +} diff --git a/ctest/tests/input/simple.out.with-renames.rs b/ctest/tests/input/simple.out.with-renames.rs index 1adb33766e337..c5c012d1940ba 100644 --- a/ctest/tests/input/simple.out.with-renames.rs +++ b/ctest/tests/input/simple.out.with-renames.rs @@ -291,7 +291,7 @@ mod generated_tests { /// this would result in a value larger than zero. For signed types, this results in a value /// smaller than 0. pub fn ctest_signededness_Byte() { - extern "C" { + extern "C" { fn ctest_signededness_of__Byte() -> u32; } let all_ones = !(0 as Byte); @@ -307,7 +307,7 @@ mod generated_tests { /// this would result in a value larger than zero. For signed types, this results in a value /// smaller than 0. pub fn ctest_signededness_volatile_char() { - extern "C" { + extern "C" { fn ctest_signededness_of__volatile_char() -> u32; } let all_ones = !(0 as volatile_char); diff --git a/ctest/tests/input/simple.out.with-skips.rs b/ctest/tests/input/simple.out.with-skips.rs index e44936fbbea37..1177b55f10c86 100644 --- a/ctest/tests/input/simple.out.with-skips.rs +++ b/ctest/tests/input/simple.out.with-skips.rs @@ -88,7 +88,7 @@ mod generated_tests { /// this would result in a value larger than zero. For signed types, this results in a value /// smaller than 0. pub fn ctest_signededness_volatile_char() { - extern "C" { + extern "C" { fn ctest_signededness_of__volatile_char() -> u32; } let all_ones = !(0 as volatile_char);