diff --git a/ctest-next/src/ast/mod.rs b/ctest-next/src/ast/mod.rs index 37ad3345e40e8..10fecbcea086b 100644 --- a/ctest-next/src/ast/mod.rs +++ b/ctest-next/src/ast/mod.rs @@ -7,6 +7,8 @@ mod structure; mod type_alias; mod union; +use std::fmt; + pub use constant::Const; pub use field::Field; pub use function::Fn; @@ -37,6 +39,16 @@ impl From<&str> for Abi { } } +impl fmt::Display for Abi { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Abi::C => write!(f, "C"), + Abi::Rust => write!(f, "Rust"), + Abi::Other(s) => write!(f, "{s}"), + } + } +} + /// Things that can appear directly inside of a module or scope. /// /// This is not an exhaustive list and only contains variants directly useful @@ -47,7 +59,7 @@ pub(crate) enum Item { /// Represents a constant defined in Rust. Const(Const), /// Represents a function defined in Rust. - Fn(Fn), + Fn(Box), /// Represents a static variable defined in Rust. Static(Static), /// Represents a type alias defined in Rust. diff --git a/ctest-next/src/ffi_items.rs b/ctest-next/src/ffi_items.rs index 8bb0b9ec3272d..a3758c54b3a1d 100644 --- a/ctest-next/src/ffi_items.rs +++ b/ctest-next/src/ffi_items.rs @@ -57,7 +57,6 @@ impl FfiItems { } /// Return a list of all foreign functions found mapped by their ABI. - #[cfg_attr(not(test), expect(unused))] pub(crate) fn foreign_functions(&self) -> &Vec { &self.foreign_functions } diff --git a/ctest-next/src/generator.rs b/ctest-next/src/generator.rs index a9177a7aed284..89d45bcc99e26 100644 --- a/ctest-next/src/generator.rs +++ b/ctest-next/src/generator.rs @@ -42,10 +42,11 @@ pub struct TestGenerator { pub(crate) skips: Vec, verbose_skip: bool, pub(crate) volatile_items: Vec, - array_arg: Option, + pub(crate) array_arg: Option, skip_private: bool, pub(crate) skip_roundtrip: Option, pub(crate) skip_signededness: Option, + pub(crate) skip_fn_ptrcheck: Option, } #[derive(Debug, Error)] @@ -856,6 +857,30 @@ impl TestGenerator { self } + /// Configures whether tests for a function pointer's value are generated. + /// + /// The closure is given a Rust FFI function and returns whether + /// the test will be generated. + /// + /// By default generated tests will ensure that the function pointer in C + /// corresponds to the same function pointer in Rust. This can often + /// uncover subtle symbol naming issues where a header file is referenced + /// through the C identifier `foo` but the underlying symbol is mapped to + /// something like `__foo_compat`. + /// + /// # Examples + /// + /// ```no_run + /// use ctest_next::TestGenerator; + /// + /// let mut cfg = TestGenerator::new(); + /// cfg.skip_fn_ptrcheck(|name| name == "T1p"); + /// ``` + pub fn skip_fn_ptrcheck(&mut self, f: impl Fn(&str) -> bool + 'static) -> &mut Self { + self.skip_fn_ptrcheck = Some(Box::new(f)); + self + } + /// Generate the Rust and C testing files. /// /// Returns the path to the generated file. diff --git a/ctest-next/src/lib.rs b/ctest-next/src/lib.rs index 5008003f8f57a..8e4f764b72b70 100644 --- a/ctest-next/src/lib.rs +++ b/ctest-next/src/lib.rs @@ -37,7 +37,7 @@ type BoxStr = Box; /// /// This is necessary because `ctest` does not parse the header file, so it /// does not know which items are volatile. -#[derive(Debug)] +#[derive(Debug, Clone)] #[non_exhaustive] pub enum VolatileItemKind { /// A struct field. diff --git a/ctest-next/src/template.rs b/ctest-next/src/template.rs index 955e862b2cd27..41091b3872bb2 100644 --- a/ctest-next/src/template.rs +++ b/ctest-next/src/template.rs @@ -4,7 +4,7 @@ use quote::ToTokens; use syn::spanned::Spanned; use crate::ffi_items::FfiItems; -use crate::translator::Translator; +use crate::translator::{TranslationErrorKind, Translator}; use crate::{ BoxStr, Field, MapInput, Result, TestGenerator, TranslationError, VolatileItemKind, cdecl, }; @@ -53,6 +53,7 @@ pub(crate) struct TestTemplate { pub field_ptr_tests: Vec, pub field_size_offset_tests: Vec, pub roundtrip_tests: Vec, + pub foreign_fn_tests: Vec, pub signededness_tests: Vec, pub size_align_tests: Vec, pub const_cstr_tests: Vec, @@ -75,6 +76,7 @@ impl TestTemplate { template.populate_field_size_offset_tests(&helper)?; template.populate_field_ptr_tests(&helper)?; template.populate_roundtrip_tests(&helper)?; + template.populate_foreign_fn_tests(&helper)?; Ok(template) } @@ -359,7 +361,7 @@ impl TestTemplate { ) .map_err(|_| { TranslationError::new( - crate::translator::TranslationErrorKind::InvalidReturn, + TranslationErrorKind::InvalidReturn, &field.ty.to_token_stream().to_string(), field.ty.span(), ) @@ -380,6 +382,38 @@ impl TestTemplate { Ok(()) } + + /// Populates tests for extern functions. + /// + /// It also keeps track of the names of each test. + fn populate_foreign_fn_tests( + &mut self, + helper: &TranslateHelper, + ) -> Result<(), TranslationError> { + let should_skip_fn_test = |ident| { + helper + .generator + .skip_fn_ptrcheck + .as_ref() + .is_some_and(|skip| skip(ident)) + }; + for func in helper.ffi_items.foreign_functions() { + if should_skip_fn_test(func.ident()) { + continue; + } + + let item = TestForeignFn { + test_name: foreign_fn_test_ident(func.ident()), + id: func.ident().into(), + c_val: helper.c_ident(func).into_boxed_str(), + }; + + self.foreign_fn_tests.push(item.clone()); + self.test_idents.push(item.test_name); + } + + Ok(()) + } } /* Many test structures have the following fields: @@ -457,6 +491,13 @@ pub(crate) struct TestRoundtrip { pub is_alias: bool, } +#[derive(Clone, Debug)] +pub(crate) struct TestForeignFn { + pub test_name: BoxStr, + pub c_val: BoxStr, + pub id: BoxStr, +} + fn signededness_test_ident(ident: &str) -> BoxStr { format!("ctest_signededness_{ident}").into() } @@ -485,6 +526,10 @@ fn roundtrip_test_ident(ident: &str) -> BoxStr { format!("ctest_roundtrip_{ident}").into() } +fn foreign_fn_test_ident(ident: &str) -> BoxStr { + format!("ctest_foreign_fn_{ident}").into() +} + /// Wrap methods that depend on both ffi items and the generator. pub(crate) struct TranslateHelper<'a> { ffi_items: &'a FfiItems, @@ -533,7 +578,7 @@ impl<'a> TranslateHelper<'a> { let ty = cdecl::cdecl(&ty, "".to_string()).map_err(|_| { TranslationError::new( - crate::translator::TranslationErrorKind::InvalidReturn, + TranslationErrorKind::InvalidReturn, ident, Span::call_site(), ) diff --git a/ctest-next/src/tests.rs b/ctest-next/src/tests.rs index 088f404b69f71..0df50a59c03a7 100644 --- a/ctest-next/src/tests.rs +++ b/ctest-next/src/tests.rs @@ -2,7 +2,7 @@ use syn::spanned::Spanned; use syn::visit::Visit; use crate::ffi_items::FfiItems; -use crate::translator::Translator; +use crate::translator::{TranslationErrorKind, Translator}; use crate::{Result, TestGenerator, TranslationError, cdecl}; const ALL_ITEMS: &str = r#" @@ -44,13 +44,13 @@ fn r2cdecl(s: &str, name: &str) -> Result { let translator = Translator::new(&ffi_items, &generator); let ty: syn::Type = syn::parse_str(s).unwrap(); let translated = translator.translate_type(&ty)?; - cdecl::cdecl(&translated, name.to_string()).map_err(|_| { - TranslationError::new( - crate::translator::TranslationErrorKind::InvalidReturn, - s, - ty.span(), - ) - }) + cdecl::cdecl(&translated, name.to_string()) + .map_err(|_| TranslationError::new(TranslationErrorKind::InvalidReturn, s, ty.span())) +} + +#[track_caller] +fn assert_r2cdecl(rust: &str, expected: &str) { + assert_eq!(r2cdecl(rust, "foo").unwrap(), expected) } #[test] @@ -70,55 +70,37 @@ fn test_extraction_ffi_items() { #[test] fn test_translation_type_ptr() { - assert_eq!( - r2cdecl("*const *mut i32", "").unwrap(), - "int32_t *const *".to_string() - ); - assert_eq!( - r2cdecl("*const [u128; 2 + 3]", "").unwrap(), - "unsigned __int128 (*)[2 + 3]".to_string() - ); - assert_eq!( - r2cdecl("*const *mut [u8; 5]", "").unwrap(), - "uint8_t (*const *)[5]".to_string() - ); + assert_r2cdecl("*const *mut i32", "int32_t *const *foo"); + assert_r2cdecl("*const [u128; 2 + 3]", "unsigned __int128 (*foo)[2 + 3]"); + assert_r2cdecl("*const *mut [u8; 5]", "uint8_t (*const *foo)[5]"); + assert_r2cdecl("*mut *const [u8; 5]", "uint8_t (**foo)[5]"); + assert_r2cdecl("*const *const [u8; 5]", "uint8_t (*const *foo)[5]"); + assert_r2cdecl("*mut *mut [u8; 5]", "uint8_t (**foo)[5]"); } #[test] fn test_translation_type_reference() { - assert_eq!(r2cdecl("&u8", "").unwrap(), "const uint8_t *".to_string()); - assert_eq!( - r2cdecl("&&u8", "").unwrap(), - "const uint8_t *const *".to_string() - ); - assert_eq!( - r2cdecl("*mut &u8", "").unwrap(), - "const uint8_t **".to_string() - ); - assert_eq!( - r2cdecl("& &mut u8", "").unwrap(), - "uint8_t *const *".to_string() - ); + assert_r2cdecl("&u8", "const uint8_t *foo"); + assert_r2cdecl("&&u8", "const uint8_t *const *foo"); + assert_r2cdecl("*mut &u8", "const uint8_t **foo"); + assert_r2cdecl("& &mut u8", "uint8_t *const *foo"); } #[test] fn test_translation_type_bare_fn() { - assert_eq!( - r2cdecl("fn(*mut u8, i16) -> *const char", "").unwrap(), - "const char *(*)(uint8_t *, int16_t)".to_string() + assert_r2cdecl( + "fn(*mut u8, i16) -> *const char", + "const char *(*foo)(uint8_t *, int16_t)", ); - assert_eq!( - r2cdecl("*const fn(*mut u8, &mut [u8; 16]) -> &mut *mut u8", "").unwrap(), - "uint8_t **(*const *)(uint8_t *, uint8_t (*)[16])".to_string() + assert_r2cdecl( + "*const fn(*mut u8, &mut [u8; 16]) -> &mut *mut u8", + "uint8_t **(*const *foo)(uint8_t *, uint8_t (*)[16])", ); } #[test] fn test_translation_type_array() { - assert_eq!( - r2cdecl("[&u8; 2 + 2]", "").unwrap(), - "const uint8_t *[2 + 2]".to_string() - ); + assert_r2cdecl("[&u8; 2 + 2]", "const uint8_t *foo[2 + 2]"); } #[test] @@ -129,25 +111,19 @@ fn test_translation_fails_for_unsupported() { #[test] fn test_translate_helper_function_pointer() { - assert_eq!( - r2cdecl("extern \"C\" fn(c_int) -> *const c_void", "test_make_cdecl").unwrap(), - "const void *(*test_make_cdecl)(int)" + assert_r2cdecl( + "extern \"C\" fn(c_int) -> *const c_void", + "const void *(*foo)(int)", ); // FIXME(ctest): Reimplement support for ABI in a more robust way. - // assert_eq!( - // cdecl("Option u8>").unwrap(), - // "uint8_t (__stdcall **test_make_cdecl)(const char *, uint32_t [16])" + // assert_r2cdecl( + // "Option u8>", + // "uint8_t (__stdcall **foo)(const char *, uint32_t [16])" // ); } #[test] fn test_translate_helper_array_1d_2d() { - assert_eq!( - r2cdecl("[u8; 10]", "test_make_cdecl").unwrap(), - "uint8_t test_make_cdecl[10]", - ); - assert_eq!( - r2cdecl("[[u8; 64]; 32]", "test_make_cdecl").unwrap(), - "uint8_t test_make_cdecl[32][64]" - ); + assert_r2cdecl("[u8; 10]", "uint8_t foo[10]"); + assert_r2cdecl("[[u8; 64]; 32]", "uint8_t foo[32][64]"); } diff --git a/ctest-next/src/translator.rs b/ctest-next/src/translator.rs index c279c40687116..027fa1a2cf3b4 100644 --- a/ctest-next/src/translator.rs +++ b/ctest-next/src/translator.rs @@ -178,7 +178,7 @@ impl<'a> Translator<'a> { } /// Translate a Rust function pointer type to its C equivalent. - fn translate_bare_fn( + pub(crate) fn translate_bare_fn( &self, function: &syn::TypeBareFn, ) -> Result { diff --git a/ctest-next/templates/test.c b/ctest-next/templates/test.c index dfa9b95778b01..3268b96f2ecbc 100644 --- a/ctest-next/templates/test.c +++ b/ctest-next/templates/test.c @@ -13,6 +13,8 @@ #include <{{ header }}> {%- endfor +%} +typedef void (*ctest_void_func)(void); + {%- for const_cstr in ctx.const_cstr_tests +%} static char *ctest_const_{{ const_cstr.id }}_val_static = {{ const_cstr.c_val }}; @@ -118,3 +120,20 @@ ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}({{ item.c_ty }} *b) { #ifdef _MSC_VER # 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 + +{%- for item in ctx.foreign_fn_tests +%} + +ctest_void_func ctest_foreign_fn__{{ item.id }}(void) { + return (ctest_void_func){{ item.c_val }}; +} +{%- endfor +%} + +#ifdef _MSC_VER +# pragma warning(default:4191) +#endif diff --git a/ctest-next/templates/test.rs b/ctest-next/templates/test.rs index fe82f095da7c7..fff33b2f51a6e 100644 --- a/ctest-next/templates/test.rs +++ b/ctest-next/templates/test.rs @@ -314,6 +314,19 @@ mod generated_tests { } } {%- endfor +%} + +{%- for item in ctx.foreign_fn_tests +%} + + /// Check if the Rust and C side function pointers point to the same underlying function. + pub fn {{ item.test_name }}() { + extern "C" { + fn ctest_foreign_fn__{{ item.id }}() -> unsafe extern "C" fn(); + } + let actual = unsafe { ctest_foreign_fn__{{ item.id }}() } as u64; + let expected = {{ item.id }} as u64; + check_same(actual, expected, "{{ item.id }} function pointer"); + } +{%- endfor +%} } use generated_tests::*; diff --git a/ctest-next/tests/input/hierarchy.out.c b/ctest-next/tests/input/hierarchy.out.c index 246e4aef0f751..f224bb7abe39c 100644 --- a/ctest-next/tests/input/hierarchy.out.c +++ b/ctest-next/tests/input/hierarchy.out.c @@ -7,6 +7,8 @@ #include +typedef void (*ctest_void_func)(void); + static bool ctest_const_ON_val_static = ON; // Define a function that returns a pointer to the value of the constant to test. @@ -64,3 +66,17 @@ in6_addr ctest_roundtrip__in6_addr( #ifdef _MSC_VER # 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 + +ctest_void_func ctest_foreign_fn__malloc(void) { + return (ctest_void_func)malloc; +} + +#ifdef _MSC_VER +# pragma warning(default:4191) +#endif diff --git a/ctest-next/tests/input/hierarchy.out.rs b/ctest-next/tests/input/hierarchy.out.rs index 7a528abe8616a..e228e9ef28690 100644 --- a/ctest-next/tests/input/hierarchy.out.rs +++ b/ctest-next/tests/input/hierarchy.out.rs @@ -213,6 +213,16 @@ mod generated_tests { } } } + + /// Check if the Rust and C side function pointers point to the same underlying function. + pub fn ctest_foreign_fn_malloc() { + extern "C" { + fn ctest_foreign_fn__malloc() -> unsafe extern "C" fn(); + } + let actual = unsafe { ctest_foreign_fn__malloc() } as u64; + let expected = malloc as u64; + check_same(actual, expected, "malloc function pointer"); + } } use generated_tests::*; @@ -236,4 +246,5 @@ fn run_all() { ctest_size_align_in6_addr(); ctest_signededness_in6_addr(); ctest_roundtrip_in6_addr(); + ctest_foreign_fn_malloc(); } diff --git a/ctest-next/tests/input/hierarchy/foo.rs b/ctest-next/tests/input/hierarchy/foo.rs index 571b7947c4c52..91727290cfdd3 100644 --- a/ctest-next/tests/input/hierarchy/foo.rs +++ b/ctest-next/tests/input/hierarchy/foo.rs @@ -4,7 +4,7 @@ use std::os::raw::c_void; pub const ON: bool = true; unsafe extern "C" { - fn malloc(size: usize) -> *mut c_void; + pub fn malloc(size: usize) -> *mut c_void; static in6addr_any: in6_addr; } diff --git a/ctest-next/tests/input/macro.out.c b/ctest-next/tests/input/macro.out.c index 7035c85a9dad4..2c035a8aeeae4 100644 --- a/ctest-next/tests/input/macro.out.c +++ b/ctest-next/tests/input/macro.out.c @@ -7,6 +7,8 @@ #include +typedef void (*ctest_void_func)(void); + // Return the size of a type. uint64_t ctest_size_of__VecU8(void) { return sizeof(struct VecU8); } @@ -158,3 +160,13 @@ struct VecU16 ctest_roundtrip__VecU16( #ifdef _MSC_VER # 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 +# pragma warning(default:4191) +#endif diff --git a/ctest-next/tests/input/simple.out.with-renames.c b/ctest-next/tests/input/simple.out.with-renames.c index 2a62d711bfa3a..0d9700f269464 100644 --- a/ctest-next/tests/input/simple.out.with-renames.c +++ b/ctest-next/tests/input/simple.out.with-renames.c @@ -7,6 +7,8 @@ #include +typedef void (*ctest_void_func)(void); + static char *ctest_const_A_val_static = A; // Define a function that returns a pointer to the value of the constant to test. @@ -233,3 +235,13 @@ union Word ctest_roundtrip__Word( #ifdef _MSC_VER # 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 +# pragma warning(default:4191) +#endif diff --git a/ctest-next/tests/input/simple.out.with-skips.c b/ctest-next/tests/input/simple.out.with-skips.c index 28b80cef4f5b7..d6fbfc9204441 100644 --- a/ctest-next/tests/input/simple.out.with-skips.c +++ b/ctest-next/tests/input/simple.out.with-skips.c @@ -7,6 +7,8 @@ #include +typedef void (*ctest_void_func)(void); + static char *ctest_const_A_val_static = A; // Define a function that returns a pointer to the value of the constant to test. @@ -225,3 +227,13 @@ union Word ctest_roundtrip__Word( #ifdef _MSC_VER # 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 +# pragma warning(default:4191) +#endif diff --git a/ctest-test/src/t2.h b/ctest-test/src/t2.h index 2f0b644bd485a..9f99e11a1e79d 100644 --- a/ctest-test/src/t2.h +++ b/ctest-test/src/t2.h @@ -17,8 +17,7 @@ typedef struct { int64_t b; } T2Union; -// FIXME(ctest): Cannot be uncommented until tests for functions are implemented in ctest-next. -// static void T2a(void) {} +static void T2a(void) {} #define T2C 4 #define T2S "a" diff --git a/ctest-test/src/t2.rs b/ctest-test/src/t2.rs index d6b93a021df4d..45eb3339ab84e 100644 --- a/ctest-test/src/t2.rs +++ b/ctest-test/src/t2.rs @@ -31,7 +31,6 @@ i! { pub const T2S: *const c_char = b"b\0".as_ptr().cast(); } -// FIXME(ctest): Cannot be uncommented until tests for functions are implemented in ctest-next. -// extern "C" { -// pub fn T2a(); -// } +extern "C" { + pub fn T2a(); +} diff --git a/ctest-test/tests/all.rs b/ctest-test/tests/all.rs index 1f63e46a8df18..1fd1d380a6c94 100644 --- a/ctest-test/tests/all.rs +++ b/ctest-test/tests/all.rs @@ -69,7 +69,7 @@ fn t2() { "bad field type a of T2Baz", "bad field offset b of T2Baz", "bad field type b of T2Baz", - // "bad T2a function pointer", + "bad T2a function pointer", "bad T2C value at byte 0", // "bad T2S string", "bad T2Union size", @@ -114,7 +114,7 @@ fn t2_cxx() { "bad field type a of T2Baz", "bad field offset b of T2Baz", "bad field type b of T2Baz", - // "bad T2a function pointer", + "bad T2a function pointer", "bad T2C value at byte 0", // "bad T2S string", "bad T2Union size", @@ -159,7 +159,7 @@ fn t2_next() { "bad field type a of T2Baz", "bad field offset b of T2Baz", "bad field type b of T2Baz", - // "bad T2a function pointer", + "bad T2a function pointer", "bad T2C value at byte 0", "bad const T2S string", "bad T2Union size",