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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions ctest-next/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
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"]
20 changes: 20 additions & 0 deletions ctest-next/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use std::env;

fn main() {
println!(
"cargo:rustc-env={}={}",
"HOST_PLATFORM",
env::var("HOST").unwrap()
);
println!(
"cargo:rustc-env={}={}",
"TARGET_PLATFORM",
env::var("TARGET").unwrap()
);
println!(
"cargo:rustc-env={}={}",
"LINKER",
env::var("CC").unwrap_or("".to_string())
);
println!("cargo:rerun-if-changed-env=TARGET")
}
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
141 changes: 136 additions & 5 deletions ctest-next/src/generator.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,159 @@
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<String>,
target: Option<String>,
host: Option<String>,
includes: Vec<PathBuf>,
out_dir: Option<PathBuf>,
}

impl TestGenerator {
/// Creates a new blank test generator.
pub fn new() -> Self {
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<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 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<()> {
pub fn generate<P: AsRef<Path>>(&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<P: AsRef<Path>>(
&mut self,
crate_path: P,
output_file_path: P,
) -> Result<PathBuf> {
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)
}
}
6 changes: 6 additions & 0 deletions ctest-next/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error>;
Expand Down
70 changes: 70 additions & 0 deletions ctest-next/src/runner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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<P: AsRef<Path>>(
output_dir: P,
crate_path: P,
library_file: P,
) -> Result<PathBuf> {
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 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<P: AsRef<Path>>(test_binary: P) -> Result<String> {
let output = Command::new(test_binary.as_ref()).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())
}
Loading
Loading