diff --git a/ctest-next/Cargo.toml b/ctest-next/Cargo.toml index 54c62d05be6ea..556d6d05f9ea5 100644 --- a/ctest-next/Cargo.toml +++ b/ctest-next/Cargo.toml @@ -9,4 +9,4 @@ publish = false [dependencies] cc = "1.2.25" -syn = { version = "2.0.101", features = ["full", "visit", "visit-mut", "fold"] } +syn = { version = "2.0.101", features = ["full", "visit", "extra-traits"] } diff --git a/ctest-next/src/ast/constant.rs b/ctest-next/src/ast/constant.rs new file mode 100644 index 0000000000000..c499994dd1c74 --- /dev/null +++ b/ctest-next/src/ast/constant.rs @@ -0,0 +1,20 @@ +use crate::BoxStr; + +/// Represents a constant variable defined in Rust. +#[derive(Debug, Clone)] +pub struct Const { + #[expect(unused)] + pub(crate) public: bool, + pub(crate) ident: BoxStr, + #[expect(unused)] + pub(crate) ty: syn::Type, + #[expect(unused)] + pub(crate) expr: syn::Expr, +} + +impl Const { + /// Return the identifier of the constant as a string. + pub fn ident(&self) -> &str { + &self.ident + } +} diff --git a/ctest-next/src/ast/field.rs b/ctest-next/src/ast/field.rs new file mode 100644 index 0000000000000..9470f3c3aa036 --- /dev/null +++ b/ctest-next/src/ast/field.rs @@ -0,0 +1,18 @@ +use crate::BoxStr; + +/// Represents a field in a struct or union defined in Rust. +#[derive(Debug, Clone)] +pub struct Field { + #[expect(unused)] + pub(crate) public: bool, + pub(crate) ident: BoxStr, + #[expect(unused)] + pub(crate) ty: syn::Type, +} + +impl Field { + /// Return the identifier of the field as a string if it exists. + pub fn ident(&self) -> &str { + &self.ident + } +} diff --git a/ctest-next/src/ast/function.rs b/ctest-next/src/ast/function.rs new file mode 100644 index 0000000000000..ac41c702e5489 --- /dev/null +++ b/ctest-next/src/ast/function.rs @@ -0,0 +1,24 @@ +use crate::{Abi, BoxStr, Parameter}; + +/// Represents a function signature defined in Rust. +/// +/// This structure is only used for parsing functions in extern blocks. +#[derive(Debug, Clone)] +pub struct Fn { + #[expect(unused)] + pub(crate) public: bool, + #[expect(unused)] + pub(crate) abi: Abi, + pub(crate) ident: BoxStr, + #[expect(unused)] + pub(crate) parameters: Vec, + #[expect(unused)] + pub(crate) return_type: Option, +} + +impl Fn { + /// Return the identifier of the function as a string. + pub fn ident(&self) -> &str { + &self.ident + } +} diff --git a/ctest-next/src/ast/mod.rs b/ctest-next/src/ast/mod.rs new file mode 100644 index 0000000000000..37ad3345e40e8 --- /dev/null +++ b/ctest-next/src/ast/mod.rs @@ -0,0 +1,59 @@ +mod constant; +mod field; +mod function; +mod parameter; +mod static_variable; +mod structure; +mod type_alias; +mod union; + +pub use constant::Const; +pub use field::Field; +pub use function::Fn; +pub use parameter::Parameter; +pub use static_variable::Static; +pub use structure::Struct; +pub use type_alias::Type; +pub use union::Union; + +/// The ABI as defined by the extern block. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Abi { + /// The C ABI. + C, + /// The Rust ABI. + Rust, + /// Any other ABI. + Other(String), +} + +impl From<&str> for Abi { + fn from(value: &str) -> Self { + match value.to_lowercase().as_str() { + "c" => Abi::C, + "rust" => Abi::Rust, + s => Abi::Other(s.to_string()), + } + } +} + +/// Things that can appear directly inside of a module or scope. +/// +/// This is not an exhaustive list and only contains variants directly useful +/// for our purposes. +#[derive(Debug, Clone)] +#[expect(unused)] +pub(crate) enum Item { + /// Represents a constant defined in Rust. + Const(Const), + /// Represents a function defined in Rust. + Fn(Fn), + /// Represents a static variable defined in Rust. + Static(Static), + /// Represents a type alias defined in Rust. + Type(Type), + /// Represents a struct defined in Rust. + Struct(Struct), + /// Represents a union defined in Rust. + Union(Union), +} diff --git a/ctest-next/src/ast/parameter.rs b/ctest-next/src/ast/parameter.rs new file mode 100644 index 0000000000000..edf879a13c175 --- /dev/null +++ b/ctest-next/src/ast/parameter.rs @@ -0,0 +1,8 @@ +/// Represents a parameter in a function signature defined in Rust. +#[derive(Debug, Clone)] +pub struct Parameter { + #[expect(unused)] + pub(crate) pattern: syn::Pat, + #[expect(unused)] + pub(crate) ty: syn::Type, +} diff --git a/ctest-next/src/ast/static_variable.rs b/ctest-next/src/ast/static_variable.rs new file mode 100644 index 0000000000000..792ce015d582c --- /dev/null +++ b/ctest-next/src/ast/static_variable.rs @@ -0,0 +1,23 @@ +use crate::{Abi, BoxStr}; + +/// Represents a static variable in Rust. +/// +/// This structure is only used for parsing statics in extern blocks, +/// as a result it does not have a field for storing the expression. +#[derive(Debug, Clone)] +pub struct Static { + #[expect(unused)] + pub(crate) public: bool, + #[expect(unused)] + pub(crate) abi: Abi, + pub(crate) ident: BoxStr, + #[expect(unused)] + pub(crate) ty: syn::Type, +} + +impl Static { + /// Return the identifier of the static variable as a string. + pub fn ident(&self) -> &str { + &self.ident + } +} diff --git a/ctest-next/src/ast/structure.rs b/ctest-next/src/ast/structure.rs new file mode 100644 index 0000000000000..647f9e7053201 --- /dev/null +++ b/ctest-next/src/ast/structure.rs @@ -0,0 +1,18 @@ +use crate::{BoxStr, Field}; + +/// Represents a struct defined in Rust. +#[derive(Debug, Clone)] +pub struct Struct { + #[expect(unused)] + pub(crate) public: bool, + pub(crate) ident: BoxStr, + #[expect(unused)] + pub(crate) fields: Vec, +} + +impl Struct { + /// Return the identifier of the struct as a string. + pub fn ident(&self) -> &str { + &self.ident + } +} diff --git a/ctest-next/src/ast/type_alias.rs b/ctest-next/src/ast/type_alias.rs new file mode 100644 index 0000000000000..463ef0f97e5c8 --- /dev/null +++ b/ctest-next/src/ast/type_alias.rs @@ -0,0 +1,18 @@ +use crate::BoxStr; + +/// Represents a type alias defined in Rust. +#[derive(Debug, Clone)] +pub struct Type { + #[expect(unused)] + pub(crate) public: bool, + pub(crate) ident: BoxStr, + #[expect(unused)] + pub(crate) ty: syn::Type, +} + +impl Type { + /// Return the identifier of the type alias as a string. + pub fn ident(&self) -> &str { + &self.ident + } +} diff --git a/ctest-next/src/ast/union.rs b/ctest-next/src/ast/union.rs new file mode 100644 index 0000000000000..caf0e30eb95a7 --- /dev/null +++ b/ctest-next/src/ast/union.rs @@ -0,0 +1,18 @@ +use crate::{BoxStr, Field}; + +/// Represents a union defined in Rust. +#[derive(Debug, Clone)] +pub struct Union { + #[expect(unused)] + pub(crate) public: bool, + pub(crate) ident: BoxStr, + #[expect(unused)] + pub(crate) fields: Vec, +} + +impl Union { + /// Return the identifier of the union as a string. + pub fn ident(&self) -> &str { + &self.ident + } +} diff --git a/ctest-next/src/ffi_items.rs b/ctest-next/src/ffi_items.rs new file mode 100644 index 0000000000000..9a1948b8cbb39 --- /dev/null +++ b/ctest-next/src/ffi_items.rs @@ -0,0 +1,219 @@ +use std::ops::Deref; + +use syn::{punctuated::Punctuated, visit::Visit}; + +use crate::{Abi, Const, Field, Fn, Parameter, Static, Struct, Type, Union}; + +/// Represents a collected set of top-level Rust items relevant to FFI generation or analysis. +/// +/// Includes foreign functions/statics, type aliases, structs, unions, and constants. +#[derive(Default, Clone, Debug)] +pub(crate) struct FfiItems { + aliases: Vec, + structs: Vec, + unions: Vec, + constants: Vec, + foreign_functions: Vec, + foreign_statics: Vec, +} + +impl FfiItems { + /// Creates a new blank FfiItems. + pub(crate) fn new() -> Self { + Self::default() + } + + /// Return whether the type has parsed a struct with the given identifier. + #[expect(unused)] + pub(crate) fn contains_struct(&self, ident: &str) -> bool { + self.structs() + .iter() + .any(|structure| structure.ident() == ident) + } + + /// Return whether the type has parsed a union with the given identifier. + #[expect(unused)] + pub(crate) fn contains_union(&self, ident: &str) -> bool { + self.unions().iter().any(|union| union.ident() == ident) + } + + /// Return a list of all type aliases found. + #[cfg_attr(not(test), expect(unused))] + pub(crate) fn aliases(&self) -> &Vec { + &self.aliases + } + + /// Return a list of all structs found. + pub(crate) fn structs(&self) -> &Vec { + &self.structs + } + + /// Return a list of all unions found. + pub(crate) fn unions(&self) -> &Vec { + &self.unions + } + + /// Return a list of all constants found. + #[cfg_attr(not(test), expect(unused))] + pub(crate) fn constants(&self) -> &Vec { + &self.constants + } + + /// 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 + } + + /// Return a list of all foreign statics found mapped by their ABI. + #[cfg_attr(not(test), expect(unused))] + pub(crate) fn foreign_statics(&self) -> &Vec { + &self.foreign_statics + } +} + +/// Determine whether an item is visible to other crates. +/// +/// This function assumes that if the visibility is restricted then it is not +/// meant to be accessed. +fn is_visible(vis: &syn::Visibility) -> bool { + match vis { + syn::Visibility::Public(_) => true, + syn::Visibility::Inherited | syn::Visibility::Restricted(_) => false, + } +} + +/// Collect fields in a syn grammar into ctest's equivalent structure. +fn collect_fields(fields: &Punctuated) -> Vec { + fields + .iter() + .filter_map(|field| { + field.ident.as_ref().map(|ident| Field { + public: is_visible(&field.vis), + ident: ident.to_string().into_boxed_str(), + ty: field.ty.clone(), + }) + }) + .collect() +} + +fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi) { + let public = is_visible(&i.vis); + let abi = abi.clone(); + let ident = i.sig.ident.to_string().into_boxed_str(); + let parameters = i + .sig + .inputs + .iter() + .map(|arg| match arg { + syn::FnArg::Typed(arg) => Parameter { + pattern: arg.pat.deref().clone(), + ty: arg.ty.deref().clone(), + }, + syn::FnArg::Receiver(_) => { + unreachable!("Foreign functions can't have self/receiver parameters.") + } + }) + .collect::>(); + let return_type = match &i.sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, ty) => Some(ty.deref().clone()), + }; + + table.foreign_functions.push(Fn { + public, + abi, + ident, + parameters, + return_type, + }); +} + +fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, abi: &Abi) { + let public = is_visible(&i.vis); + let abi = abi.clone(); + let ident = i.ident.to_string().into_boxed_str(); + let ty = i.ty.deref().clone(); + + table.foreign_statics.push(Static { + public, + abi, + ident, + ty, + }); +} + +impl<'ast> Visit<'ast> for FfiItems { + fn visit_item_type(&mut self, i: &'ast syn::ItemType) { + let public = is_visible(&i.vis); + let ty = i.ty.deref().clone(); + let ident = i.ident.to_string().into_boxed_str(); + + self.aliases.push(Type { public, ident, ty }); + } + + fn visit_item_struct(&mut self, i: &'ast syn::ItemStruct) { + let public = is_visible(&i.vis); + let ident = i.ident.to_string().into_boxed_str(); + let fields = match &i.fields { + syn::Fields::Named(fields) => collect_fields(&fields.named), + syn::Fields::Unnamed(fields) => collect_fields(&fields.unnamed), + syn::Fields::Unit => Vec::new(), + }; + + self.structs.push(Struct { + public, + ident, + fields, + }); + } + + fn visit_item_union(&mut self, i: &'ast syn::ItemUnion) { + let public = is_visible(&i.vis); + let ident = i.ident.to_string().into_boxed_str(); + let fields = collect_fields(&i.fields.named); + + self.unions.push(Union { + public, + ident, + fields, + }); + } + + fn visit_item_const(&mut self, i: &'ast syn::ItemConst) { + let public = is_visible(&i.vis); + let ident = i.ident.to_string().into_boxed_str(); + let ty = i.ty.deref().clone(); + let expr = i.expr.deref().clone(); + + self.constants.push(Const { + public, + ident, + ty, + expr, + }); + } + + fn visit_item_foreign_mod(&mut self, i: &'ast syn::ItemForeignMod) { + // Because we need to store the ABI we can't directly visit the foreign + // functions/statics. + + // Since this is an extern block, assume extern "C" by default. + let abi = i + .abi + .name + .clone() + .map(|s| Abi::from(s.value().as_str())) + .unwrap_or_else(|| Abi::C); + + for item in &i.items { + match item { + syn::ForeignItem::Fn(function) => visit_foreign_item_fn(self, function, &abi), + syn::ForeignItem::Static(static_variable) => { + visit_foreign_item_static(self, static_variable, &abi) + } + _ => (), + } + } + } +} diff --git a/ctest-next/src/generator.rs b/ctest-next/src/generator.rs index b5cc95818e251..acfcd1e76370a 100644 --- a/ctest-next/src/generator.rs +++ b/ctest-next/src/generator.rs @@ -1,26 +1,28 @@ use std::path::Path; -use crate::{expand, Result}; +use syn::visit::Visit; + +use crate::{expand, ffi_items::FfiItems, Result}; /// A builder used to generate a test suite. #[non_exhaustive] +#[derive(Default, Debug, Clone)] pub struct TestGenerator {} -impl Default for TestGenerator { - fn default() -> Self { - Self::new() - } -} - impl TestGenerator { /// Creates a new blank test generator. pub fn new() -> Self { - Self {} + Self::default() } /// Generate all tests for the given crate and output the Rust side to a file. - pub fn generate>(&self, crate_path: P, _output_file_path: P) -> Result<()> { - let _expanded = expand(crate_path)?; + pub fn generate>(&mut self, crate_path: P, _output_file_path: P) -> Result<()> { + let expanded = expand(crate_path)?; + let ast = syn::parse_file(&expanded)?; + + let mut ffi_items = FfiItems::new(); + ffi_items.visit_file(&ast); + Ok(()) } } diff --git a/ctest-next/src/lib.rs b/ctest-next/src/lib.rs index 37ea15946b8e9..bc4e5f3375586 100644 --- a/ctest-next/src/lib.rs +++ b/ctest-next/src/lib.rs @@ -1,5 +1,6 @@ #![warn(missing_docs)] #![warn(unreachable_pub)] +#![warn(missing_debug_implementations)] //! # ctest2 - an FFI binding validator //! @@ -7,9 +8,15 @@ //! project from the main repo to generate tests which can be used to validate //! FFI bindings in Rust against the headers from which they come from. +#[cfg(test)] +mod tests; + +mod ast; +mod ffi_items; mod generator; mod macro_expansion; +pub use ast::{Abi, Const, Field, Fn, Parameter, Static, Struct, Type, Union}; pub use generator::TestGenerator; pub use macro_expansion::expand; @@ -17,3 +24,5 @@ pub use macro_expansion::expand; pub type Error = Box; /// A type alias for `std::result::Result` that defaults to our error type. pub type Result = std::result::Result; +/// A boxed string for representing identifiers. +type BoxStr = Box; diff --git a/ctest-next/src/tests.rs b/ctest-next/src/tests.rs new file mode 100644 index 0000000000000..c8e7f25e2d062 --- /dev/null +++ b/ctest-next/src/tests.rs @@ -0,0 +1,91 @@ +use crate::ffi_items::FfiItems; + +use syn::visit::Visit; + +const ALL_ITEMS: &str = r#" +use std::os::raw::c_void; + +mod level1 { + pub type Foo = u8; + + pub const bar: u32 = 512; + + pub union Word { + word: u16, + bytes: [u8; 2], + } +} + +pub struct Array { + ptr: *mut c_void, + len: usize, +} + +extern "C" { + static baz: u16; + + fn malloc(size: usize) -> *mut c_void; +} +"#; + +#[test] +fn test_extraction_ffi_items() { + let ast = syn::parse_file(ALL_ITEMS).unwrap(); + + let mut ffi_items = FfiItems::new(); + ffi_items.visit_file(&ast); + + assert_eq!( + ffi_items + .aliases() + .iter() + .map(|a| a.ident()) + .collect::>(), + ["Foo"] + ); + + assert_eq!( + ffi_items + .constants() + .iter() + .map(|a| a.ident()) + .collect::>(), + ["bar"] + ); + + assert_eq!( + ffi_items + .foreign_functions() + .iter() + .map(|a| a.ident()) + .collect::>(), + ["malloc"] + ); + + assert_eq!( + ffi_items + .foreign_statics() + .iter() + .map(|a| a.ident()) + .collect::>(), + ["baz"] + ); + + assert_eq!( + ffi_items + .structs() + .iter() + .map(|a| a.ident()) + .collect::>(), + ["Array"] + ); + + assert_eq!( + ffi_items + .unions() + .iter() + .map(|a| a.ident()) + .collect::>(), + ["Word"] + ); +} diff --git a/ctest-next/tests/basic.rs b/ctest-next/tests/basic.rs index 1662c0e50d78c..42d9a139d566c 100644 --- a/ctest-next/tests/basic.rs +++ b/ctest-next/tests/basic.rs @@ -2,7 +2,7 @@ use ctest_next::TestGenerator; #[test] fn test_entrypoint_hierarchy() { - let generator = TestGenerator::new(); + let mut generator = TestGenerator::new(); generator .generate("./tests/input/hierarchy/lib.rs", "hierarchy_out.rs") @@ -11,7 +11,7 @@ fn test_entrypoint_hierarchy() { #[test] fn test_entrypoint_simple() { - let generator = TestGenerator::new(); + let mut generator = TestGenerator::new(); generator .generate("./tests/input/simple.rs", "simple_out.rs") @@ -20,7 +20,7 @@ fn test_entrypoint_simple() { #[test] fn test_entrypoint_macro() { - let generator = TestGenerator::new(); + let mut generator = TestGenerator::new(); generator .generate("./tests/input/macro.rs", "macro_out.rs") @@ -29,7 +29,7 @@ fn test_entrypoint_macro() { #[test] fn test_entrypoint_invalid_syntax() { - let generator = TestGenerator::new(); + let mut generator = TestGenerator::new(); let fails = generator .generate("./tests/input/invalid_syntax.rs", "invalid_syntax_out.rs")