Skip to content

Commit 9ba4f41

Browse files
authored
Merge pull request #4501 from mbyx/ctest-c-translation
ctest: Add translation of Rust types.
2 parents 59204ee + cad9438 commit 9ba4f41

26 files changed

+1324
-70
lines changed

ctest-next/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,13 @@ repository = "https://github.com/rust-lang/libc"
88
publish = false
99

1010
[dependencies]
11+
askama = "0.14.0"
1112
cc = "1.2.25"
13+
proc-macro2 = { version = "1.0.95", features = ["span-locations"] }
14+
quote = "1.0.40"
1215
syn = { version = "2.0.101", features = ["full", "visit", "extra-traits"] }
16+
thiserror = "2.0.12"
17+
18+
[dev-dependencies]
19+
pretty_assertions = "1.4.1"
20+
tempfile = "3.20.0"

ctest-next/askama.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[[escaper]]
2+
path = "askama::filters::Text"
3+
extensions = ["rs", "c", "cpp"]

ctest-next/build.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use std::env;
2+
3+
// When we call `cargo test` for a cross compiled target, the following is required:
4+
// - CARGO_TARGET_{}_LINKER: To link the integration tests.
5+
// - CARGO_TARGET_{}_RUNNER: To run the integration tests.
6+
//
7+
// This is already set by the CI for all platforms, so there is no problem up till here.
8+
//
9+
// The integration tests (which are run in qemu, but use host rustc and cc) require the
10+
// following:
11+
// - TARGET_PLATFORM or target set manually. (We forward TARGET in build.rs for this.)
12+
// - HOST_PLATFORM or host set manually. (We forward HOST in build.rs for this.)
13+
// - LINKER: To link the C headers. (We forward CARGO_TARGET_{}_LINKER for this.)
14+
// - FLAGS: Any flags to pass when compiling the test binary for the cross compiled platform.
15+
// (Forwarded from CARGO_TARGET_{}_RUSTFLAGS)
16+
// - RUNNER: To run the test binary with. (Forward the same runner as CARGO_TARGET_{}_RUNNER)
17+
//
18+
// The TARGET_PLATFORM and HOST_PLATFORM variables are not an issue, cargo will automatically set
19+
// TARGET and PLATFORM and we will forward them.
20+
//
21+
// Similarly FLAGS and RUNNER are also not an issue, if CARGO_TARGET_{}_RUSTFLAGS are present
22+
// they're forwarded. And RUSTFLAGS works by default anyway. Similarly the test binary doesn't
23+
// require any external applications so just the RUNNER is enough to run it.
24+
//
25+
// However since rustc and cc are the host versions, they will only work if we specify the
26+
// correct variables for them. Because we only use them to compile, not run things. For CC we
27+
// MUST specify CC or CC_target otherwise it will fail. (Other flags like AR etc. work without
28+
// forwarding because it is run in the host.) For rustc we MUST specify the correct linker.
29+
// Usually this is the same as CC or CC_target.
30+
//
31+
// In the CI, the CARGO_TARGET_{} variables are always set.
32+
33+
fn main() {
34+
let host = env::var("HOST").unwrap();
35+
let target = env::var("TARGET").unwrap();
36+
let target_key = target.replace('-', "_").to_uppercase();
37+
38+
println!("cargo:rustc-env=HOST_PLATFORM={host}");
39+
println!("cargo:rerun-if-changed-env=HOST");
40+
41+
println!("cargo:rustc-env=TARGET_PLATFORM={target}");
42+
println!("cargo:rerun-if-changed-env=TARGET");
43+
44+
let link_var = format!("CARGO_TARGET_{target_key}_LINKER");
45+
println!("cargo:rerun-if-changed-env={link_var}");
46+
if let Ok(linker) = env::var(link_var) {
47+
println!("cargo:rustc-env=LINKER={linker}");
48+
}
49+
50+
let run_var = format!("CARGO_TARGET_{target_key}_RUNNER");
51+
println!("cargo:rerun-if-changed-env={run_var}");
52+
if let Ok(runner) = env::var(run_var) {
53+
println!("cargo:rustc-env=RUNNER={runner}");
54+
}
55+
56+
// As we invoke rustc directly this does not get passed to it, although RUSTFLAGS does.
57+
let flag_var = format!("CARGO_TARGET_{target_key}_RUSTFLAGS");
58+
println!("cargo:rerun-if-changed-env={flag_var}");
59+
if let Ok(flags) = env::var(flag_var) {
60+
println!("cargo:rustc-env=FLAGS={flags}");
61+
}
62+
63+
// Rerun this build script if any of these environment variables change.
64+
println!("cargo:rerun-if-changed-env=CC");
65+
println!(
66+
"cargo:rerun-if-changed-env=CC_{}",
67+
target_key.to_lowercase()
68+
);
69+
}

