Skip to content

ctest: Add translation of Rust types. #4501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ctest-next/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 3 additions & 0 deletions ctest-next/askama.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[[escaper]]
path = "askama::filters::Text"
extensions = ["rs", "c", "cpp"]
69 changes: 69 additions & 0 deletions ctest-next/build.rs
Original file line number Diff line number Diff line change
@@ -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()
);
}
1 change: 0 additions & 1 deletion ctest-next/src/ast/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion ctest-next/src/ffi_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Const> {
&self.constants
}
Expand Down
121 changes: 112 additions & 9 deletions ctest-next/src/generator.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,131 @@
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<String>,
pub(crate) target: Option<String>,
pub(crate) includes: Vec<PathBuf>,
out_dir: Option<PathBuf>,
}

impl TestGenerator {
/// Creates a new blank test generator.
pub fn new() -> Self {
Self::default()
}

/// Generate all tests for the given crate and output the Rust side to a file.
pub fn generate<P: AsRef<Path>>(&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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For each generated file, you should do a regex to replace \n{3,} with \n\n to get rid of all the extra whitespace that the templates seem to insert.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, it looks like Askama has #[template(whitespace = "suppress")] or #[template(whitespace = "minimize")]. Might be worth playing with before the regex thing.

crate_path: impl AsRef<Path>,
output_file_path: impl AsRef<Path>,
) -> Result<PathBuf, GenerationError> {
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)
}
}
5 changes: 5 additions & 0 deletions ctest-next/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error>;
Expand Down
2 changes: 2 additions & 0 deletions ctest-next/src/macro_expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub fn expand<P: AsRef<Path>>(crate_path: P) -> Result<String> {
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()?;

Expand Down
Loading
Loading