diff --git a/ctest-next/Cargo.toml b/ctest-next/Cargo.toml index 556d6d05f9ea5..692e16b8654cb 100644 --- a/ctest-next/Cargo.toml +++ b/ctest-next/Cargo.toml @@ -8,5 +8,7 @@ repository = "https://github.com/rust-lang/libc" publish = false [dependencies] +askama = "0.14.0" cc = "1.2.25" +quote = "1.0.40" syn = { version = "2.0.101", features = ["full", "visit", "extra-traits"] } diff --git a/ctest-next/askama.toml b/ctest-next/askama.toml new file mode 100644 index 0000000000000..ffcb461b888f5 --- /dev/null +++ b/ctest-next/askama.toml @@ -0,0 +1,3 @@ +[[escaper]] +path = "askama::filters::Text" +extensions = ["rs", "c", "cpp"] diff --git a/ctest-next/build.rs b/ctest-next/build.rs new file mode 100644 index 0000000000000..8a61a0cd86213 --- /dev/null +++ b/ctest-next/build.rs @@ -0,0 +1,31 @@ +use std::env; + +fn main() { + let host = env::var("HOST").unwrap(); + let target = env::var("TARGET").unwrap(); + let target_key = target.replace('-', "_"); + + println!("cargo:rustc-env=HOST_PLATFORM={host}"); + println!("cargo:rustc-env=TARGET_PLATFORM={target}"); + + let linker = env::var(format!("CARGO_TARGET_{}_LINKER", target_key.to_uppercase())) + .or_else(|_| env::var("CC")) + .or_else(|_| env::var(format!("CC_{}", target_key))) + .unwrap_or_default(); + + let runner = + env::var(format!("CARGO_TARGET_{}_RUNNER", target_key.to_uppercase())).unwrap_or_default(); + + // As we invoke rustc directly this does not get passed to it, although RUSTFLAGS does. + let flags = env::var(format!( + "CARGO_TARGET_{}_RUSTFLAGS", + target_key.to_uppercase() + )) + .unwrap_or_default(); + + println!("cargo:rustc-env=LINKER={linker}"); + println!("cargo:rustc-env=RUNNER={runner}"); + println!("cargo:rustc-env=FLAGS={flags}"); + + println!("cargo:rerun-if-changed-env=TARGET"); +} diff --git a/ctest-next/lib b/ctest-next/lib new file mode 100755 index 0000000000000..b99a401702024 Binary files /dev/null and b/ctest-next/lib differ diff --git a/ctest-next/src/ast/constant.rs b/ctest-next/src/ast/constant.rs index c499994dd1c74..654d691df66d5 100644 --- a/ctest-next/src/ast/constant.rs +++ b/ctest-next/src/ast/constant.rs @@ -6,7 +6,6 @@ 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, diff --git a/ctest-next/src/ffi_items.rs b/ctest-next/src/ffi_items.rs index 9a1948b8cbb39..ff26152383882 100644 --- a/ctest-next/src/ffi_items.rs +++ b/ctest-next/src/ffi_items.rs @@ -54,7 +54,6 @@ impl FfiItems { } /// Return a list of all constants found. - #[cfg_attr(not(test), expect(unused))] pub(crate) fn constants(&self) -> &Vec { &self.constants } diff --git a/ctest-next/src/generator.rs b/ctest-next/src/generator.rs index acfcd1e76370a..f8e6312795161 100644 --- a/ctest-next/src/generator.rs +++ b/ctest-next/src/generator.rs @@ -1,13 +1,30 @@ -use std::path::Path; +use std::{ + env, + fs::File, + io::Write, + path::{Path, PathBuf}, +}; +use askama::Template; use syn::visit::Visit; -use crate::{expand, ffi_items::FfiItems, Result}; +use crate::{ + expand, + ffi_items::FfiItems, + template::{CTestTemplate, RustTestTemplate}, + Result, +}; /// A builder used to generate a test suite. #[non_exhaustive] #[derive(Default, Debug, Clone)] -pub struct TestGenerator {} +pub struct TestGenerator { + headers: Vec, + target: Option, + host: Option, + includes: Vec, + out_dir: Option, +} impl TestGenerator { /// Creates a new blank test generator. @@ -15,14 +32,128 @@ impl TestGenerator { Self::default() } + /// Add a header to be included as part of the generated C file. + pub fn header(&mut self, header: &str) -> &mut Self { + self.headers.push(header.to_string()); + self + } + + /// Configures the target to compile C code for. + pub fn target(&mut self, target: &str) -> &mut Self { + self.target = Some(target.to_string()); + self + } + + /// Configures the host. + pub fn host(&mut self, host: &str) -> &mut Self { + self.host = Some(host.to_string()); + self + } + + /// Add a path to the C compiler header lookup path. + /// + /// This is useful for if the C library is installed to a nonstandard + /// location to ensure that compiling the C file succeeds. + pub fn include>(&mut self, p: P) -> &mut Self { + self.includes.push(p.as_ref().to_owned()); + self + } + + /// Configures the output directory of the generated Rust and C code. + pub fn out_dir>(&mut self, p: P) -> &mut Self { + self.out_dir = Some(p.as_ref().to_owned()); + self + } + /// Generate all tests for the given crate and output the Rust side to a file. - pub fn generate>(&mut self, crate_path: P, _output_file_path: P) -> Result<()> { + pub fn generate>(&mut self, crate_path: P, output_file_path: P) -> Result<()> { + let output_file_path = self.generate_files(crate_path, output_file_path)?; + + let target = self + .target + .clone() + .or(Some(env::var("TARGET_PLATFORM").unwrap())) + .unwrap(); + let host = self + .host + .clone() + .or(Some(env::var("HOST_PLATFORM").unwrap())) + .unwrap(); + + let mut cfg = cc::Build::new(); + // FIXME: Cpp not supported. + cfg.file(output_file_path.with_extension("c")); + cfg.host(&host); + if target.contains("msvc") { + cfg.flag("/W3") + .flag("/Wall") + .flag("/WX") + // ignored warnings + .flag("/wd4820") // warning about adding padding? + .flag("/wd4100") // unused parameters + .flag("/wd4996") // deprecated functions + .flag("/wd4296") // '<' being always false + .flag("/wd4255") // converting () to (void) + .flag("/wd4668") // using an undefined thing in preprocessor? + .flag("/wd4366") // taking ref to packed struct field might be unaligned + .flag("/wd4189") // local variable initialized but not referenced + .flag("/wd4710") // function not inlined + .flag("/wd5045") // compiler will insert Spectre mitigation + .flag("/wd4514") // unreferenced inline function removed + .flag("/wd4711"); // function selected for automatic inline + } else { + cfg.flag("-Wall") + .flag("-Wextra") + .flag("-Werror") + .flag("-Wno-unused-parameter") + .flag("-Wno-type-limits") + // allow taking address of packed struct members: + .flag("-Wno-address-of-packed-member") + .flag("-Wno-unknown-warning-option") + .flag("-Wno-deprecated-declarations"); // allow deprecated items + } + + for p in &self.includes { + cfg.include(p); + } + + let stem: &str = output_file_path.file_stem().unwrap().to_str().unwrap(); + cfg.target(&target) + .out_dir(output_file_path.parent().unwrap()) + .compile(stem); + + Ok(()) + } + + /// Generate the Rust and C testing files. + pub(crate) fn generate_files>( + &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(()) + let output_directory = self + .out_dir + .clone() + .unwrap_or_else(|| PathBuf::from(env::var_os("OUT_DIR").unwrap())); + let output_file_path = output_directory.join(output_file_path); + + // Generate the Rust side of the tests. + File::create(&output_file_path)? + .write_all(RustTestTemplate::new(&ffi_items)?.render()?.as_bytes())?; + + // Generate the C side of the tests. + // FIXME: Cpp not supported yet. + let c_output_path = output_file_path.with_extension("c"); + let headers = self.headers.iter().map(|h| h.as_str()).collect(); + File::create(&c_output_path)? + .write_all(CTestTemplate::new(headers, &ffi_items).render()?.as_bytes())?; + + Ok(output_file_path) } } diff --git a/ctest-next/src/lib.rs b/ctest-next/src/lib.rs index bc4e5f3375586..944aea61ab86a 100644 --- a/ctest-next/src/lib.rs +++ b/ctest-next/src/lib.rs @@ -15,10 +15,16 @@ mod ast; mod ffi_items; mod generator; mod macro_expansion; +mod runner; +mod rustc_queries; +mod template; +mod translator; pub use ast::{Abi, Const, Field, Fn, Parameter, Static, Struct, Type, Union}; pub use generator::TestGenerator; pub use macro_expansion::expand; +pub use runner::{compile_test, run_test}; +pub use rustc_queries::{rustc_host, rustc_version, RustcVersion}; /// A possible error that can be encountered in our library. pub type Error = Box; diff --git a/ctest-next/src/runner.rs b/ctest-next/src/runner.rs new file mode 100644 index 0000000000000..0a7bfdc74bb51 --- /dev/null +++ b/ctest-next/src/runner.rs @@ -0,0 +1,85 @@ +use crate::Result; + +use std::env; +use std::fs::{canonicalize, File}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Compile the given Rust file with the given static library. +/// All arguments must be valid paths. +pub fn compile_test>( + output_dir: P, + crate_path: P, + library_file: P, +) -> Result { + let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into()); + + let output_dir = output_dir.as_ref(); + let crate_path = crate_path.as_ref(); + let library_file = library_file.as_ref(); + + let rust_file = output_dir + .join(crate_path.file_stem().unwrap()) + .with_extension("rs"); + let binary_path = output_dir.join(rust_file.file_stem().unwrap()); + + let mut file = File::create(&rust_file)?; + writeln!( + file, + "include!(r#\"{}\"#);", + canonicalize(crate_path)?.display() + )?; + writeln!(file, "include!(r#\"{}.rs\"#);", library_file.display())?; + let mut cmd = Command::new(rustc); + cmd.arg(&rust_file) + .arg(format!("-Lnative={}", output_dir.display())) + .arg(format!( + "-lstatic={}", + library_file.file_stem().unwrap().to_str().unwrap() + )) + .arg("--target") + .arg(env::var("TARGET_PLATFORM").unwrap()) + .arg("-o") + .arg(&binary_path) + .arg("-Aunused"); + + let linker = env::var("LINKER").unwrap(); + if !linker.is_empty() { + cmd.arg(format!("-Clinker={}", linker)); + } + + let flags = env::var("FLAGS").unwrap(); + if !flags.is_empty() { + cmd.args(flags.split_whitespace().collect::>()); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Err(std::str::from_utf8(&output.stderr)?.into()); + } + + Ok(binary_path) +} + +/// Run the compiled test binary. +pub fn run_test(test_binary: &str) -> Result { + let cmd = env::var("RUNNER").unwrap(); + let output = if cmd.is_empty() { + Command::new(test_binary).output()? + } else { + let mut cmd = cmd.split_whitespace(); + Command::new(cmd.next().unwrap()) + .args(cmd.collect::>()) + .arg(test_binary) + .output()? + }; + + if !output.status.success() { + return Err(std::str::from_utf8(&output.stderr)?.into()); + } + + // The template prints to stderr regardless. + Ok(std::str::from_utf8(&output.stderr)?.to_string()) +} diff --git a/ctest-next/src/rustc_queries.rs b/ctest-next/src/rustc_queries.rs new file mode 100644 index 0000000000000..fd4568ea9bbc6 --- /dev/null +++ b/ctest-next/src/rustc_queries.rs @@ -0,0 +1,99 @@ +use std::{env, fmt::Display, num::ParseIntError, process::Command}; + +use crate::Result; + +/// Represents the current version of the rustc compiler globally in use. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct RustcVersion { + major: u8, + minor: u8, + patch: u8, +} + +impl RustcVersion { + /// Define a rustc version with the given major.minor.patch. + pub fn new(major: u8, minor: u8, patch: u8) -> Self { + Self { + major, + minor, + patch, + } + } +} + +impl Display for RustcVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "RustcVersion({}, {}, {})", + self.major, self.minor, self.patch + ) + } +} + +/// Return the global rustc version. +pub fn rustc_version() -> Result { + let rustc = env::var("RUSTC").unwrap_or_else(|_| String::from("rustc")); + + let output = Command::new(rustc).arg("--version").output()?; + + if !output.status.success() { + let error = std::str::from_utf8(&output.stderr)?; + return Err(error.into()); + } + + // eg: rustc 1.87.0-(optionally nightly) (17067e9ac 2025-05-09) + // Assume the format does not change. + let [major, minor, patch] = std::str::from_utf8(&output.stdout)? + .split_whitespace() + .nth(1) + .unwrap() + .split('.') + .take(3) + .map(|s| { + s.chars() + .take_while(|c| c.is_ascii_digit()) + .collect::() + .trim() + .parse::() + }) + .collect::, ParseIntError>>()? + .try_into() + .unwrap(); + + Ok(RustcVersion::new(major, minor, patch)) +} + +/// Return the host triple. +pub fn rustc_host() -> Result { + let rustc = env::var("RUSTC").unwrap_or_else(|_| String::from("rustc")); + + let output = Command::new(rustc) + .arg("--version") + .arg("--verbose") + .output()?; + + if !output.status.success() { + let error = std::str::from_utf8(&output.stderr)?; + return Err(error.into()); + } + + // eg: rustc 1.87.0-(optionally nightly) (17067e9ac 2025-05-09) + // binary: rustc + // commit-hash: 17067e9ac6d7ecb70e50f92c1944e545188d2359 + // commit-date: 2025-05-09 + // host: x86_64-unknown-linux-gnu + // release: 1.87.0 + // LLVM version: 20.1.1 + // Assume the format does not change. + let host = std::str::from_utf8(&output.stdout)? + .lines() + .nth(4) + .unwrap() + .split(':') + .next_back() + .map(|s| s.trim()) + .unwrap(); + + Ok(host.to_string()) +} diff --git a/ctest-next/src/template.rs b/ctest-next/src/template.rs new file mode 100644 index 0000000000000..85f8ee1be0c10 --- /dev/null +++ b/ctest-next/src/template.rs @@ -0,0 +1,37 @@ +use askama::Template; +use quote::ToTokens; + +use crate::{ffi_items::FfiItems, rustc_version, translator::Translator, Result, RustcVersion}; + +/// Represents the Rust side of the generated testing suite. +#[derive(Template, Debug, Clone)] +#[template(path = "test.rs")] +pub(crate) struct RustTestTemplate<'a> { + rustc_version: RustcVersion, + ffi_items: &'a FfiItems, +} + +/// Represents the C side of the generated testing suite. +#[derive(Template, Debug, Clone)] +#[template(path = "test.c")] +pub(crate) struct CTestTemplate<'a> { + headers: Vec<&'a str>, + ffi_items: &'a FfiItems, +} + +impl<'a> RustTestTemplate<'a> { + /// Create a new test template to test the given items. + pub(crate) fn new(ffi_items: &'a FfiItems) -> Result { + Ok(Self { + ffi_items, + rustc_version: rustc_version()?, + }) + } +} + +impl<'a> CTestTemplate<'a> { + /// Create a new test template to test the given items. + pub(crate) fn new(headers: Vec<&'a str>, ffi_items: &'a FfiItems) -> Self { + Self { headers, ffi_items } + } +} diff --git a/ctest-next/src/tests.rs b/ctest-next/src/tests.rs index c8e7f25e2d062..7e4b4dbaf1a6a 100644 --- a/ctest-next/src/tests.rs +++ b/ctest-next/src/tests.rs @@ -1,4 +1,4 @@ -use crate::ffi_items::FfiItems; +use crate::{ffi_items::FfiItems, translator::Translator}; use syn::visit::Visit; @@ -28,6 +28,12 @@ extern "C" { } "#; +macro_rules! collect_idents { + ($items:expr) => { + $items.iter().map(|a| a.ident()).collect::>() + }; +} + #[test] fn test_extraction_ffi_items() { let ast = syn::parse_file(ALL_ITEMS).unwrap(); @@ -35,57 +41,53 @@ fn test_extraction_ffi_items() { 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!(collect_idents!(ffi_items.aliases()), ["Foo"]); + assert_eq!(collect_idents!(ffi_items.constants()), ["bar"]); + assert_eq!(collect_idents!(ffi_items.foreign_functions()), ["malloc"]); + assert_eq!(collect_idents!(ffi_items.foreign_statics()), ["baz"]); + assert_eq!(collect_idents!(ffi_items.structs()), ["Array"]); + assert_eq!(collect_idents!(ffi_items.unions()), ["Word"]); +} - assert_eq!( - ffi_items - .constants() - .iter() - .map(|a| a.ident()) - .collect::>(), - ["bar"] - ); +#[test] +fn test_translation_type_path() { + let translator = Translator {}; + let ty: syn::Type = syn::parse_str("std::option::Option").unwrap(); - assert_eq!( - ffi_items - .foreign_functions() - .iter() - .map(|a| a.ident()) - .collect::>(), - ["malloc"] - ); + assert_eq!(translator.translate_type(&ty), "uint8_t"); +} - assert_eq!( - ffi_items - .foreign_statics() - .iter() - .map(|a| a.ident()) - .collect::>(), - ["baz"] - ); +#[test] +fn test_translation_type_ptr() { + let translator = Translator {}; + let ty: syn::Type = syn::parse_str("*const *mut i32").unwrap(); - assert_eq!( - ffi_items - .structs() - .iter() - .map(|a| a.ident()) - .collect::>(), - ["Array"] - ); + assert_eq!(translator.translate_type(&ty), " int32_t* const*"); +} + +#[test] +fn test_translation_type_reference() { + let translator = Translator {}; + let ty: syn::Type = syn::parse_str("&u8").unwrap(); + + assert_eq!(translator.translate_type(&ty), "uint8_t*"); +} + +#[test] +fn test_translation_type_bare_fn() { + let translator = Translator {}; + let ty: syn::Type = syn::parse_str("fn(*mut u8, i16) -> &str").unwrap(); assert_eq!( - ffi_items - .unions() - .iter() - .map(|a| a.ident()) - .collect::>(), - ["Word"] + translator.translate_type(&ty), + "char*(*)( uint8_t*, int16_t)" ); } + +#[test] +fn test_translation_type_array() { + let translator = Translator {}; + let ty: syn::Type = syn::parse_str("[&u8; 2 + 2]").unwrap(); + + assert_eq!(translator.translate_type(&ty), "uint8_t*[2 + 2]"); +} diff --git a/ctest-next/src/translator.rs b/ctest-next/src/translator.rs new file mode 100644 index 0000000000000..2be1514a4f0f7 --- /dev/null +++ b/ctest-next/src/translator.rs @@ -0,0 +1,207 @@ +use std::ops::Deref; + +#[derive(Debug, Default)] +/// A Rust to C/Cxx translator. +pub(crate) struct Translator {} + +impl Translator { + /// Create a new translator. + pub(crate) fn new() -> Self { + Self::default() + } + + /// Return whether a type is a Rust primitive type. + fn is_rust_primitive(&self, ty: &str) -> bool { + let rustc_types = [ + "usize", "u8", "u16", "u32", "u64", "isize", "i8", "i16", "i32", "i64", "f32", "f64", + ]; + ty.starts_with("c_") || rustc_types.contains(&ty) + } + + /// Translate mutability from Rust to C. + fn translate_mut(&self, mutability: Option) -> String { + mutability.map(|_| "const ").unwrap_or("").to_string() + } + + /// Translate a Rust type into its equivalent C type. + pub(crate) fn translate_type(&self, ty: &syn::Type) -> String { + match ty { + syn::Type::Ptr(ptr) => self.translate_ptr(ptr), + syn::Type::Path(path) => self.translate_path(path), + syn::Type::Tuple(tuple) if tuple.elems.is_empty() => "void".to_string(), + syn::Type::Array(array) => self.translate_array(array), + syn::Type::Reference(reference) => self.translate_reference(reference), + syn::Type::BareFn(function) => self.translate_bare_fn(function), + syn::Type::Never(_) => "void".to_string(), + _ => unimplemented!(), + } + } + + /// Translate a Rust reference to its C equivalent. + fn translate_reference(&self, reference: &syn::TypeReference) -> String { + let path = match reference.elem.deref() { + syn::Type::Path(path) => path.path.segments.last().unwrap(), + syn::Type::Array(array) => { + return format!( + "{}{}*", + self.translate_mut(reference.mutability), + self.translate_type(&array.elem), + ) + } + _ => panic!("Unknown type! {:?}", reference.elem), + }; + + let ident = path.ident.to_string(); + match ident.as_str() { + "str" => { + if reference.mutability.is_some() { + panic!("Unknown type, &mut str"); + } + + "char*".to_string() + } + c if self.is_rust_primitive(c) => format!( + "{}{}*", + self.translate_mut(reference.mutability), + self.translate_primitive_type(&path.ident) + ), + _ => unimplemented!("References to non primitive types are not implemented."), + } + } + + /// Translate a Rust function pointer type to its C equivalent. + fn translate_bare_fn(&self, function: &syn::TypeBareFn) -> String { + assert!(function.lifetimes.is_none(), "No lifetimes allowed."); + assert!(function.variadic.is_none(), "No variadics allowed."); + + let mut parameters = function + .inputs + .iter() + .map(|arg| self.translate_type(&arg.ty)) + .collect::>(); + let return_type = match &function.output { + syn::ReturnType::Default => "void".to_string(), + syn::ReturnType::Type(_, ty) => self.translate_type(ty), + }; + + if parameters.is_empty() { + parameters.push("void".to_string()); + } + + if return_type.contains("(*)") { + return_type.replace("(*)", &format!("(*(*)({}))", parameters.join(", "))) + } else { + format!("{}(*)({})", return_type, parameters.join(", ")) + } + } + + /// Translate a Rust primitve type into its C equivalent. + fn translate_primitive_type(&self, ty: &syn::Ident) -> String { + let ty = ty.to_string(); + match ty.as_str() { + "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(), + "f32" => "float".to_string(), + "f64" => "double".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(), + } + } + // Overriding type names not yet implemented. + s => s.to_string(), + } + } + + /// Translate a Rust path into its C equivalent. + fn translate_path(&self, path: &syn::TypePath) -> String { + // Paths should be fully qualified otherwise they won't properly be translated. + let last = path.path.segments.last().unwrap(); + if last.ident == "Option" { + if let syn::PathArguments::AngleBracketed(p) = &last.arguments { + if let syn::GenericArgument::Type(ty) = p.args.first().unwrap() { + self.translate_type(ty) + } else { + unimplemented!("Only simple generic types are supported!") + } + } else { + unreachable!("Option cannot have parentheses.") + } + } else { + self.translate_primitive_type(&last.ident) + } + } + + /// Translate a Rust array declaration into its C equivalent. + fn translate_array(&self, array: &syn::TypeArray) -> String { + format!( + "{}[{}]", + self.translate_type(array.elem.deref()), + self.translate_expr(&array.len) + ) + } + + /// Translate a Rust pointer into its equivalent C pointer. + fn translate_ptr(&self, ptr: &syn::TypePtr) -> String { + let modifier = ptr.mutability.map(|_| "").unwrap_or("const"); + let inner = ptr.elem.deref(); + match inner { + syn::Type::BareFn(_) => self.translate_type(inner), + syn::Type::Ptr(_) => format!("{} {}*", self.translate_type(inner), modifier), + syn::Type::Array(arr) => { + let len = self.translate_expr(&arr.len); + let ty = self.translate_type(inner); + format!("{} {} [{}]", modifier, ty, len) + } + _ => format!("{} {}*", modifier, self.translate_type(inner)), + } + } + + /// Translate a simple Rust expression to C. + /// + /// This method is only used for translating expressions inside of + /// array brackets, and will fail for expressions not allowed inside of + /// those brackets. + #[expect(clippy::only_used_in_recursion)] + fn translate_expr(&self, expr: &syn::Expr) -> String { + match expr { + syn::Expr::Lit(l) => match &l.lit { + syn::Lit::Int(i) => i.to_string(), + _ => panic!("Invalid Syntax! Cannot have non integer literal in array expression."), + }, + syn::Expr::Path(p) => p.path.segments.last().unwrap().ident.to_string(), + syn::Expr::Cast(c) => self.translate_expr(c.expr.deref()), + syn::Expr::Binary(b) => { + let left = self.translate_expr(b.left.deref()); + let right = self.translate_expr(b.right.deref()); + + match b.op { + syn::BinOp::Add(_) => format!("{} + {}", left, right), + syn::BinOp::Sub(_) => format!("{} - {}", left, right), + // Some operators have not been implemented, such as + // shift left, shift right etc. Some other operators cannot be + // placed inside array brackets. + _ => unimplemented!("Unknown Operator! {:?}", b.op), + } + } + // Some expressions have not been implemented, such as + // braces eg: [u8; { expr }], constant functions, etc. + _ => unimplemented!("Unknown Expression! {:?}", expr), + } + } +} diff --git a/ctest-next/templates/constants/test_constant.c b/ctest-next/templates/constants/test_constant.c new file mode 100644 index 0000000000000..30816e0e385bd --- /dev/null +++ b/ctest-next/templates/constants/test_constant.c @@ -0,0 +1,8 @@ +{% let c_type = Translator::new().translate_type(constant.ty) %} +{% let ident = constant.ident() %} + +static const {{ c_type }} __test_const_{{ ident }}_val = {{ ident }}; + +const {{ c_type }}* __test_const_{{ ident }}(void) { + return &__test_const_{{ ident }}_val; +} \ No newline at end of file diff --git a/ctest-next/templates/constants/test_constant.rs b/ctest-next/templates/constants/test_constant.rs new file mode 100644 index 0000000000000..8a6950583724e --- /dev/null +++ b/ctest-next/templates/constants/test_constant.rs @@ -0,0 +1,38 @@ +{% let ty = constant.ty.to_token_stream().to_string() %} +{% let ident = constant.ident() %} + +{% if ty == "&str" %} + #[inline(never)] + #[allow(non_snake_case)] + fn const_{{ ident }}() { + extern "C" { + #[allow(non_snake_case)] + fn __test_const_{{ ident }}() -> *const *const u8; + } + let val = {{ ident }}; + unsafe { + let ptr = *__test_const_{{ ident }}(); + let c = ::std::ffi::CStr::from_ptr(ptr as *const _); + let c = c.to_str().expect("const {{ ident }} not utf8"); + same(val, c, "{{ ident }} string"); + } + } +{% else %} + #[allow(non_snake_case)] + fn const_{{ ident }}() { + extern "C" { + #[allow(non_snake_case)] + fn __test_const_{{ ident }}() -> *const {{ ty }}; + } + let val = {{ ident }}; + unsafe { + let ptr1 = &val as *const _ as *const u8; + let ptr2 = __test_const_{{ ident }}() as *const u8; + for i in 0..mem::size_of::<{{ ty }}>() { + let i = i as isize; + same(*ptr1.offset(i), *ptr2.offset(i), + &format!("{{ ident }} value at byte {}", i)); + } + } + } +{% endif %} diff --git a/ctest-next/templates/test.c b/ctest-next/templates/test.c new file mode 100644 index 0000000000000..eeace93848ce1 --- /dev/null +++ b/ctest-next/templates/test.c @@ -0,0 +1,12 @@ +#include +#include +#include +#include + +{% for header in headers %} + #include <{{ header }}> +{% endfor %} + +{% for constant in ffi_items.constants() %} + {% include "constants/test_constant.c" %} +{% endfor %} \ No newline at end of file diff --git a/ctest-next/templates/test.rs b/ctest-next/templates/test.rs new file mode 100644 index 0000000000000..95b24bec5b0db --- /dev/null +++ b/ctest-next/templates/test.rs @@ -0,0 +1,74 @@ +use std::mem; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +{% if rustc_version < RustcVersion::new(1, 30, 0) %} + static FAILED: AtomicBool = std::sync::atomic::ATOMIC_BOOL_INIT; + static NTESTS: AtomicUsize = std::sync::atomic::ATOMIC_USIZE_INIT; +{% else %} + static FAILED: AtomicBool = AtomicBool::new(false); + static NTESTS: AtomicUsize = AtomicUsize::new(0); +{% endif %} + +fn main() { + eprintln!("RUNNING ALL TESTS"); + run_all(); + if FAILED.load(Ordering::SeqCst) { + panic!("some tests failed"); + } else { + eprintln!("PASSED {} tests", NTESTS.load(Ordering::SeqCst)); + } +} + +trait Pretty { + fn pretty(&self) -> String; +} + +impl<'a> Pretty for &'a str { + fn pretty(&self) -> String { format!("{:?}", self) } +} +impl Pretty for *const T { + fn pretty(&self) -> String { format!("{:?}", self) } +} +impl Pretty for *mut T { + fn pretty(&self) -> String { format!("{:?}", self) } +} +macro_rules! p { + ($($i:ident)*) => ($( + impl Pretty for $i { + fn pretty(&self) -> String { + format!("{} ({:#x})", self, self) + } + } + )*) +} +p! { i8 i16 i32 i64 u8 u16 u32 u64 usize isize } + +fn same(rust: T, c: T, attr: &str) { + if rust != c { + eprintln!("bad {}: rust: {} != c {}", attr, rust.pretty(), + c.pretty()); + FAILED.store(true, Ordering::SeqCst); + } else { + NTESTS.fetch_add(1, Ordering::SeqCst); + } +} + +macro_rules! offset_of { + ($ty:ident, $field:ident) => ({ + let value = std::mem::MaybeUninit::<$ty>::uninit(); + let base_pointer = value.as_ptr(); + let offset_pointer = std::ptr::addr_of!((*base_pointer).$field); + (offset_pointer as u64) - (base_pointer as u64) + }) +} + +{% for constant in ffi_items.constants() %} + {% include "constants/test_constant.rs" %} +{% endfor %} + +#[inline(never)] +fn run_all() { + {% for constant in ffi_items.constants() %} + const_{{ constant.ident() }}(); + {% endfor %} +} \ No newline at end of file diff --git a/ctest-next/tests/basic.rs b/ctest-next/tests/basic.rs index 42d9a139d566c..ceee3b542d8de 100644 --- a/ctest-next/tests/basic.rs +++ b/ctest-next/tests/basic.rs @@ -1,30 +1,72 @@ -use ctest_next::TestGenerator; +use std::env; + +use ctest_next::{compile_test, run_test, Result, TestGenerator}; + +// TODO: Create a unique directory within the temp_dir where all this is done +// to prevent name collisions. + +// Headers are found relevative to the include directory, all files are generated +// relative to the output directory. + +/// Create a test generator configured to useful settings. +fn default_generator(opt_level: u8) -> Result { + env::set_var("OPT_LEVEL", opt_level.to_string()); + Ok(TestGenerator::new() + .out_dir(env::temp_dir()) + .include("tests/input") + .clone()) +} #[test] fn test_entrypoint_hierarchy() { - let mut generator = TestGenerator::new(); + let crate_path = "tests/input/hierarchy/lib.rs"; - generator - .generate("./tests/input/hierarchy/lib.rs", "hierarchy_out.rs") + default_generator(1) + .unwrap() + .header("hierarchy.h") + .generate(crate_path, "hierarchy_out.rs") .unwrap(); + + let test_binary = compile_test( + env::temp_dir().to_str().unwrap(), + crate_path, + "hierarchy_out", + ) + .unwrap(); + + assert!(run_test(test_binary.to_str().unwrap()).is_ok()); } #[test] fn test_entrypoint_simple() { - let mut generator = TestGenerator::new(); + let crate_path = "tests/input/simple.rs"; - generator - .generate("./tests/input/simple.rs", "simple_out.rs") + default_generator(1) + .unwrap() + .header("simple.h") + .generate(crate_path, "simple_out.rs") .unwrap(); + + let test_binary = + compile_test(env::temp_dir().to_str().unwrap(), crate_path, "simple_out").unwrap(); + + assert!(run_test(test_binary.to_str().unwrap()).is_ok()); } #[test] fn test_entrypoint_macro() { - let mut generator = TestGenerator::new(); + let crate_path = "tests/input/macro.rs"; - generator - .generate("./tests/input/macro.rs", "macro_out.rs") + default_generator(1) + .unwrap() + .header("macro.h") + .generate(crate_path, "macro_out.rs") .unwrap(); + + let test_binary = + compile_test(env::temp_dir().to_str().unwrap(), crate_path, "macro_out").unwrap(); + + assert!(run_test(test_binary.to_str().unwrap()).is_ok()); } #[test] diff --git a/ctest-next/tests/input/hierarchy.h b/ctest-next/tests/input/hierarchy.h new file mode 100644 index 0000000000000..051136be9ab9b --- /dev/null +++ b/ctest-next/tests/input/hierarchy.h @@ -0,0 +1,9 @@ +#include +#include + +typedef unsigned int in6_addr; + +#define ON true + +extern void *malloc(size_t size); +extern in6_addr in6addr_any; diff --git a/ctest-next/tests/input/hierarchy/lib.rs b/ctest-next/tests/input/hierarchy/lib.rs index 6c840d79bac21..174a780dc790c 100644 --- a/ctest-next/tests/input/hierarchy/lib.rs +++ b/ctest-next/tests/input/hierarchy/lib.rs @@ -1,4 +1,7 @@ -//! Ensure that our crate is able to handle definitions spread across many files +// Ensure that our crate is able to handle definitions spread across many files mod bar; mod foo; + +use bar::*; +use foo::*; diff --git a/ctest-next/tests/input/macro.h b/ctest-next/tests/input/macro.h new file mode 100644 index 0000000000000..686f233aea4d6 --- /dev/null +++ b/ctest-next/tests/input/macro.h @@ -0,0 +1,13 @@ +#include + +struct VecU8 +{ + uint8_t x; + uint8_t y; +}; + +struct VecU16 +{ + uint16_t x; + uint16_t y; +}; \ No newline at end of file diff --git a/ctest-next/tests/input/simple.h b/ctest-next/tests/input/simple.h new file mode 100644 index 0000000000000..39df54641c2ad --- /dev/null +++ b/ctest-next/tests/input/simple.h @@ -0,0 +1,15 @@ +#include + +typedef uint8_t Byte; + +struct Person +{ + const char *name; + uint8_t age; +}; + +union Word +{ + uint16_t word; + Byte byte[2]; +};