Skip to content

Commit 17c3633

Browse files
committed
ctest: add foreign func tests
1 parent 7f3d696 commit 17c3633

File tree

17 files changed

+354
-84
lines changed

17 files changed

+354
-84
lines changed

ctest-next/src/ast/function.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use quote::ToTokens;
2+
13
use crate::{Abi, BoxStr, Parameter};
24

35
/// Represents a function signature defined in Rust.
@@ -6,14 +8,12 @@ use crate::{Abi, BoxStr, Parameter};
68
#[derive(Debug, Clone)]
79
pub struct Fn {
810
pub(crate) public: bool,
9-
#[expect(unused)]
1011
pub(crate) abi: Abi,
1112
pub(crate) ident: BoxStr,
1213
pub(crate) link_name: Option<BoxStr>,
13-
#[expect(unused)]
1414
pub(crate) parameters: Vec<Parameter>,
15-
#[expect(unused)]
1615
pub(crate) return_type: Option<syn::Type>,
16+
pub(crate) bare_fn_signature: syn::Signature,
1717
}
1818

1919
impl Fn {
@@ -26,4 +26,9 @@ impl Fn {
2626
pub fn link_name(&self) -> Option<&str> {
2727
self.link_name.as_deref()
2828
}
29+
30+
/// Returns the signature of the function as a bare function type.
31+
pub fn signature(&self) -> String {
32+
self.bare_fn_signature.to_token_stream().to_string()
33+
}
2934
}

ctest-next/src/ast/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ mod structure;
77
mod type_alias;
88
mod union;
99

10+
use std::fmt;
11+
1012
pub use constant::Const;
1113
pub use field::Field;
1214
pub use function::Fn;
@@ -37,6 +39,16 @@ impl From<&str> for Abi {
3739
}
3840
}
3941