ctest-next/src/ast/constant.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ pub struct Const {
66
#[expect(unused)]
77
pub(crate) public: bool,
88
pub(crate) ident: BoxStr,
9-
#[expect(unused)]
109
pub(crate) ty: syn::Type,
1110
#[expect(unused)]
1211
pub(crate) expr: syn::Expr,

ctest-next/src/ffi_items.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ impl FfiItems {
5454
}
5555

5656
/// Return a list of all constants found.
57-
#[cfg_attr(not(test), expect(unused))]
5857
pub(crate) fn constants(&self) -> &Vec<Const> {
5958
&self.constants
6059
}

ctest-next/src/generator.rs

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,131 @@
1-
use std::path::Path;
1+
use std::{
2+
env,
3+
fs::File,
4+
io::Write,
5+
path::{Path, PathBuf},
6+
};
27

8+
use askama::Template;
39
use syn::visit::Visit;
10+
use thiserror::Error;
411

5-
use crate::{expand, ffi_items::FfiItems, Result};
12+
use crate::{
13+
expand,
14+
ffi_items::FfiItems,
15+
template::{CTestTemplate, RustTestTemplate},
16+
};
17+
18+
#[derive(Debug, Error)]
19+
pub enum GenerationError {
20+
#[error("unable to expand crate {0}: {1}")]
21+
MacroExpansion(PathBuf, String),
22+
#[error("unable to parse expanded crate {0}: {1}")]
23+
RustSyntax(String, String),
24+
#[error("unable to render {0} template: {1}")]
25+
TemplateRender(String, String),
26+
#[error("unable to create or write template file: {0}")]
27+
OsError(std::io::Error),
28+
}
629

730
/// A builder used to generate a test suite.
8-
#[non_exhaustive]
931
#[derive(Default, Debug, Clone)]
10-
pub struct TestGenerator {}
32+
pub struct TestGenerator {
33+
headers: Vec<String>,
34+
pub(crate) target: Option<String>,
35+
pub(crate) includes: Vec<PathBuf>,
36+
out_dir: Option<PathBuf>,
37+
}
1138

1239
impl TestGenerator {
1340
/// Creates a new blank test generator.
1441
pub fn new() -> Self {
1542
Self::default()
1643
}
1744

18-
/// Generate all tests for the given crate and output the Rust side to a file.
19-
pub fn generate<P: AsRef<Path>>(&mut self, crate_path: P, _output_file_path: P) -> Result<()> {
20-
let expanded = expand(crate_path)?;
21-
let ast = syn::parse_file(&expanded)?;
45+
/// Add a header to be included as part of the generated C file.
46+
///
47+
/// The generate C test will be compiled by a C compiler, and this can be
48+
/// used to ensure that all the necessary header files are included to test
49+
/// all FFI definitions.
50+
pub fn header(&mut self, header: &str) -> &mut Self {
51+
self.headers.push(header.to_string());
52+
self
53+
}
54+
55+
/// Configures the target to compile C code for.
56+
///
57+
/// Note that for Cargo builds this defaults to `$TARGET` and it's not
58+
/// necessary to call.
59+
pub fn target(&mut self, target: &str) -> &mut Self {
60+
self.target = Some(target.to_string());
61+
self
62+
}
63+
64+
/// Add a path to the C compiler header lookup path.
65+
///
66+
/// This is useful for if the C library is installed to a nonstandard
67+
/// location to ensure that compiling the C file succeeds.
68+
pub fn include<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
69+
self.includes.push(p.as_ref().to_owned());
70+
self
71+
}
72+
73+
/// Configures the output directory of the generated Rust and C code.
74+
pub fn out_dir<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
75+
self.out_dir = Some(p.as_ref().to_owned());
76+
self
77+
}
78+
79+
/// Generate the Rust and C testing files.
80+
///
81+
/// Returns the path to t generated file.
82+
pub fn generate_files(
83+
&mut self,
84+
crate_path: impl AsRef<Path>,
85+
output_file_path: impl AsRef<Path>,
86+
) -> Result<PathBuf, GenerationError> {
87+
let expanded = expand(&crate_path).map_err(|e| {
88+
GenerationError::MacroExpansion(crate_path.as_ref().to_path_buf(), e.to_string())
89+
})?;
90+
let ast = syn::parse_file(&expanded)
91+
.map_err(|e| GenerationError::RustSyntax(expanded, e.to_string()))?;
2292

2393
let mut ffi_items = FfiItems::new();
2494
ffi_items.visit_file(&ast);
2595

26-
Ok(())
96+
let output_directory = self
97+
.out_dir
98+
.clone()
99+
.unwrap_or_else(|| env::var("OUT_DIR").unwrap().into());
100+
let output_file_path = output_directory.join(output_file_path);
101+
102+
// Generate the Rust side of the tests.
103+
File::create(output_file_path.with_extension("rs"))
104+
.map_err(GenerationError::OsError)?
105+
.write_all(
106+
RustTestTemplate::new(&ffi_items)
107+
.render()
108+
.map_err(|e| {
109+
GenerationError::TemplateRender("Rust".to_string(), e.to_string())
110+
})?
111+
.as_bytes(),
112+
)
113+
.map_err(GenerationError::OsError)?;
114+
115+
// Generate the C side of the tests.
116+
// FIXME(ctest): Cpp not supported yet.
117+
let c_output_path = output_file_path.with_extension("c");
118+
let headers = self.headers.iter().map(|h| h.as_str()).collect();
119+
File::create(&c_output_path)
120+
.map_err(GenerationError::OsError)?
121+
.write_all(
122+
CTestTemplate::new(headers, &ffi_items)
123+
.render()
124+
.map_err(|e| GenerationError::TemplateRender("C".to_string(), e.to_string()))?
125+
.as_bytes(),
126+
)
127+
.map_err(GenerationError::OsError)?;
128+
129+
Ok(output_file_path)
27130
}
28131
}

ctest-next/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ mod ast;
1515
mod ffi_items;
1616
mod generator;
1717
mod macro_expansion;
18+
mod runner;
19+
mod template;
20+
mod translator;
1821

1922
pub use ast::{Abi, Const, Field, Fn, Parameter, Static, Struct, Type, Union};
2023
pub use generator::TestGenerator;
2124
pub use macro_expansion::expand;
25+
pub use runner::{__compile_test, __run_test, generate_test};
26+
pub use translator::TranslationError;
2227

2328
/// A possible error that can be encountered in our library.
2429
pub type Error = Box<dyn std::error::Error>;

ctest-next/src/macro_expansion.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub fn expand<P: AsRef<Path>>(crate_path: P) -> Result<String> {
99
let output = Command::new(rustc)
1010
.env("RUSTC_BOOTSTRAP", "1")
1111
.arg("-Zunpretty=expanded")
12+
.arg("--edition")
13+
.arg("2024") // By default, -Zunpretty=expanded uses 2015 edition.
1214
.arg(canonicalize(crate_path)?)
1315
.output()?;
1416

0 commit comments

Comments
 (0)