From cad9438326739667bc210f9943ed2081cfb13fb1 Mon Sep 17 00:00:00 2001 From: mbyx Date: Tue, 1 Jul 2025 12:09:10 +0500 Subject: [PATCH] ctest: Add translation of Rust types. --- ctest-next/Cargo.toml | 8 + ctest-next/askama.toml | 3 + ctest-next/build.rs | 69 +++++ ctest-next/src/ast/constant.rs | 1 - ctest-next/src/ffi_items.rs | 1 - ctest-next/src/generator.rs | 121 ++++++++- ctest-next/src/lib.rs | 5 + ctest-next/src/macro_expansion.rs | 2 + ctest-next/src/runner.rs | 162 ++++++++++++ ctest-next/src/template.rs | 38 +++ ctest-next/src/tests.rs | 99 ++++--- ctest-next/src/translator.rs | 327 ++++++++++++++++++++++++ ctest-next/templates/test.c | 25 ++ ctest-next/templates/test.rs | 112 ++++++++ ctest-next/tests/basic.rs | 114 +++++++-- ctest-next/tests/input/hierarchy.h | 9 + ctest-next/tests/input/hierarchy.out.c | 15 ++ ctest-next/tests/input/hierarchy.out.rs | 82 ++++++ ctest-next/tests/input/hierarchy/lib.rs | 5 +- ctest-next/tests/input/macro.h | 13 + ctest-next/tests/input/macro.out.c | 7 + ctest-next/tests/input/macro.out.rs | 62 +++++ ctest-next/tests/input/simple.h | 17 ++ ctest-next/tests/input/simple.out.c | 15 ++ ctest-next/tests/input/simple.out.rs | 80 ++++++ ctest-next/tests/input/simple.rs | 2 + 26 files changed, 1324 insertions(+), 70 deletions(-) create mode 100644 ctest-next/askama.toml create mode 100644 ctest-next/build.rs create mode 100644 ctest-next/src/runner.rs create mode 100644 ctest-next/src/template.rs create mode 100644 ctest-next/src/translator.rs create mode 100644 ctest-next/templates/test.c create mode 100644 ctest-next/templates/test.rs create mode 100644 ctest-next/tests/input/hierarchy.h create mode 100644 ctest-next/tests/input/hierarchy.out.c create mode 100644 ctest-next/tests/input/hierarchy.out.rs create mode 100644 ctest-next/tests/input/macro.h create mode 100644 ctest-next/tests/input/macro.out.c create mode 100644 ctest-next/tests/input/macro.out.rs create mode 100644 ctest-next/tests/input/simple.h create mode 100644 ctest-next/tests/input/simple.out.c create mode 100644 ctest-next/tests/input/simple.out.rs diff --git a/ctest-next/Cargo.toml b/ctest-next/Cargo.toml index 556d6d05f9ea5..c9f8ecfb80d07 100644 --- a/ctest-next/Cargo.toml +++ b/ctest-next/Cargo.toml @@ -8,5 +8,13 @@ repository = "https://github.com/rust-lang/libc" publish = false [dependencies] +askama = "0.14.0" cc = "1.2.25" +proc-macro2 = { version = "1.0.95", features = ["span-locations"] } +quote = "1.0.40" syn = { version = "2.0.101", features = ["full", "visit", "extra-traits"] } +thiserror = "2.0.12" + +[dev-dependencies] +pretty_assertions = "1.4.1" +tempfile = "3.20.0" 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..e22a7dc131684 --- /dev/null +++ b/ctest-next/build.rs @@ -0,0 +1,69 @@ +use std::env; + +// When we call `cargo test` for a cross compiled target, the following is required: +// - CARGO_TARGET_{}_LINKER: To link the integration tests. +// - CARGO_TARGET_{}_RUNNER: To run the integration tests. +// +// This is already set by the CI for all platforms, so there is no problem up till here. +// +// The integration tests (which are run in qemu, but use host rustc and cc) require the +// following: +// - TARGET_PLATFORM or target set manually. (We forward TARGET in build.rs for this.) +// - HOST_PLATFORM or host set manually. (We forward HOST in build.rs for this.) +// - LINKER: To link the C headers. (We forward CARGO_TARGET_{}_LINKER for this.) +// - FLAGS: Any flags to pass when compiling the test binary for the cross compiled platform. +// (Forwarded from CARGO_TARGET_{}_RUSTFLAGS) +// - RUNNER: To run the test binary with. (Forward the same runner as CARGO_TARGET_{}_RUNNER) +// +// The TARGET_PLATFORM and HOST_PLATFORM variables are not an issue, cargo will automatically set +// TARGET and PLATFORM and we will forward them. +// +// Similarly FLAGS and RUNNER are also not an issue, if CARGO_TARGET_{}_RUSTFLAGS are present +// they're forwarded. And RUSTFLAGS works by default anyway. Similarly the test binary doesn't +// require any external applications so just the RUNNER is enough to run it. +// +// However since rustc and cc are the host versions, they will only work if we specify the +// correct variables for them. Because we only use them to compile, not run things. For CC we +// MUST specify CC or CC_target otherwise it will fail. (Other flags like AR etc. work without +// forwarding because it is run in the host.) For rustc we MUST specify the correct linker. +// Usually this is the same as CC or CC_target. +// +// In the CI, the CARGO_TARGET_{} variables are always set. + +fn main() { + let host = env::var("HOST").unwrap(); + let target = env::var("TARGET").unwrap(); + let target_key = target.replace('-', "_").to_uppercase(); + + println!("cargo:rustc-env=HOST_PLATFORM={host}"); + println!("cargo:rerun-if-changed-env=HOST"); + + println!("cargo:rustc-env=TARGET_PLATFORM={target}"); + println!("cargo:rerun-if-changed-env=TARGET"); + + let link_var = format!("CARGO_TARGET_{target_key}_LINKER"); + println!("cargo:rerun-if-changed-env={link_var}"); + if let Ok(linker) = env::var(link_var) { + println!("cargo:rustc-env=LINKER={linker}"); + } + + let run_var = format!("CARGO_TARGET_{target_key}_RUNNER"); + println!("cargo:rerun-if-changed-env={run_var}"); + if let Ok(runner) = env::var(run_var) { + println!("cargo:rustc-env=RUNNER={runner}"); + } + + // As we invoke rustc directly this does not get passed to it, although RUSTFLAGS does. + let flag_var = format!("CARGO_TARGET_{target_key}_RUSTFLAGS"); + println!("cargo:rerun-if-changed-env={flag_var}"); + if let Ok(flags) = env::var(flag_var) { + println!("cargo:rustc-env=FLAGS={flags}"); + } + + // Rerun this build script if any of these environment variables change. + println!("cargo:rerun-if-changed-env=CC"); + println!( + "cargo:rerun-if-changed-env=CC_{}", + target_key.to_lowercase() + ); +} 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..4e7071e2e953e 100644 --- a/ctest-next/src/generator.rs +++ b/ctest-next/src/generator.rs @@ -1,13 +1,40 @@ -use std::path::Path; +use std::{ + env, + fs::File, + io::Write, + path::{Path, PathBuf}, +}; +use askama::Template; use syn::visit::Visit; +use thiserror::Error; -use crate::{expand, ffi_items::FfiItems, Result}; +use crate::{ + expand, + ffi_items::FfiItems, + template::{CTestTemplate, RustTestTemplate}, +}; + +#[derive(Debug, Error)] +pub enum GenerationError { + #[error("unable to expand crate {0}: {1}")] + MacroExpansion(PathBuf, String), + #[error("unable to parse expanded crate {0}: {1}")] + RustSyntax(String, String), + #[error("unable to render {0} template: {1}")] + TemplateRender(String, String), + #[error("unable to create or write template file: {0}")] + OsError(std::io::Error), +} /// A builder used to generate a test suite. -#[non_exhaustive] #[derive(Default, Debug, Clone)] -pub struct TestGenerator {} +pub struct TestGenerator { + headers: Vec, + pub(crate) target: Option, + pub(crate) includes: Vec, + out_dir: Option, +} impl TestGenerator { /// Creates a new blank test generator. @@ -15,14 +42,90 @@ impl TestGenerator { Self::default() } - /// 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<()> { - let expanded = expand(crate_path)?; - let ast = syn::parse_file(&expanded)?; + /// Add a header to be included as part of the generated C file. + /// + /// The generate C test will be compiled by a C compiler, and this can be + /// used to ensure that all the necessary header files are included to test + /// all FFI definitions. + pub fn header(&mut self, header: &str) -> &mut Self { + self.headers.push(header.to_string()); + self + } + + /// Configures the target to compile C code for. + /// + /// Note that for Cargo builds this defaults to `$TARGET` and it's not + /// necessary to call. + pub fn target(&mut self, target: &str) -> &mut Self { + self.target = Some(target.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 the Rust and C testing files. + /// + /// Returns the path to t generated file. + pub fn generate_files( + &mut self, + crate_path: impl AsRef, + output_file_path: impl AsRef, + ) -> Result { + let expanded = expand(&crate_path).map_err(|e| { + GenerationError::MacroExpansion(crate_path.as_ref().to_path_buf(), e.to_string()) + })?; + let ast = syn::parse_file(&expanded) + .map_err(|e| GenerationError::RustSyntax(expanded, e.to_string()))?; let mut ffi_items = FfiItems::new(); ffi_items.visit_file(&ast); - Ok(()) + let output_directory = self + .out_dir + .clone() + .unwrap_or_else(|| env::var("OUT_DIR").unwrap().into()); + let output_file_path = output_directory.join(output_file_path); + + // Generate the Rust side of the tests. + File::create(output_file_path.with_extension("rs")) + .map_err(GenerationError::OsError)? + .write_all( + RustTestTemplate::new(&ffi_items) + .render() + .map_err(|e| { + GenerationError::TemplateRender("Rust".to_string(), e.to_string()) + })? + .as_bytes(), + ) + .map_err(GenerationError::OsError)?; + + // Generate the C side of the tests. + // FIXME(ctest): 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) + .map_err(GenerationError::OsError)? + .write_all( + CTestTemplate::new(headers, &ffi_items) + .render() + .map_err(|e| GenerationError::TemplateRender("C".to_string(), e.to_string()))? + .as_bytes(), + ) + .map_err(GenerationError::OsError)?; + + Ok(output_file_path) } } diff --git a/ctest-next/src/lib.rs b/ctest-next/src/lib.rs index bc4e5f3375586..1640deb4c707d 100644 --- a/ctest-next/src/lib.rs +++ b/ctest-next/src/lib.rs @@ -15,10 +15,15 @@ mod ast; mod ffi_items; mod generator; mod macro_expansion; +mod runner; +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, generate_test}; +pub use translator::TranslationError; /// A possible error that can be encountered in our library. pub type Error = Box; diff --git a/ctest-next/src/macro_expansion.rs b/ctest-next/src/macro_expansion.rs index c41ad6c71b2c5..4d6f5d7e1cd56 100644 --- a/ctest-next/src/macro_expansion.rs +++ b/ctest-next/src/macro_expansion.rs @@ -9,6 +9,8 @@ pub fn expand>(crate_path: P) -> Result { let output = Command::new(rustc) .env("RUSTC_BOOTSTRAP", "1") .arg("-Zunpretty=expanded") + .arg("--edition") + .arg("2024") // By default, -Zunpretty=expanded uses 2015 edition. .arg(canonicalize(crate_path)?) .output()?; diff --git a/ctest-next/src/runner.rs b/ctest-next/src/runner.rs new file mode 100644 index 0000000000000..1eabf4af5ed9c --- /dev/null +++ b/ctest-next/src/runner.rs @@ -0,0 +1,162 @@ +use crate::{Result, TestGenerator}; +use std::env; +use std::fs::{canonicalize, File}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Generate all tests for the given crate and output the Rust side to a file. +#[doc(hidden)] +pub fn generate_test( + generator: &mut TestGenerator, + crate_path: impl AsRef, + output_file_path: impl AsRef, +) -> Result { + let output_file_path = generator.generate_files(crate_path, output_file_path)?; + + // Search for the target and host to build for if specified manually + // (generator.target, generator.host), + // via build script (TARGET, HOST), or for internal testing (TARGET_PLATFORM, HOST_PLATFORM). + let target = generator.target.clone().unwrap_or_else(|| { + env::var("TARGET").unwrap_or_else(|_| env::var("TARGET_PLATFORM").unwrap()) + }); + let host = env::var("HOST").unwrap_or_else(|_| env::var("HOST_PLATFORM").unwrap()); + + let mut cfg = cc::Build::new(); + // FIXME(ctest): 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 &generator.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(output_file_path) +} + +/// Compiles a Rust source file and links it against a static library. +/// +/// Returns the path to the generated binary. +#[doc(hidden)] +pub fn __compile_test( + output_dir: impl AsRef, + crate_path: impl AsRef, + library_file: impl AsRef, +) -> 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().file_stem().unwrap(); + + 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()); + + // Create a file that contains the Rust 'bindings' as well as the generated test code. + File::create(&rust_file)?.write_all( + format!( + "include!(r#\"{}\"#);\ninclude!(r#\"{}.rs\"#);", + canonicalize(crate_path)?.display(), + library_file.to_str().unwrap() + ) + .as_bytes(), + )?; + + // Compile the test file with the compiled C library file found in `output_dir` + // into a binary file, ignoring all warnings about unused items. (not all items + // are currently tested) + + let mut cmd = Command::new(rustc); + cmd.arg(&rust_file) + .arg(format!("-Lnative={}", output_dir.display())) + .arg(format!("-lstatic={}", library_file.to_str().unwrap())) + .arg("--edition") + .arg("2021") // Defaults to 2015. + .arg("-o") + .arg(&binary_path) + .arg("-Aunused"); + + // Pass in a different target, linker or flags if set, useful for cross compilation. + + let target = env::var("TARGET_PLATFORM").unwrap_or_default(); + if !target.is_empty() { + cmd.arg("--target").arg(target); + } + + let linker = env::var("LINKER").unwrap_or_default(); + if !linker.is_empty() { + cmd.arg(format!("-Clinker={linker}")); + } + + let flags = env::var("FLAGS").unwrap_or_default(); + if !flags.is_empty() { + cmd.args(flags.split_whitespace()); + } + + let output = cmd.output()?; + if !output.status.success() { + return Err(std::str::from_utf8(&output.stderr)?.into()); + } + + Ok(binary_path) +} + +/// Executes the compiled test binary and returns its output. +/// +/// If a RUNNER environment variable is present, it will use that to run the binary. +#[doc(hidden)] +pub fn __run_test>(test_binary: P) -> Result { + let runner = env::var("RUNNER").unwrap_or_default(); + let mut cmd; + if runner.is_empty() { + cmd = Command::new(test_binary.as_ref()); + } else { + let mut args = runner.split_whitespace(); + cmd = Command::new(args.next().unwrap()); + cmd.args(args); + }; + + cmd.arg(test_binary.as_ref()); + let output = cmd.output()?; + + if !output.status.success() { + return Err(std::str::from_utf8(&output.stderr)?.into()); + } + + Ok(std::str::from_utf8(&output.stdout)?.to_string()) +} diff --git a/ctest-next/src/template.rs b/ctest-next/src/template.rs new file mode 100644 index 0000000000000..5b63d66d8e2d3 --- /dev/null +++ b/ctest-next/src/template.rs @@ -0,0 +1,38 @@ +use askama::Template; +use quote::ToTokens; + +use crate::{ffi_items::FfiItems, translator::Translator}; + +/// Represents the Rust side of the generated testing suite. +#[derive(Template, Debug, Clone)] +#[template(path = "test.rs")] +pub(crate) struct RustTestTemplate<'a> { + 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> { + translator: Translator, + 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) -> Self { + Self { ffi_items } + } +} + +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, + translator: Translator::new(), + } + } +} diff --git a/ctest-next/src/tests.rs b/ctest-next/src/tests.rs index c8e7f25e2d062..5548e70543771 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, Result, TranslationError}; use syn::visit::Visit; @@ -28,6 +28,18 @@ extern "C" { } "#; +macro_rules! collect_idents { + ($items:expr) => { + $items.iter().map(|a| a.ident()).collect::>() + }; +} + +fn ty(s: &str) -> Result { + let translator = Translator {}; + let ty: syn::Type = syn::parse_str(s).unwrap(); + translator.translate_type(&ty) +} + #[test] fn test_extraction_ffi_items() { let ast = syn::parse_file(ALL_ITEMS).unwrap(); @@ -35,57 +47,62 @@ 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"]); +} +#[test] +fn test_translation_type_ptr() { assert_eq!( - ffi_items - .constants() - .iter() - .map(|a| a.ident()) - .collect::>(), - ["bar"] + ty("*const *mut i32").unwrap(), + "int32_t * const*".to_string() ); - assert_eq!( - ffi_items - .foreign_functions() - .iter() - .map(|a| a.ident()) - .collect::>(), - ["malloc"] + ty("*const [u128; 2 + 3]").unwrap(), + "unsigned __int128 (*const) [2 + 3]".to_string() ); + // FIXME(ctest): While not a valid C type, it will be used to + // generate a valid test in the future. + // assert_eq!( + // ty("*const *mut [u8; 5]").unwrap(), + // "uint8_t (*const *) [5]".to_string() + // ); +} +#[test] +fn test_translation_type_reference() { + assert_eq!(ty("&u8").unwrap(), "const uint8_t*".to_string()); + assert_eq!(ty("&&u8").unwrap(), "const uint8_t* const*".to_string()); + assert_eq!(ty("*mut &u8").unwrap(), "const uint8_t* *".to_string()); + assert_eq!(ty("& &mut u8").unwrap(), "uint8_t* const*".to_string()); +} + +#[test] +fn test_translation_type_bare_fn() { assert_eq!( - ffi_items - .foreign_statics() - .iter() - .map(|a| a.ident()) - .collect::>(), - ["baz"] + ty("fn(*mut u8, i16) -> *const char").unwrap(), + "char const*(*)(uint8_t *, int16_t)".to_string() ); - assert_eq!( - ffi_items - .structs() - .iter() - .map(|a| a.ident()) - .collect::>(), - ["Array"] + ty("*const fn(*mut u8, &mut [u8; 16]) -> &mut *mut u8").unwrap(), + "uint8_t * *(*const)(uint8_t *, uint8_t (*) [16])".to_string() ); +} +#[test] +fn test_translation_type_array() { assert_eq!( - ffi_items - .unions() - .iter() - .map(|a| a.ident()) - .collect::>(), - ["Word"] + ty("[&u8; 2 + 2]").unwrap(), + "const uint8_t*[2 + 2]".to_string() ); } + +#[test] +fn test_translation_fails_for_unsupported() { + assert!(ty("[&str; 2 + 2]").is_err()); + assert!(ty("fn(*mut [u8], i16) -> *const char").is_err()); +} diff --git a/ctest-next/src/translator.rs b/ctest-next/src/translator.rs new file mode 100644 index 0000000000000..e1b6f03122061 --- /dev/null +++ b/ctest-next/src/translator.rs @@ -0,0 +1,327 @@ +//! Translation of Rust types to C for test generation. +//! +//! Simple to semi complex types are supported only. + +use std::{fmt, ops::Deref}; + +use proc_macro2::Span; +use quote::ToTokens; +use syn::spanned::Spanned; +use thiserror::Error; + +/// An error that occurs during translation, detailing cause and location. +#[derive(Debug)] +pub struct TranslationError { + kind: TranslationErrorKind, + source: String, + #[expect(unused)] + span: Span, +} + +impl TranslationError { + /// Create a new translation error. + pub(crate) fn new(kind: TranslationErrorKind, source: &str, span: Span) -> Self { + Self { + kind, + source: source.to_string(), + span, + } + } +} + +impl fmt::Display for TranslationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}: `{}`", + self.kind, + self.source, + // FIXME(ctest): Not yet stable, see: + // https://github.com/dtolnay/proc-macro2/issues/503 + // self.span.file(), + // self.span.start().line, + // self.span.start().column, + ) + } +} + +/// Errors that can occur during the translation of a type. +#[derive(Debug, Error, PartialEq, Eq)] +pub(crate) enum TranslationErrorKind { + /// The provided type is unknown or unrecognized. + #[error("unsupported type")] + UnsupportedType, + + /// A reference to a non-primitive type was encountered, which is not supported. + #[error("references to non-primitive types are not allowed")] + NonPrimitiveReference, + + /// Variadic functions or parameters were found, which cannot be handled. + #[error("variadics cannot be translated")] + HasVariadics, + + /// Lifetimes were found in the type or function signature, which are not supported. + #[error("lifetimes cannot be translated")] + HasLifetimes, + + /// A type that is not ffi compatible was found. + #[error("this type is not guaranteed to have a C compatible layout. See improper_ctypes_definitions lint")] + NotFfiCompatible, +} + +#[derive(Clone, 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() + } + + /// Translate mutability from Rust to C. + fn translate_mut(&self, mutability: Option) -> String { + mutability.map(|_| "").unwrap_or("const").to_string() + } + + /// Translate a Rust type into its equivalent C type. + pub(crate) fn translate_type(&self, ty: &syn::Type) -> Result { + 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() => Ok("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(_) => Ok("void".to_string()), + syn::Type::Slice(slice) => Err(TranslationError::new( + TranslationErrorKind::NotFfiCompatible, + &slice.to_token_stream().to_string(), + slice.span(), + )), + syn::Type::Paren(paren) => self.translate_type(&paren.elem), + syn::Type::Group(group) => self.translate_type(&group.elem), + ty => Err(TranslationError::new( + TranslationErrorKind::UnsupportedType, + &ty.to_token_stream().to_string(), + ty.span(), + )), + } + } + + /// Translate a Rust reference to its C equivalent. + fn translate_reference( + &self, + reference: &syn::TypeReference, + ) -> Result { + let modifier = self.translate_mut(reference.mutability); + + match reference.elem.deref() { + syn::Type::Path(path) => { + let last_segment = path.path.segments.last().unwrap(); + let ident = last_segment.ident.to_string(); + + match ident.as_str() { + "str" => { + // &str is not ABI safe and should not be supported. + Err(TranslationError::new( + TranslationErrorKind::NotFfiCompatible, + "&str", + path.span(), + )) + } + c if is_rust_primitive(c) => { + let base_type = self.translate_primitive_type(&last_segment.ident); + Ok(format!("{modifier} {base_type}*").trim().to_string()) + } + _ => Err(TranslationError::new( + TranslationErrorKind::NonPrimitiveReference, + &ident, + path.span(), + )), + } + } + syn::Type::Array(arr) => { + let len = translate_expr(&arr.len); + let ty = self.translate_type(arr.elem.deref())?; + let inner_type = format!("{ty} (*) [{len}]"); + Ok(inner_type + .replacen("(*)", &format!("(*{modifier})"), 1) + .trim() + .to_string()) + } + syn::Type::BareFn(_) => { + let inner_type = self.translate_type(reference.elem.deref())?; + Ok(inner_type + .replacen("(*)", &format!("(*{modifier})"), 1) + .trim() + .to_string()) + } + syn::Type::Reference(_) | syn::Type::Ptr(_) => { + let inner_type = self.translate_type(reference.elem.deref())?; + if inner_type.contains("(*)") { + Ok(inner_type + .replacen("(*)", &format!("(*{modifier})"), 1) + .trim() + .to_string()) + } else { + Ok(format!("{inner_type} {modifier}*").trim().to_string()) + } + } + _ => Err(TranslationError::new( + TranslationErrorKind::UnsupportedType, + &reference.elem.to_token_stream().to_string(), + reference.elem.span(), + )), + } + } + + /// Translate a Rust function pointer type to its C equivalent. + fn translate_bare_fn(&self, function: &syn::TypeBareFn) -> Result { + if function.lifetimes.is_some() { + return Err(TranslationError::new( + TranslationErrorKind::HasLifetimes, + &function.to_token_stream().to_string(), + function.span(), + )); + } + if function.variadic.is_some() { + return Err(TranslationError::new( + TranslationErrorKind::HasVariadics, + &function.to_token_stream().to_string(), + function.span(), + )); + } + + let mut parameters = function + .inputs + .iter() + .map(|arg| self.translate_type(&arg.ty)) + .collect::, TranslationError>>()?; + + 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("(*)") { + let params = parameters.join(", "); + Ok(return_type.replacen("(*)", &format!("(*(*)({params}))"), 1)) + } else { + Ok(format!("{return_type}(*)({})", parameters.join(", "))) + } + } + + /// Translate a Rust primitive type into its C equivalent. + fn translate_primitive_type(&self, ty: &syn::Ident) -> String { + match ty.to_string().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(), + "u128" => "unsigned __int128".to_string(), + "i8" => "int8_t".to_string(), + "i16" => "int16_t".to_string(), + "i32" => "int32_t".to_string(), + "i64" => "int64_t".to_string(), + "i128" => "__int128".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.as_str() { + "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(), + } + } + // Pass typedefs as is. + s => s.to_string(), + } + } + + /// Translate a Rust path into its C equivalent. + fn translate_path(&self, path: &syn::TypePath) -> Result { + let last = path.path.segments.last().unwrap(); + Ok(self.translate_primitive_type(&last.ident)) + } + + /// Translate a Rust array declaration into its C equivalent. + fn translate_array(&self, array: &syn::TypeArray) -> Result { + Ok(format!( + "{}[{}]", + self.translate_type(array.elem.deref())?, + translate_expr(&array.len) + )) + } + + /// Translate a Rust pointer into its equivalent C pointer. + fn translate_ptr(&self, ptr: &syn::TypePtr) -> Result { + let modifier = self.translate_mut(ptr.mutability); + let inner = ptr.elem.deref(); + + match inner { + syn::Type::BareFn(_) => { + let inner_type = self.translate_type(ptr.elem.deref())?; + Ok(inner_type + .replacen("(*)", &format!("(*{modifier})"), 1) + .trim() + .to_string()) + } + syn::Type::Array(arr) => { + let len = translate_expr(&arr.len); + let ty = self.translate_type(arr.elem.deref())?; + let inner_type = format!("{ty} (*) [{len}]"); + Ok(inner_type + .replacen("(*)", &format!("(*{modifier})"), 1) + .trim() + .to_string()) + } + syn::Type::Reference(_) | syn::Type::Ptr(_) => { + let inner_type = self.translate_type(ptr.elem.deref())?; + if inner_type.contains("(*)") { + Ok(inner_type + .replacen("(*)", &format!("(*{modifier} *)"), 1) + .trim() + .to_string()) + } else { + Ok(format!("{inner_type} {modifier}*").trim().to_string()) + } + } + _ => { + let inner_type = self.translate_type(inner)?; + Ok(format!("{inner_type} {modifier}*")) + } + } + } +} + +/// Translate a simple Rust expression to C. +/// +/// This function will just pass the expression as is in most cases. +fn translate_expr(expr: &syn::Expr) -> String { + match expr { + syn::Expr::Path(p) => p.path.segments.last().unwrap().ident.to_string(), + syn::Expr::Cast(c) => translate_expr(c.expr.deref()), + expr => expr.to_token_stream().to_string(), + } +} + +/// Return whether a type is a Rust primitive type. +fn is_rust_primitive(ty: &str) -> bool { + let rustc_types = [ + "usize", "u8", "u16", "u32", "u64", "u128", "isize", "i8", "i16", "i32", "i64", "i128", + "f32", "f64", + ]; + ty.starts_with("c_") || rustc_types.contains(&ty) +} diff --git a/ctest-next/templates/test.c b/ctest-next/templates/test.c new file mode 100644 index 0000000000000..46e415a49bb8d --- /dev/null +++ b/ctest-next/templates/test.c @@ -0,0 +1,25 @@ +/* This file was autogenerated by ctest; do not modify directly */ +{#- ↑ Doesn't apply here, this is the template! +#} + +#include +#include +#include +#include + +{%- for header in headers +%} +#include <{{ header }}> +{%- endfor +%} + +{%- for constant in ffi_items.constants() +%} +{%- let c_type = translator.translate_type(constant.ty).unwrap() +%} +{%- let ident = constant.ident() +%} + +static {{ c_type }} __test_const_{{ ident }}_val = {{ ident }}; + +// Define a function that returns a pointer to the value of the constant to test. +// This will later be called on the Rust side via FFI. +{{ c_type }}* __test_const_{{ ident }}(void) { + return &__test_const_{{ ident }}_val; +} +{%- endfor +%} + diff --git a/ctest-next/templates/test.rs b/ctest-next/templates/test.rs new file mode 100644 index 0000000000000..f73f927f646b7 --- /dev/null +++ b/ctest-next/templates/test.rs @@ -0,0 +1,112 @@ +/* This file was autogenerated by ctest; do not modify directly */ +{#- ↑ Doesn't apply here, this is the template! +#} + +/// As this file is sometimes built using rustc, crate level attributes +/// are not allowed at the top-level, so we hack around this by keeping it +/// inside of a module. +mod generated_tests { + #![allow(non_snake_case)] + #![deny(improper_ctypes_definitions)] + use std::ffi::CStr; + use std::fmt::{Debug, LowerHex}; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::{mem, ptr, slice}; + + use super::*; + + pub static FAILED: AtomicBool = AtomicBool::new(false); + pub static NTESTS: AtomicUsize = AtomicUsize::new(0); + + /// Check that the value returned from the Rust and C side in a certain test is equivalent. + /// + /// Internally it will remember which checks failed and how many tests have been run. + fn check_same(rust: T, c: T, attr: &str) { + if rust != c { + eprintln!("bad {attr}: rust: {rust:?} != c {c:?}"); + FAILED.store(true, Ordering::Relaxed); + } else { + NTESTS.fetch_add(1, Ordering::Relaxed); + } + } + + /// Check that the value returned from the Rust and C side in a certain test is equivalent. + /// + /// Internally it will remember which checks failed and how many tests have been run. This + /// method is the same as `check_same` but prints errors in bytes in hex. + fn check_same_hex(rust: T, c: T, attr: &str) { + if rust != c { + eprintln!("bad {attr}: rust: {rust:?} ({rust:#x}) != c {c:?} ({c:#x})"); + FAILED.store(true, Ordering::Relaxed); + } else { + NTESTS.fetch_add(1, Ordering::Relaxed); + } + } + + {%- for constant in ffi_items.constants() +%} + {%- let ty = constant.ty.to_token_stream().to_string() +%} + {%- let ident = constant.ident() +%} + + {%- if ty == "* const c_char" +%} + // Test that the string constant is the same in both Rust and C. + // While fat pointers can't be translated, we instead of * const c_char. + pub fn const_{{ ident }}() { + extern "C" { + fn __test_const_{{ ident }}() -> *const *const u8; + } + let val = {{ ident }}; + unsafe { + let ptr = *__test_const_{{ ident }}(); + // c_char can be i8 or u8, so just cast to i8. + let val = CStr::from_ptr(ptr.cast::()); + let val = val.to_str().expect("const {{ ident }} not utf8"); + let c = ::std::ffi::CStr::from_ptr(ptr as *const _); + let c = c.to_str().expect("const {{ ident }} not utf8"); + check_same(val, c, "{{ ident }} string"); + } + } + {%- else +%} + // Test that the value of the constant is the same in both Rust and C. + // This performs a byte by byte comparision of the constant value. + pub fn const_{{ ident }}() { + extern "C" { + fn __test_const_{{ ident }}() -> *const {{ ty }}; + } + let val = {{ ident }}; + unsafe { + let ptr1 = ptr::from_ref(&val).cast::(); + let ptr2 = __test_const_{{ ident }}().cast::(); + let ptr1_bytes = slice::from_raw_parts(ptr1, mem::size_of::<{{ ty }}>()); + let ptr2_bytes = slice::from_raw_parts(ptr2, mem::size_of::<{{ ty }}>()); + for (i, (&b1, &b2)) in ptr1_bytes.iter().zip(ptr2_bytes.iter()).enumerate() { + // HACK: This may read uninitialized data! We do this because + // there isn't a good way to recursively iterate all fields. + check_same_hex(b1, b2, &format!("{{ ident }} value at byte {}", i)); + } + } + } + {%- endif +%} + {%- endfor +%} +} + +use generated_tests::*; + +fn main() { + println!("RUNNING ALL TESTS"); + run_all(); + if FAILED.load(std::sync::atomic::Ordering::Relaxed) { + panic!("some tests failed"); + } else { + println!( + "PASSED {} tests", + NTESTS.load(std::sync::atomic::Ordering::Relaxed) + ); + } +} + +// Run all tests by calling the functions that define them. +fn run_all() { + {%- for constant in ffi_items.constants() +%} + const_{{ constant.ident() }}(); + {%- endfor +%} +} + diff --git a/ctest-next/tests/basic.rs b/ctest-next/tests/basic.rs index 42d9a139d566c..4880ed41fb338 100644 --- a/ctest-next/tests/basic.rs +++ b/ctest-next/tests/basic.rs @@ -1,39 +1,119 @@ -use ctest_next::TestGenerator; +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use pretty_assertions::assert_eq; + +use ctest_next::{Result, TestGenerator, __compile_test, __run_test, generate_test}; + +// 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. +/// +/// The files will be generated in a unique temporary directory that gets +/// deleted when it goes out of scope. +fn default_generator(opt_level: u8, header: &str) -> Result<(TestGenerator, tempfile::TempDir)> { + env::set_var("OPT_LEVEL", opt_level.to_string()); + let temp_dir = tempfile::tempdir()?; + let mut generator = TestGenerator::new(); + + Ok(( + generator + .out_dir(&temp_dir) + .include("tests/input") + .header(header) + .to_owned(), + temp_dir, + )) +} + +/// Assert whether the contents of two files match. +/// +/// If the contents do not match and LIBC_BLESS is set, overwrite the +/// test file with the content of the generated file. +fn bless_equal(new_file: impl AsRef, old_file: impl AsRef) { + let new_content = fs::read_to_string(&new_file).unwrap().replace("\r", ""); + let old_content = fs::read_to_string(&old_file).unwrap().replace("\r", ""); + + let equal = new_content != old_content; + if env::var("LIBC_BLESS").is_ok() && !equal { + fs::write(old_file, &new_content).unwrap(); + } else { + // Use pretty_assertions for easier diffs. + assert_eq!(new_content, old_content); + } +} + +/// Generate test files for the given header and crate path and compare with pregenerated test files. +/// +/// If LIBC_BLESS is set, it will overwrite the pregenerated files with the new ones. +/// Additionally, if this test is not being ran on a cross compiled target, it will compile +/// and run the generated tests as well. +fn check_entrypoint( + header_name: &str, + crate_path: impl AsRef, + library_path: impl AsRef, + include_path: impl AsRef, +) { + let (mut gen, out_dir) = default_generator(1, header_name).unwrap(); + let output_file = gen.generate_files(&crate_path, &library_path).unwrap(); + + let rs = include_path + .as_ref() + .join(library_path.as_ref().with_extension("rs")); + let c = include_path + .as_ref() + .join(library_path.as_ref().with_extension("c")); + + bless_equal(output_file.with_extension("rs"), rs); + bless_equal(output_file.with_extension("c"), c); + + if env::var("TARGET_PLATFORM") == env::var("HOST_PLATFORM") { + generate_test(&mut gen, &crate_path, &library_path).unwrap(); + let test_binary = __compile_test(&out_dir, crate_path, library_path).unwrap(); + let result = __run_test(test_binary); + if let Err(err) = &result { + eprintln!("Test failed: {err:?}"); + } + assert!(result.is_ok()); + } +} #[test] fn test_entrypoint_hierarchy() { - let mut generator = TestGenerator::new(); + let include_path = PathBuf::from("tests/input"); + let crate_path = include_path.join("hierarchy/lib.rs"); + let library_path = "hierarchy.out.a"; - generator - .generate("./tests/input/hierarchy/lib.rs", "hierarchy_out.rs") - .unwrap(); + check_entrypoint("hierarchy.h", crate_path, library_path, include_path); } #[test] fn test_entrypoint_simple() { - let mut generator = TestGenerator::new(); + let include_path = PathBuf::from("tests/input"); + let crate_path = include_path.join("simple.rs"); + let library_path = "simple.out.a"; - generator - .generate("./tests/input/simple.rs", "simple_out.rs") - .unwrap(); + check_entrypoint("simple.h", crate_path, library_path, include_path); } #[test] fn test_entrypoint_macro() { - let mut generator = TestGenerator::new(); + let include_path = PathBuf::from("tests/input"); + let crate_path = include_path.join("macro.rs"); + let library_path = "macro.out.a"; - generator - .generate("./tests/input/macro.rs", "macro_out.rs") - .unwrap(); + check_entrypoint("macro.h", crate_path, library_path, include_path); } #[test] fn test_entrypoint_invalid_syntax() { - let mut generator = TestGenerator::new(); + let crate_path = "tests/input/invalid_syntax.rs"; + let mut gen = TestGenerator::new(); - let fails = generator - .generate("./tests/input/invalid_syntax.rs", "invalid_syntax_out.rs") - .is_err(); + let fails = generate_test(&mut gen, crate_path, "invalid_syntax.out").is_err(); assert!(fails) } 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.out.c b/ctest-next/tests/input/hierarchy.out.c new file mode 100644 index 0000000000000..0574cbc03c6f1 --- /dev/null +++ b/ctest-next/tests/input/hierarchy.out.c @@ -0,0 +1,15 @@ +/* This file was autogenerated by ctest; do not modify directly */ + +#include +#include +#include +#include +#include + +static bool __test_const_ON_val = ON; + +// Define a function that returns a pointer to the value of the constant to test. +// This will later be called on the Rust side via FFI. +bool* __test_const_ON(void) { + return &__test_const_ON_val; +} diff --git a/ctest-next/tests/input/hierarchy.out.rs b/ctest-next/tests/input/hierarchy.out.rs new file mode 100644 index 0000000000000..cd335d73bd9f3 --- /dev/null +++ b/ctest-next/tests/input/hierarchy.out.rs @@ -0,0 +1,82 @@ +/* This file was autogenerated by ctest; do not modify directly */ + +/// As this file is sometimes built using rustc, crate level attributes +/// are not allowed at the top-level, so we hack around this by keeping it +/// inside of a module. +mod generated_tests { + #![allow(non_snake_case)] + #![deny(improper_ctypes_definitions)] + use std::ffi::CStr; + use std::fmt::{Debug, LowerHex}; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::{mem, ptr, slice}; + + use super::*; + + pub static FAILED: AtomicBool = AtomicBool::new(false); + pub static NTESTS: AtomicUsize = AtomicUsize::new(0); + + /// Check that the value returned from the Rust and C side in a certain test is equivalent. + /// + /// Internally it will remember which checks failed and how many tests have been run. + fn check_same(rust: T, c: T, attr: &str) { + if rust != c { + eprintln!("bad {attr}: rust: {rust:?} != c {c:?}"); + FAILED.store(true, Ordering::Relaxed); + } else { + NTESTS.fetch_add(1, Ordering::Relaxed); + } + } + + /// Check that the value returned from the Rust and C side in a certain test is equivalent. + /// + /// Internally it will remember which checks failed and how many tests have been run. This + /// method is the same as `check_same` but prints errors in bytes in hex. + fn check_same_hex(rust: T, c: T, attr: &str) { + if rust != c { + eprintln!("bad {attr}: rust: {rust:?} ({rust:#x}) != c {c:?} ({c:#x})"); + FAILED.store(true, Ordering::Relaxed); + } else { + NTESTS.fetch_add(1, Ordering::Relaxed); + } + } + // Test that the value of the constant is the same in both Rust and C. + // This performs a byte by byte comparision of the constant value. + pub fn const_ON() { + extern "C" { + fn __test_const_ON() -> *const bool; + } + let val = ON; + unsafe { + let ptr1 = ptr::from_ref(&val).cast::(); + let ptr2 = __test_const_ON().cast::(); + let ptr1_bytes = slice::from_raw_parts(ptr1, mem::size_of::()); + let ptr2_bytes = slice::from_raw_parts(ptr2, mem::size_of::()); + for (i, (&b1, &b2)) in ptr1_bytes.iter().zip(ptr2_bytes.iter()).enumerate() { + // HACK: This may read uninitialized data! We do this because + // there isn't a good way to recursively iterate all fields. + check_same_hex(b1, b2, &format!("ON value at byte {}", i)); + } + } + } +} + +use generated_tests::*; + +fn main() { + println!("RUNNING ALL TESTS"); + run_all(); + if FAILED.load(std::sync::atomic::Ordering::Relaxed) { + panic!("some tests failed"); + } else { + println!( + "PASSED {} tests", + NTESTS.load(std::sync::atomic::Ordering::Relaxed) + ); + } +} + +// Run all tests by calling the functions that define them. +fn run_all() { + const_ON(); +} 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..2b0ef6b80e351 --- /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; +}; diff --git a/ctest-next/tests/input/macro.out.c b/ctest-next/tests/input/macro.out.c new file mode 100644 index 0000000000000..736c06b8291bd --- /dev/null +++ b/ctest-next/tests/input/macro.out.c @@ -0,0 +1,7 @@ +/* This file was autogenerated by ctest; do not modify directly */ + +#include +#include +#include +#include +#include diff --git a/ctest-next/tests/input/macro.out.rs b/ctest-next/tests/input/macro.out.rs new file mode 100644 index 0000000000000..61c7b4a3a4f91 --- /dev/null +++ b/ctest-next/tests/input/macro.out.rs @@ -0,0 +1,62 @@ +/* This file was autogenerated by ctest; do not modify directly */ + +/// As this file is sometimes built using rustc, crate level attributes +/// are not allowed at the top-level, so we hack around this by keeping it +/// inside of a module. +mod generated_tests { + #![allow(non_snake_case)] + #![deny(improper_ctypes_definitions)] + use std::ffi::CStr; + use std::fmt::{Debug, LowerHex}; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::{mem, ptr, slice}; + + use super::*; + + pub static FAILED: AtomicBool = AtomicBool::new(false); + pub static NTESTS: AtomicUsize = AtomicUsize::new(0); + + /// Check that the value returned from the Rust and C side in a certain test is equivalent. + /// + /// Internally it will remember which checks failed and how many tests have been run. + fn check_same(rust: T, c: T, attr: &str) { + if rust != c { + eprintln!("bad {attr}: rust: {rust:?} != c {c:?}"); + FAILED.store(true, Ordering::Relaxed); + } else { + NTESTS.fetch_add(1, Ordering::Relaxed); + } + } + + /// Check that the value returned from the Rust and C side in a certain test is equivalent. + /// + /// Internally it will remember which checks failed and how many tests have been run. This + /// method is the same as `check_same` but prints errors in bytes in hex. + fn check_same_hex(rust: T, c: T, attr: &str) { + if rust != c { + eprintln!("bad {attr}: rust: {rust:?} ({rust:#x}) != c {c:?} ({c:#x})"); + FAILED.store(true, Ordering::Relaxed); + } else { + NTESTS.fetch_add(1, Ordering::Relaxed); + } + } +} + +use generated_tests::*; + +fn main() { + println!("RUNNING ALL TESTS"); + run_all(); + if FAILED.load(std::sync::atomic::Ordering::Relaxed) { + panic!("some tests failed"); + } else { + println!( + "PASSED {} tests", + NTESTS.load(std::sync::atomic::Ordering::Relaxed) + ); + } +} + +// Run all tests by calling the functions that define them. +fn run_all() { +} diff --git a/ctest-next/tests/input/simple.h b/ctest-next/tests/input/simple.h new file mode 100644 index 0000000000000..446be60e87c75 --- /dev/null +++ b/ctest-next/tests/input/simple.h @@ -0,0 +1,17 @@ +#include + +typedef uint8_t Byte; + +struct Person +{ + const char *name; + uint8_t age; +}; + +union Word +{ + uint16_t word; + Byte byte[2]; +}; + +#define A "abc" diff --git a/ctest-next/tests/input/simple.out.c b/ctest-next/tests/input/simple.out.c new file mode 100644 index 0000000000000..94df4ec988166 --- /dev/null +++ b/ctest-next/tests/input/simple.out.c @@ -0,0 +1,15 @@ +/* This file was autogenerated by ctest; do not modify directly */ + +#include +#include +#include +#include +#include + +static char const* __test_const_A_val = A; + +// Define a function that returns a pointer to the value of the constant to test. +// This will later be called on the Rust side via FFI. +char const** __test_const_A(void) { + return &__test_const_A_val; +} diff --git a/ctest-next/tests/input/simple.out.rs b/ctest-next/tests/input/simple.out.rs new file mode 100644 index 0000000000000..078b763a69f5c --- /dev/null +++ b/ctest-next/tests/input/simple.out.rs @@ -0,0 +1,80 @@ +/* This file was autogenerated by ctest; do not modify directly */ + +/// As this file is sometimes built using rustc, crate level attributes +/// are not allowed at the top-level, so we hack around this by keeping it +/// inside of a module. +mod generated_tests { + #![allow(non_snake_case)] + #![deny(improper_ctypes_definitions)] + use std::ffi::CStr; + use std::fmt::{Debug, LowerHex}; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::{mem, ptr, slice}; + + use super::*; + + pub static FAILED: AtomicBool = AtomicBool::new(false); + pub static NTESTS: AtomicUsize = AtomicUsize::new(0); + + /// Check that the value returned from the Rust and C side in a certain test is equivalent. + /// + /// Internally it will remember which checks failed and how many tests have been run. + fn check_same(rust: T, c: T, attr: &str) { + if rust != c { + eprintln!("bad {attr}: rust: {rust:?} != c {c:?}"); + FAILED.store(true, Ordering::Relaxed); + } else { + NTESTS.fetch_add(1, Ordering::Relaxed); + } + } + + /// Check that the value returned from the Rust and C side in a certain test is equivalent. + /// + /// Internally it will remember which checks failed and how many tests have been run. This + /// method is the same as `check_same` but prints errors in bytes in hex. + fn check_same_hex(rust: T, c: T, attr: &str) { + if rust != c { + eprintln!("bad {attr}: rust: {rust:?} ({rust:#x}) != c {c:?} ({c:#x})"); + FAILED.store(true, Ordering::Relaxed); + } else { + NTESTS.fetch_add(1, Ordering::Relaxed); + } + } + // Test that the string constant is the same in both Rust and C. + // While fat pointers can't be translated, we instead of * const c_char. + pub fn const_A() { + extern "C" { + fn __test_const_A() -> *const *const u8; + } + let val = A; + unsafe { + let ptr = *__test_const_A(); + // c_char can be i8 or u8, so just cast to i8. + let val = CStr::from_ptr(ptr.cast::()); + let val = val.to_str().expect("const A not utf8"); + let c = ::std::ffi::CStr::from_ptr(ptr as *const _); + let c = c.to_str().expect("const A not utf8"); + check_same(val, c, "A string"); + } + } +} + +use generated_tests::*; + +fn main() { + println!("RUNNING ALL TESTS"); + run_all(); + if FAILED.load(std::sync::atomic::Ordering::Relaxed) { + panic!("some tests failed"); + } else { + println!( + "PASSED {} tests", + NTESTS.load(std::sync::atomic::Ordering::Relaxed) + ); + } +} + +// Run all tests by calling the functions that define them. +fn run_all() { + const_A(); +} diff --git a/ctest-next/tests/input/simple.rs b/ctest-next/tests/input/simple.rs index e62b4e927dd8a..be58e89e7e8be 100644 --- a/ctest-next/tests/input/simple.rs +++ b/ctest-next/tests/input/simple.rs @@ -13,3 +13,5 @@ pub union Word { word: u16, byte: [Byte; 2], } + +const A: *const c_char = c"abc".as_ptr();