42+
impl fmt::Display for Abi {
43+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44+
match self {
45+
Abi::C => write!(f, "C"),
46+
Abi::Rust => write!(f, "Rust"),
47+
Abi::Other(s) => write!(f, "{s}"),
48+
}
49+
}
50+
}
51+
4052
/// Things that can appear directly inside of a module or scope.
4153
///
4254
/// This is not an exhaustive list and only contains variants directly useful
@@ -47,7 +59,7 @@ pub(crate) enum Item {
4759
/// Represents a constant defined in Rust.
4860
Const(Const),
4961
/// Represents a function defined in Rust.
50-
Fn(Fn),
62+
Fn(Box<Fn>),
5163
/// Represents a static variable defined in Rust.
5264
Static(Static),
5365
/// Represents a type alias defined in Rust.

ctest-next/src/cdecl.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ pub(crate) enum CTy {
3939
}
4040

4141
impl CTy {
42+
/// Walk through the C type and optionally modify each node.
43+
pub(crate) fn walk_mut(&mut self, mut f: impl FnMut(&mut Self)) {
44+
let mut head = self;
45+
loop {
46+
f(head);
47+
match head {
48+
Self::Named { .. } => break,
49+
Self::Ptr { ty, .. } => head = ty,
50+
Self::Array { ty, .. } => head = ty,
51+
Self::Fn { ret, .. } => head = ret,
52+
}
53+
}
54+
}
55+
4256
/// Validate that we aren't returning an array or a function without indirection, which isn't
4357
/// allowed in C.
4458
fn check_ret_ty(&self) -> Result<(), InvalidReturn> {

ctest-next/src/ffi_items.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ impl FfiItems {
5757
}
5858

5959
/// Return a list of all foreign functions found mapped by their ABI.
60-
#[cfg_attr(not(test), expect(unused))]
6160
pub(crate) fn foreign_functions(&self) -> &Vec<Fn> {
6261
&self.foreign_functions
6362
}
@@ -142,6 +141,7 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi
142141
syn::ReturnType::Type(_, ty) => Some(ty.deref().clone()),
143142
};
144143
let link_name = extract_single_link_name(&i.attrs);
144+
let bare_fn_signature = i.sig.clone();
145145

146146
table.foreign_functions.push(Fn {
147147
public,
@@ -150,6 +150,7 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi
150150
link_name,
151151
parameters,
152152
return_type,
153+
bare_fn_signature,
153154
});
154155
}
155156

ctest-next/src/generator.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ pub struct TestGenerator {
4242
pub(crate) skips: Vec<Skip>,
4343
verbose_skip: bool,
4444
pub(crate) volatile_items: Vec<VolatileItem>,
45-
array_arg: Option<ArrayArg>,
45+
pub(crate) array_arg: Option<ArrayArg>,
4646
skip_private: bool,
4747
pub(crate) skip_roundtrip: Option<SkipTest>,
4848
pub(crate) skip_signededness: Option<SkipTest>,
49+
pub(crate) skip_fn_ptrcheck: Option<SkipTest>,
4950
}
5051

5152
#[derive(Debug, Error)]
@@ -856,6 +857,30 @@ impl TestGenerator {
856857
self
857858
}
858859

860+
/// Configures whether tests for a function pointer's value are generated.
861+
///
862+
/// The closure is given a Rust FFI function and returns whether
863+
/// the test will be generated.
864+
///
865+
/// By default generated tests will ensure that the function pointer in C
866+
/// corresponds to the same function pointer in Rust. This can often
867+
/// uncover subtle symbol naming issues where a header file is referenced
868+
/// through the C identifier `foo` but the underlying symbol is mapped to
869+
/// something like `__foo_compat`.
870+
///
871+
/// # Examples
872+
///
873+
/// ```no_run
874+
/// use ctest_next::TestGenerator;
875+
///
876+
/// let mut cfg = TestGenerator::new();
877+
/// cfg.skip_fn_ptrcheck(|name| name == "T1p");
878+
/// ```
879+
pub fn skip_fn_ptrcheck(&mut self, f: impl Fn(&str) -> bool + 'static) -> &mut Self {
880+
self.skip_fn_ptrcheck = Some(Box::new(f));
881+
self
882+
}
883+
859884
/// Generate the Rust and C testing files.
860885
///
861886
/// Returns the path to the generated file.

ctest-next/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type BoxStr = Box<str>;
3737
///
3838
/// This is necessary because `ctest` does not parse the header file, so it
3939
/// does not know which items are volatile.
40-
#[derive(Debug)]
40+
#[derive(Debug, Clone)]
4141
#[non_exhaustive]
4242
pub enum VolatileItemKind {
4343
/// A struct field.

ctest-next/src/template.rs

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
use std::ops::Deref;
2+
13
use askama::Template;
24
use proc_macro2::Span;
35
use quote::ToTokens;
46
use syn::spanned::Spanned;
57

68
use crate::ffi_items::FfiItems;
7-
use crate::translator::Translator;
9+
use crate::translator::{TranslationErrorKind, Translator};
810
use crate::{
911
BoxStr, Field, MapInput, Result, TestGenerator, TranslationError, VolatileItemKind, cdecl,
1012
};
@@ -53,6 +55,7 @@ pub(crate) struct TestTemplate {
5355
pub field_ptr_tests: Vec<TestFieldPtr>,
5456
pub field_size_offset_tests: Vec<TestFieldSizeOffset>,
5557
pub roundtrip_tests: Vec<TestRoundtrip>,
58+
pub foreign_fn_tests: Vec<TestForeignFn>,
5659
pub signededness_tests: Vec<TestSignededness>,
5760
pub size_align_tests: Vec<TestSizeAlign>,
5861
pub const_cstr_tests: Vec<TestCStr>,
@@ -75,6 +78,7 @@ impl TestTemplate {
7578
template.populate_field_size_offset_tests(&helper)?;
7679
template.populate_field_ptr_tests(&helper)?;
7780
template.populate_roundtrip_tests(&helper)?;
81+
template.populate_foreign_fn_tests(&helper)?;
7882

7983
Ok(template)
8084
}
@@ -359,7 +363,7 @@ impl TestTemplate {
359363
)
360364
.map_err(|_| {
361365
TranslationError::new(
362-
crate::translator::TranslationErrorKind::InvalidReturn,
366+
TranslationErrorKind::InvalidReturn,
363367
&field.ty.to_token_stream().to_string(),
364368
field.ty.span(),
365369
)
@@ -380,6 +384,54 @@ impl TestTemplate {
380384

381385
Ok(())
382386
}
387+
388+
/// Populates tests for extern functions.
389+
///
390+
/// It also keeps track of the names of each test.
391+
fn populate_foreign_fn_tests(
392+
&mut self,
393+
helper: &TranslateHelper,
394+
) -> Result<(), TranslationError> {
395+
let should_skip_fn_test = |ident| {
396+
helper
397+
.generator
398+
.skip_fn_ptrcheck
399+
.as_ref()
400+
.is_some_and(|skip| skip(ident))
401+
};
402+
for func in helper.ffi_items.foreign_functions() {
403+
if should_skip_fn_test(func.ident()) {
404+
continue;
405+
}
406+
407+
let signature =
408+
helper.parse_signature_to_bare_fn(&func.signature(), &func.abi.to_string())?;
409+
let bare_fn = helper
410+
.translator
411+
.translate_bare_fn(&signature, func.ident())?;
412+
let return_type =
413+
cdecl::cdecl(&bare_fn, format!("ctest_foreign_fn_type__{}", func.ident()))
414+
.map_err(|_| {
415+
TranslationError::new(
416+
TranslationErrorKind::InvalidReturn,
417+
func.ident(),
418+
func.return_type.span(),
419+
)
420+
})?;
421+
422+
let item = TestForeignFn {
423+
test_name: foreign_fn_test_ident(func.ident()),
424+
id: func.ident().into(),
425+
c_val: helper.c_ident(func).into_boxed_str(),
426+
return_type: return_type.into(),
427+
};
428+
429+
self.foreign_fn_tests.push(item.clone());
430+
self.test_idents.push(item.test_name);
431+
}
432+
433+
Ok(())
434+
}
383435
}
384436

385437
/* Many test structures have the following fields:
@@ -457,6 +509,14 @@ pub(crate) struct TestRoundtrip {
457509
pub is_alias: bool,
458510
}
459511

512+
#[derive(Clone, Debug)]
513+
pub(crate) struct TestForeignFn {
514+
pub test_name: BoxStr,
515+
pub c_val: BoxStr,
516+
pub id: BoxStr,
517+
pub return_type: BoxStr,
518+
}
519+
460520
fn signededness_test_ident(ident: &str) -> BoxStr {
461521
format!("ctest_signededness_{ident}").into()
462522
}
@@ -485,6 +545,10 @@ fn roundtrip_test_ident(ident: &str) -> BoxStr {
485545
format!("ctest_roundtrip_{ident}").into()
486546
}
487547

548+
fn foreign_fn_test_ident(ident: &str) -> BoxStr {
549+
format!("ctest_foreign_fn_{ident}").into()
550+
}
551+
488552
/// Wrap methods that depend on both ffi items and the generator.
489553
pub(crate) struct TranslateHelper<'a> {
490554
ffi_items: &'a FfiItems,
@@ -533,7 +597,7 @@ impl<'a> TranslateHelper<'a> {
533597

534598
let ty = cdecl::cdecl(&ty, "".to_string()).map_err(|_| {
535599
TranslationError::new(
536-
crate::translator::TranslationErrorKind::InvalidReturn,
600+
TranslationErrorKind::InvalidReturn,
537601
ident,
538602
Span::call_site(),
539603
)
@@ -549,4 +613,30 @@ impl<'a> TranslateHelper<'a> {
549613

550614
Ok(self.generator.rty_to_cty(item))
551615
}
616+
617+
/// Translate a proper function into a bare function type.
618+
///
619+
/// Actual functions foreign or not do not have a type to use, however they can be
620+
/// modelled as a bare function type. This method converts them into that bare function
621+
/// type so that it can be used later for translation.
622+
pub(crate) fn parse_signature_to_bare_fn(
623+
&self,
624+
signature: &str,
625+
abi: &str,
626+
) -> Result<syn::TypeBareFn, TranslationError> {
627+
// Transform `fn name() -> ...` to `fn() -> ...`.
628+
let (_, s) = signature.split_once('(').unwrap();
629+
let type_str = format!("type T = unsafe extern \"{abi}\" fn({s};");
630+
let item: syn::ItemType = syn::parse_str(&type_str).map_err(|_| {
631+
TranslationError::new(
632+
TranslationErrorKind::UnsupportedType,
633+
signature,
634+
Span::call_site(),
635+
)
636+
})?;
637+
if let syn::Type::BareFn(f) = item.ty.deref() {
638+
return Ok(f.clone());
639+
}
640+
unreachable!("`parse_signature_to_bare_fn` returned a non function type.")
641+
}
552642
}

0 commit comments

Comments
 (0)