Skip to content

ctest: add foreign fn tests #4594

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 11, 2025
Merged
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
14 changes: 13 additions & 1 deletion ctest-next/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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<Fn>),
/// Represents a static variable defined in Rust.
Static(Static),
/// Represents a type alias defined in Rust.
Expand Down
1 change: 0 additions & 1 deletion ctest-next/src/ffi_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Fn> {
&self.foreign_functions
}
Expand Down
27 changes: 26 additions & 1 deletion ctest-next/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ pub struct TestGenerator {
pub(crate) skips: Vec<Skip>,
verbose_skip: bool,
pub(crate) volatile_items: Vec<VolatileItem>,
array_arg: Option<ArrayArg>,
pub(crate) array_arg: Option<ArrayArg>,
skip_private: bool,
pub(crate) skip_roundtrip: Option<SkipTest>,
pub(crate) skip_signededness: Option<SkipTest>,
pub(crate) skip_fn_ptrcheck: Option<SkipTest>,
}

#[derive(Debug, Error)]
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion ctest-next/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type BoxStr = Box<str>;
///
/// 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.
Expand Down
51 changes: 48 additions & 3 deletions ctest-next/src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -53,6 +53,7 @@ pub(crate) struct TestTemplate {
pub field_ptr_tests: Vec<TestFieldPtr>,
pub field_size_offset_tests: Vec<TestFieldSizeOffset>,
pub roundtrip_tests: Vec<TestRoundtrip>,
pub foreign_fn_tests: Vec<TestForeignFn>,
pub signededness_tests: Vec<TestSignededness>,
pub size_align_tests: Vec<TestSizeAlign>,
pub const_cstr_tests: Vec<TestCStr>,
Expand All @@ -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)
}
Expand Down Expand Up @@ -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(),
)
Expand All @@ -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:
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
)
Expand Down
90 changes: 33 additions & 57 deletions ctest-next/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down Expand Up @@ -44,13 +44,13 @@ fn r2cdecl(s: &str, name: &str) -> Result<String, TranslationError> {
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]
Expand All @@ -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]
Expand All @@ -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<extern \"stdcall\" fn(*const c_char, [u32; 16]) -> u8>").unwrap(),
// "uint8_t (__stdcall **test_make_cdecl)(const char *, uint32_t [16])"
// assert_r2cdecl(
// "Option<extern \"stdcall\" fn(*const c_char, [u32; 16]) -> 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]");
}
2 changes: 1 addition & 1 deletion ctest-next/src/translator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<cdecl::CTy, TranslationError> {
Expand Down
19 changes: 19 additions & 0 deletions ctest-next/templates/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 }};
Expand Down Expand Up @@ -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
13 changes: 13 additions & 0 deletions ctest-next/templates/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down
Loading
Loading