diff --git a/ctest-next/Cargo.toml b/ctest-next/Cargo.toml index 54c62d05be6ea..ddbc7e559542c 100644 --- a/ctest-next/Cargo.toml +++ b/ctest-next/Cargo.toml @@ -9,4 +9,6 @@ publish = false [dependencies] cc = "1.2.25" +proc-macro2 = "1.0.95" +quote = "1.0.40" syn = { version = "2.0.101", features = ["full", "visit", "visit-mut", "fold"] } diff --git a/ctest-next/src/cargo_expand.rs b/ctest-next/src/cargo_expand.rs new file mode 100644 index 0000000000000..593b5a9161d12 --- /dev/null +++ b/ctest-next/src/cargo_expand.rs @@ -0,0 +1,16 @@ +use std::{env, error::Error, path::Path, process::Command}; + +/// Use cargo expand to expand all macros and pretty print the crate +/// into a single file. +pub fn expand(crate_path: &Path) -> Result> { + let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); + + let output = Command::new(cargo) + .arg("expand") + .current_dir(crate_path) + .output()?; + + let expanded = std::str::from_utf8(&output.stdout)?.to_string(); + + Ok(expanded) +} diff --git a/ctest-next/src/ir/constant.rs b/ctest-next/src/ir/constant.rs new file mode 100644 index 0000000000000..c497dd6844d25 --- /dev/null +++ b/ctest-next/src/ir/constant.rs @@ -0,0 +1,37 @@ +use proc_macro2::TokenStream; +use syn::Ident; + +#[derive(Debug)] +pub struct Constant { + public: bool, + ident: Ident, + ty: TokenStream, + value: TokenStream, +} + +impl Constant { + pub fn new(public: bool, ident: Ident, ty: TokenStream, value: TokenStream) -> Self { + Self { + public, + ident, + ty, + value, + } + } + + pub fn public(&self) -> bool { + self.public + } + + pub fn ident(&self) -> &Ident { + &self.ident + } + + pub fn ty(&self) -> &TokenStream { + &self.ty + } + + pub fn value(&self) -> &TokenStream { + &self.value + } +} diff --git a/ctest-next/src/ir/field.rs b/ctest-next/src/ir/field.rs new file mode 100644 index 0000000000000..fd7141de6a8de --- /dev/null +++ b/ctest-next/src/ir/field.rs @@ -0,0 +1,27 @@ +use proc_macro2::TokenStream; +use syn::Ident; + +#[derive(Debug)] +pub struct Field { + public: bool, + ident: Option, + ty: TokenStream, +} + +impl Field { + pub fn new(public: bool, ident: Option, ty: TokenStream) -> Self { + Self { public, ident, ty } + } + + pub fn public(&self) -> bool { + self.public + } + + pub fn ident(&self) -> &Option { + &self.ident + } + + pub fn ty(&self) -> &TokenStream { + &self.ty + } +} diff --git a/ctest-next/src/ir/function.rs b/ctest-next/src/ir/function.rs new file mode 100644 index 0000000000000..331a2c72c748a --- /dev/null +++ b/ctest-next/src/ir/function.rs @@ -0,0 +1,44 @@ +use proc_macro2::TokenStream; +use syn::Ident; + +use crate::ir::Parameter; + +#[derive(Debug)] +pub struct Function { + public: bool, + ident: Ident, + parameters: Vec, + return_value: TokenStream, +} + +impl Function { + pub fn new( + public: bool, + ident: Ident, + parameters: Vec, + return_value: TokenStream, + ) -> Self { + Self { + public, + ident, + parameters, + return_value, + } + } + + pub fn public(&self) -> bool { + self.public + } + + pub fn ident(&self) -> &Ident { + &self.ident + } + + pub fn parameters(&self) -> &Vec { + &self.parameters + } + + pub fn return_value(&self) -> &TokenStream { + &self.return_value + } +} diff --git a/ctest-next/src/ir/mod.rs b/ctest-next/src/ir/mod.rs new file mode 100644 index 0000000000000..7511ed3d6eb57 --- /dev/null +++ b/ctest-next/src/ir/mod.rs @@ -0,0 +1,17 @@ +mod constant; +mod field; +mod function; +mod parameter; +mod static_variable; +mod structure; +mod type_alias; +mod union; + +pub use constant::Constant; +pub use field::Field; +pub use function::Function; +pub use parameter::Parameter; +pub use static_variable::Static; +pub use structure::Struct; +pub use type_alias::TypeAlias; +pub use union::Union; diff --git a/ctest-next/src/ir/parameter.rs b/ctest-next/src/ir/parameter.rs new file mode 100644 index 0000000000000..d40c77df007d0 --- /dev/null +++ b/ctest-next/src/ir/parameter.rs @@ -0,0 +1,21 @@ +use proc_macro2::TokenStream; + +#[derive(Debug)] +pub struct Parameter { + pattern: TokenStream, + ty: TokenStream, +} + +impl Parameter { + pub fn new(pattern: TokenStream, ty: TokenStream) -> Self { + Self { pattern, ty } + } + + pub fn pattern(&self) -> &TokenStream { + &self.pattern + } + + pub fn ty(&self) -> &TokenStream { + &self.ty + } +} diff --git a/ctest-next/src/ir/static_variable.rs b/ctest-next/src/ir/static_variable.rs new file mode 100644 index 0000000000000..045c55a6cd5be --- /dev/null +++ b/ctest-next/src/ir/static_variable.rs @@ -0,0 +1,28 @@ +use proc_macro2::TokenStream; +use syn::Ident; + +#[derive(Debug)] +pub struct Static { + public: bool, + ident: Ident, + ty: TokenStream, + // We do not care about the value as we only parse foreign statics. +} + +impl Static { + pub fn new(public: bool, ident: Ident, ty: TokenStream) -> Self { + Self { public, ident, ty } + } + + pub fn public(&self) -> bool { + self.public + } + + pub fn ident(&self) -> &Ident { + &self.ident + } + + pub fn ty(&self) -> &TokenStream { + &self.ty + } +} diff --git a/ctest-next/src/ir/structure.rs b/ctest-next/src/ir/structure.rs new file mode 100644 index 0000000000000..d2fcb2aef9ecd --- /dev/null +++ b/ctest-next/src/ir/structure.rs @@ -0,0 +1,32 @@ +use syn::Ident; + +use crate::ir::Field; + +#[derive(Debug)] +pub struct Struct { + public: bool, + ident: Ident, + fields: Vec, +} + +impl Struct { + pub fn new(public: bool, ident: Ident, fields: Vec) -> Self { + Self { + public, + ident, + fields, + } + } + + pub fn public(&self) -> bool { + self.public + } + + pub fn ident(&self) -> &Ident { + &self.ident + } + + pub fn fields(&self) -> &Vec { + &self.fields + } +} diff --git a/ctest-next/src/ir/type_alias.rs b/ctest-next/src/ir/type_alias.rs new file mode 100644 index 0000000000000..c4c4d31ec3a56 --- /dev/null +++ b/ctest-next/src/ir/type_alias.rs @@ -0,0 +1,27 @@ +use proc_macro2::TokenStream; +use syn::Ident; + +#[derive(Debug)] +pub struct TypeAlias { + public: bool, + ident: Ident, + ty: TokenStream, +} + +impl TypeAlias { + pub fn new(public: bool, ident: Ident, ty: TokenStream) -> Self { + Self { public, ident, ty } + } + + pub fn public(&self) -> bool { + self.public + } + + pub fn ident(&self) -> &Ident { + &self.ident + } + + pub fn ty(&self) -> &TokenStream { + &self.ty + } +} diff --git a/ctest-next/src/ir/union.rs b/ctest-next/src/ir/union.rs new file mode 100644 index 0000000000000..74bcd9f674056 --- /dev/null +++ b/ctest-next/src/ir/union.rs @@ -0,0 +1,32 @@ +use syn::Ident; + +use crate::ir::Field; + +#[derive(Debug)] +pub struct Union { + public: bool, + ident: Ident, + fields: Vec, +} + +impl Union { + pub fn new(public: bool, ident: Ident, fields: Vec) -> Self { + Self { + public, + ident, + fields, + } + } + + pub fn public(&self) -> bool { + self.public + } + + pub fn ident(&self) -> &Ident { + &self.ident + } + + pub fn fields(&self) -> &Vec { + &self.fields + } +} diff --git a/ctest-next/src/lib.rs b/ctest-next/src/lib.rs index 7d12d9af8195b..819a1a48818d8 100644 --- a/ctest-next/src/lib.rs +++ b/ctest-next/src/lib.rs @@ -1,14 +1,10 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} +#![allow(dead_code)] -#[cfg(test)] -mod tests { - use super::*; +mod cargo_expand; +mod ir; +mod skip; +mod symbol_table; +mod translation; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +// TODO: Implement proper error types instead of Box. +// TODO: Add documentation. diff --git a/ctest-next/src/skip.rs b/ctest-next/src/skip.rs new file mode 100644 index 0000000000000..4cd05e2cf6bf7 --- /dev/null +++ b/ctest-next/src/skip.rs @@ -0,0 +1,39 @@ +use crate::ir::{Constant, Field, Function, Static, Struct, TypeAlias, Union}; + +/// A boxed predicate function wrapper for filtering items of type `T`. +pub struct Predicate(Box bool>); + +impl Predicate { + pub fn new(f: F) -> Self + where + F: Fn(T) -> bool + 'static, + { + Self(Box::new(f)) + } + + pub fn test(&self, value: T) -> bool { + (self.0)(value) + } +} + +impl From for Predicate +where + F: Fn(T) -> bool + 'static, +{ + fn from(f: F) -> Self { + Predicate(Box::new(f)) + } +} + +/// Specifies a filter condition for skipping specific kinds of items. +/// +/// To be used as `SkipItem::Struct((|s: Struct| !s.public()).into())` +pub enum SkipItem { + Constant(Predicate), + Function(Predicate), + Static(Predicate), + TypeAlias(Predicate), + Struct(Predicate), + Field(Predicate), + Union(Predicate), +} diff --git a/ctest-next/src/symbol_table.rs b/ctest-next/src/symbol_table.rs new file mode 100644 index 0000000000000..4a9f1a033ae6f --- /dev/null +++ b/ctest-next/src/symbol_table.rs @@ -0,0 +1,190 @@ +use std::collections::HashMap; + +use quote::ToTokens; +use syn::visit::Visit; + +use crate::ir::{Constant, Field, Function, Parameter, Static, Struct, TypeAlias, Union}; + +type Abi = String; + +/// A `SymbolTable` represents a collected set of top-level Rust items +/// relevant to FFI generation or analysis, including foreign functions/statics, +/// type aliases, structs, unions, and constants. +#[derive(Debug)] +pub struct SymbolTable { + aliases: Vec, + structs: Vec, + unions: Vec, + constants: Vec, + foreign_functions: HashMap>, + foreign_statics: HashMap>, +} + +impl SymbolTable { + pub fn new() -> Self { + Self { + aliases: Vec::new(), + structs: Vec::new(), + unions: Vec::new(), + constants: Vec::new(), + foreign_functions: HashMap::new(), + foreign_statics: HashMap::new(), + } + } + + pub fn contains_struct(&self, ident: &str) -> bool { + self.structs() + .iter() + .any(|structure| structure.ident().to_string() == ident) + } + + pub fn contains_union(&self, ident: &str) -> bool { + self.unions() + .iter() + .any(|structure| structure.ident().to_string() == ident) + } + + pub fn aliases(&self) -> &Vec { + &self.aliases + } + + pub fn structs(&self) -> &Vec { + &self.structs + } + + pub fn unions(&self) -> &Vec { + &self.unions + } + + pub fn constants(&self) -> &Vec { + &self.constants + } + + pub fn foreign_functions(&self) -> &HashMap> { + &self.foreign_functions + } + + pub fn foreign_statics(&self) -> &HashMap> { + &self.foreign_statics + } +} + +fn is_visible(vis: &syn::Visibility) -> bool { + // Assume that if the visibility is restricted we are not meant to access it. + match vis { + syn::Visibility::Public(_) => true, + syn::Visibility::Inherited | syn::Visibility::Restricted(_) => false, + } +} + +fn collect_fields(fields: &syn::punctuated::Punctuated) -> Vec { + fields + .iter() + .map(|field| { + Field::new( + is_visible(&field.vis), + field.ident.clone(), + field.ty.to_token_stream(), + ) + }) + .collect() +} + +fn visit_foreign_item_fn(table: &mut SymbolTable, i: &syn::ForeignItemFn, abi: &str) { + let public = is_visible(&i.vis); + let ident = i.sig.ident.clone(); + let parameters = i + .sig + .inputs + .iter() + .map(|arg| match arg { + syn::FnArg::Typed(arg) => { + Parameter::new(arg.pat.to_token_stream(), arg.ty.to_token_stream()) + } + syn::FnArg::Receiver(_) => { + unreachable!("Foreign functions can't have self/receiver parameters.") + } + }) + .collect::>(); + let return_value = match &i.sig.output { + syn::ReturnType::Default => "()".to_token_stream(), + syn::ReturnType::Type(_, ty) => ty.to_token_stream(), + }; + + table + .foreign_functions + .entry(abi.to_string()) + .or_default() + .push(Function::new(public, ident, parameters, return_value)); +} + +fn visit_foreign_item_static(table: &mut SymbolTable, i: &syn::ForeignItemStatic, abi: &str) { + let public = is_visible(&i.vis); + let ident = i.ident.clone(); + let ty = i.ty.to_token_stream(); + + table + .foreign_statics + .entry(abi.to_string()) + .or_default() + .push(Static::new(public, ident, ty)); +} + +impl<'ast> Visit<'ast> for SymbolTable { + fn visit_item_type(&mut self, i: &'ast syn::ItemType) { + let public = is_visible(&i.vis); + let ty = i.ty.to_token_stream(); + let ident = i.ident.clone(); + + self.aliases.push(TypeAlias::new(public, ident, ty)); + } + + fn visit_item_struct(&mut self, i: &'ast syn::ItemStruct) { + let public = is_visible(&i.vis); + let ident = i.ident.clone(); + 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::new(public, ident, fields)); + } + + fn visit_item_union(&mut self, i: &'ast syn::ItemUnion) { + let public = is_visible(&i.vis); + let ident = i.ident.clone(); + let fields = collect_fields(&i.fields.named); + + self.unions.push(Union::new(public, ident, fields)); + } + + fn visit_item_const(&mut self, i: &'ast syn::ItemConst) { + let public = is_visible(&i.vis); + let ident = i.ident.clone(); + let ty = i.ty.to_token_stream(); + let value = i.expr.to_token_stream(); + + self.constants.push(Constant::new(public, ident, ty, value)); + } + + fn visit_item_foreign_mod(&mut self, i: &'ast syn::ItemForeignMod) { + // Because we require the ABI we can't directly visit the foreign functions/statics. + let abi = i + .abi + .name + .clone() + .map(|s| s.value()) + .unwrap_or_else(|| "C".to_string()); + + 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/translation/mod.rs b/ctest-next/src/translation/mod.rs new file mode 100644 index 0000000000000..8bc83cbce8607 --- /dev/null +++ b/ctest-next/src/translation/mod.rs @@ -0,0 +1,5 @@ +mod overrides; +mod translator; + +pub use overrides::TranslationOverrides; +pub use translator::RustToCTranslator; diff --git a/ctest-next/src/translation/overrides.rs b/ctest-next/src/translation/overrides.rs new file mode 100644 index 0000000000000..ebf5c3107e9da --- /dev/null +++ b/ctest-next/src/translation/overrides.rs @@ -0,0 +1,36 @@ +use crate::{ + ir::{Constant, Field, Function, Struct}, + symbol_table::SymbolTable, +}; + +/// `TranslationOverrides` defines customization hooks for how identifiers +/// are translated during code generation. +/// +/// Implementors can override name generation for fields, types, constants, +/// and functions. +pub trait TranslationOverrides { + fn field_name(&self, structure: &Struct, field: &Field) -> String { + field.ident().clone().unwrap().to_string() + } + fn type_name(&self, name: &str, table: &SymbolTable) -> String { + if table.contains_struct(name) { + format!("struct {}", name) + } else if table.contains_union(name) { + format!("union {}", name) + } else { + name.to_string() + } + } + fn const_name(&self, constant: Constant) -> String { + constant.ident().clone().to_string() + } + fn func_name(&self, function: Function, link_name: String) -> String { + function.ident().to_string() + } +} + +/// The default implementation of `TranslationOverrides`, +/// which performs no renaming and uses the source identifiers directly. +pub struct DefaultOverrides; + +impl TranslationOverrides for DefaultOverrides {} diff --git a/ctest-next/src/translation/translator.rs b/ctest-next/src/translation/translator.rs new file mode 100644 index 0000000000000..f2f1e5406acc7 --- /dev/null +++ b/ctest-next/src/translation/translator.rs @@ -0,0 +1,206 @@ +use std::fmt::{self, Write}; + +use crate::{ + ir::TypeAlias, + symbol_table::SymbolTable, + translation::{overrides::DefaultOverrides, TranslationOverrides}, +}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum Language { + C, + CXX, + Rust, +} + +pub struct RustToCTranslator { + table: SymbolTable, + overrides: O, +} + +impl RustToCTranslator { + pub fn new(table: SymbolTable) -> Self { + Self { + table, + overrides: DefaultOverrides, + } + } +} + +impl RustToCTranslator { + pub fn table(&self) -> &SymbolTable { + &self.table + } + + pub fn new_with_overrides(table: SymbolTable, overrides: O) -> Self { + Self { table, overrides } + } + + pub fn translate_type_alias(&self, type_alias: &TypeAlias) -> Result { + let mut s = String::new(); + writeln!( + s, + "typedef {} {};", + // TODO: Add support for more complex types such as crate::c_int. + self.translate_ptr_type(&type_alias.ty().to_string()), + type_alias.ident().to_string() + )?; + Ok(s) + } + + pub fn translate_primitive_type(&self, ty: &str) -> String { + match ty { + "usize" => "size_t".to_string(), + "isize" => "ssize_t".to_string(), + "u8" => "uint8_t".to_string(), + "u16" => "uint16_t".to_string(), + "u32" => "uint32_t".to_string(), + "u64" => "uint64_t".to_string(), + "i8" => "int8_t".to_string(), + "i16" => "int16_t".to_string(), + "i32" => "int32_t".to_string(), + "i64" => "int64_t".to_string(), + "()" => "void".to_string(), + + "c_longdouble" | "c_long_double" => "long double".to_string(), + ty if ty.starts_with("c_") => { + let ty = &ty[2..].replace("long", " long")[..]; + match ty { + "short" => "short".to_string(), + s if s.starts_with('u') => format!("unsigned {}", &s[1..]), + s if s.starts_with('s') => format!("signed {}", &s[1..]), + s => s.to_string(), + } + } + s => self.overrides.type_name(s, &self.table), + } + } + + pub fn translate_ptr_type(&self, mut ty: &str) -> String { + if ty == "&str" { + return "char*".to_string(); + } + + // FIXME: This might fail on some edge case. + // This had to be slightly modified because the syn roundtrip + // always adds whitespace between punctuation like *mut => * mut. + let cty = ty.replace("* mut ", "").replace("* const ", ""); + let mut cty = self.translate_primitive_type(&cty); + while ty.starts_with("*") { + if ty.starts_with("* const") { + cty = format!("const {}*", cty); + ty = &ty[8..]; + } else { + cty = format!("{}*", cty); + ty = &ty[6..]; + } + } + + cty + } +} + +#[cfg(test)] +mod tests { + use std::error::Error; + + use proc_macro2::TokenStream; + use quote::quote; + use syn::visit::Visit; + + use super::*; + + fn construct_table(code: TokenStream) -> Result> { + let mut table = SymbolTable::new(); + let code: syn::File = syn::parse2(code)?; + table.visit_file(&code); + Ok(table) + } + + #[test] + fn test_type_alias_primitive_type() -> Result<(), Box> { + let table = construct_table(quote! { + pub type byte = u8; + })?; + assert_eq!(table.aliases().len(), 1); + + let translator = RustToCTranslator::new(table); + let alias = &translator.table().aliases()[0]; + + assert_eq!( + translator.translate_type_alias(&alias)?, + "typedef uint8_t byte;\n" + ); + + Ok(()) + } + + #[test] + fn test_type_alias_struct_type() -> Result<(), Box> { + let table = construct_table(quote! { + struct Person { + name: String, + age: u8, + } + + pub type Bob = Person; + })?; + assert_eq!(table.aliases().len(), 1); + + let translator = RustToCTranslator::new(table); + let alias = &translator.table().aliases()[0]; + + assert_eq!( + translator.translate_type_alias(&alias)?, + "typedef struct Person Bob;\n" + ); + + Ok(()) + } + + #[test] + fn test_type_alias_complex_type() -> Result<(), Box> { + let table = construct_table(quote! { + pub type ptr = *mut *const i32; + })?; + assert_eq!(table.aliases().len(), 1); + + let translator = RustToCTranslator::new(table); + let alias = &translator.table().aliases()[0]; + + assert_eq!( + translator.translate_type_alias(&alias)?, + "typedef const int32_t** ptr;\n" + ); + + Ok(()) + } + + #[test] + fn test_type_name_override() -> Result<(), Box> { + let table = construct_table(quote! { + struct Bob; + + pub type ptr = *mut *const Bob; + })?; + assert_eq!(table.aliases().len(), 1); + + struct PrefixTypeWith__; + + impl TranslationOverrides for PrefixTypeWith__ { + fn type_name(&self, name: &str, _: &SymbolTable) -> String { + format!("__{}", name) + } + } + + let translator = RustToCTranslator::new_with_overrides(table, PrefixTypeWith__); + let alias = &translator.table().aliases()[0]; + + assert_eq!( + translator.translate_type_alias(&alias)?, + "typedef const __Bob** ptr;\n" + ); + + Ok(()) + } +}