Skip to content
Open
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
21 changes: 21 additions & 0 deletions ctest/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Fn(&MapInput) -> Option<String>>;
Expand Down Expand Up @@ -73,6 +76,8 @@ pub struct TestGenerator {
pub(crate) skip_roundtrip: Option<SkipTest>,
pub(crate) skip_signededness: Option<SkipTest>,
pub(crate) skip_fn_ptrcheck: Option<SkipTest>,
/// The Rust edition to generate code against.
pub(crate) edition: Option<u32>,
}

/// An error that occurs when generating the test files.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
17 changes: 17 additions & 0 deletions ctest/src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use crate::{
#[template(path = "test.rs")]
pub(crate) struct RustTestTemplate {
pub template: TestTemplate,
pub extern_keyword: BoxStr,
}

impl RustTestTemplate {
Expand All @@ -36,8 +37,24 @@ impl RustTestTemplate {
) -> Result<Self, TranslationError> {
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.
Expand Down
19 changes: 10 additions & 9 deletions ctest/templates/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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;
}
Expand All @@ -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 }});
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<U>, is_padding_byte: *const bool, value_bytes: *mut u8
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down
14 changes: 14 additions & 0 deletions ctest/tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion ctest/tests/input/hierarchy.out.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
195 changes: 195 additions & 0 deletions ctest/tests/input/macro.out.edition-2024.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/* This file was autogenerated by ctest; do not modify directly */

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#define SUPPRESS_ERROR
#include <macro.h>
#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
Loading