Skip to content

Commit 96df628

Browse files
committed
ctest: Add translation of Rust types.
1 parent 59204ee commit 96df628

26 files changed

+1291
-68
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: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,100 @@
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;
410

5-
use crate::{expand, ffi_items::FfiItems, Result};
11+
use crate::{
12+
expand,
13+
ffi_items::FfiItems,
14+
template::{CTestTemplate, RustTestTemplate},
15+
Result,
16+
};
617

718
/// A builder used to generate a test suite.
8-
#[non_exhaustive]
919
#[derive(Default, Debug, Clone)]
10-
pub struct TestGenerator {}
20+
pub struct TestGenerator {
21+
headers: Vec<String>,
22+
pub(crate) target: Option<String>,
23+
pub(crate) includes: Vec<PathBuf>,
24+
out_dir: Option<PathBuf>,
25+
}
1126

1227
impl TestGenerator {
1328
/// Creates a new blank test generator.
1429
pub fn new() -> Self {
1530
Self::default()
1631
}
1732

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<()> {
33+
/// Add a header to be included as part of the generated C file.
34+
///
35+
/// The generate C test will be compiled by a C compiler, and this can be
36+
/// used to ensure that all the necessary header files are included to test
37+
/// all FFI definitions.
38+
pub fn header(&mut self, header: &str) -> &mut Self {
39+
self.headers.push(header.to_string());
40+
self
41+
}
42+
43+
/// Configures the target to compile C code for.
44+
///
45+
/// Note that for Cargo builds this defaults to `$TARGET` and it's not
46+
/// necessary to call.
47+
pub fn target(&mut self, target: &str) -> &mut Self {
48+
self.target = Some(target.to_string());
49+
self
50+
}
51+
52+
/// Add a path to the C compiler header lookup path.
53+
///
54+
/// This is useful for if the C library is installed to a nonstandard
55+
/// location to ensure that compiling the C file succeeds.
56+
pub fn include<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
57+
self.includes.push(p.as_ref().to_owned());
58+
self
59+
}
60+
61+
/// Configures the output directory of the generated Rust and C code.
62+
pub fn out_dir<P: AsRef<Path>>(&mut self, p: P) -> &mut Self {
63+
self.out_dir = Some(p.as_ref().to_owned());
64+
self
65+
}
66+
67+
/// Generate the Rust and C testing files.
68+
///
69+
/// Returns the path to t generated file.
70+
pub fn generate_files(
71+
&mut self,
72+
crate_path: impl AsRef<Path>,
73+
output_file_path: impl AsRef<Path>,
74+
) -> Result<PathBuf> {
2075
let expanded = expand(crate_path)?;
2176
let ast = syn::parse_file(&expanded)?;
2277

2378
let mut ffi_items = FfiItems::new();
2479
ffi_items.visit_file(&ast);
2580

26-
Ok(())
81+
let output_directory = self
82+
.out_dir
83+
.clone()
84+
.unwrap_or_else(|| env::var("OUT_DIR").unwrap().into());
85+
let output_file_path = output_directory.join(output_file_path);
86+
87+
// Generate the Rust side of the tests.
88+
File::create(output_file_path.with_extension("rs"))?
89+
.write_all(RustTestTemplate::new(&ffi_items)?.render()?.as_bytes())?;
90+
91+
// Generate the C side of the tests.
92+
// FIXME(ctest): Cpp not supported yet.
93+
let c_output_path = output_file_path.with_extension("c");
94+
let headers = self.headers.iter().map(|h| h.as_str()).collect();
95+
File::create(&c_output_path)?
96+
.write_all(CTestTemplate::new(headers, &ffi_items).render()?.as_bytes())?;
97+
98+
Ok(output_file_path)
2799
}
28100
}

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

ctest-next/src/runner.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
use crate::{Result, TestGenerator};
2+
use std::env;
3+
use std::fs::{canonicalize, File};
4+
use std::io::Write;
5+
use std::path::{Path, PathBuf};
6+
use std::process::Command;
7+
8+
/// Generate all tests for the given crate and output the Rust side to a file.
9+
#[doc(hidden)]
10+
pub fn generate_test(
11+
generator: &mut TestGenerator,
12+
crate_path: impl AsRef<Path>,
13+
output_file_path: impl AsRef<Path>,
14+
) -> Result<PathBuf> {
15+
let output_file_path = generator.generate_files(crate_path, output_file_path)?;
16+
17+
// Search for the target and host to build for if specified manually
18+
// (generator.target, generator.host),
19+
// via build script (TARGET, HOST), or for internal testing (TARGET_PLATFORM, HOST_PLATFORM).
20+
let target = generator.target.clone().unwrap_or_else(|| {
21+
env::var("TARGET").unwrap_or_else(|_| env::var("TARGET_PLATFORM").unwrap())
22+
});
23+
let host = env::var("HOST").unwrap_or_else(|_| env::var("HOST_PLATFORM").unwrap());
24+
25+
let mut cfg = cc::Build::new();
26+
// FIXME(ctest): Cpp not supported.
27+
cfg.file(output_file_path.with_extension("c"));
28+
cfg.host(&host);
29+
30+
if target.contains("msvc") {
31+
cfg.flag("/W3")
32+
.flag("/Wall")
33+
.flag("/WX")
34+
// ignored warnings
35+
.flag("/wd4820") // warning about adding padding?
36+
.flag("/wd4100") // unused parameters
37+
.flag("/wd4996") // deprecated functions
38+
.flag("/wd4296") // '<' being always false
39+
.flag("/wd4255") // converting () to (void)
40+
.flag("/wd4668") // using an undefined thing in preprocessor?
41+
.flag("/wd4366") // taking ref to packed struct field might be unaligned
42+
.flag("/wd4189") // local variable initialized but not referenced
43+
.flag("/wd4710") // function not inlined
44+
.flag("/wd5045") // compiler will insert Spectre mitigation
45+
.flag("/wd4514") // unreferenced inline function removed
46+
.flag("/wd4711"); // function selected for automatic inline
47+
} else {
48+
cfg.flag("-Wall")
49+
.flag("-Wextra")
50+
.flag("-Werror")
51+
.flag("-Wno-unused-parameter")
52+
.flag("-Wno-type-limits")
53+
// allow taking address of packed struct members:
54+
.flag("-Wno-address-of-packed-member")
55+
.flag("-Wno-unknown-warning-option")
56+
.flag("-Wno-deprecated-declarations"); // allow deprecated items
57+
}
58+
59+
for p in &generator.includes {
60+
cfg.include(p);
61+
}
62+
63+
let stem: &str = output_file_path.file_stem().unwrap().to_str().unwrap();
64+
cfg.target(&target)
65+
.out_dir(output_file_path.parent().unwrap())
66+
.compile(stem);
67+
68+
Ok(output_file_path)
69+
}
70+
71+
/// Compiles a Rust source file and links it against a static library.
72+
///
73+
/// Returns the path to the generated binary.
74+
#[doc(hidden)]
75+
pub fn __compile_test(
76+
output_dir: impl AsRef<Path>,
77+
crate_path: impl AsRef<Path>,
78+
library_file: impl AsRef<Path>,
79+
) -> Result<PathBuf> {
80+
let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into());
81+
let output_dir = output_dir.as_ref();
82+
let crate_path = crate_path.as_ref();
83+
let library_file = library_file.as_ref().file_stem().unwrap();
84+
85+
let rust_file = output_dir
86+
.join(crate_path.file_stem().unwrap())
87+
.with_extension("rs");
88+
let binary_path = output_dir.join(rust_file.file_stem().unwrap());
89+
90+
// Create a file that contains the Rust 'bindings' as well as the generated test code.
91+
File::create(&rust_file)?.write_all(
92+
format!(
93+
"include!(r#\"{}\"#);\ninclude!(r#\"{}.rs\"#);",
94+
canonicalize(crate_path)?.display(),
95+
library_file.to_str().unwrap()
96+
)
97+
.as_bytes(),
98+
)?;
99+
100+
// Compile the test file with the compiled C library file found in `output_dir`
101+
// into a binary file, ignoring all warnings about unused items. (not all items
102+
// are currently tested)
103+
104+
let mut cmd = Command::new(rustc);
105+
cmd.arg(&rust_file)
106+
.arg(format!("-Lnative={}", output_dir.display()))
107+
.arg(format!("-lstatic={}", library_file.to_str().unwrap()))
108+
.arg("--edition")
109+
.arg("2021") // Defaults to 2015.
110+
.arg("-o")
111+
.arg(&binary_path)
112+
.arg("-Aunused");
113+
114+
// Pass in a different target, linker or flags if set, useful for cross compilation.
115+
116+
let target = env::var("TARGET_PLATFORM").unwrap_or_default();
117+
if !target.is_empty() {
118+
cmd.arg("--target").arg(target);
119+
}
120+
121+
let linker = env::var("LINKER").unwrap_or_default();
122+
if !linker.is_empty() {
123+
cmd.arg(format!("-Clinker={linker}"));
124+
}
125+
126+
let flags = env::var("FLAGS").unwrap_or_default();
127+
if !flags.is_empty() {
128+
cmd.args(flags.split_whitespace());
129+
}
130+
131+
let output = cmd.output()?;
132+
if !output.status.success() {
133+
return Err(std::str::from_utf8(&output.stderr)?.into());
134+
}
135+
136+
Ok(binary_path)
137+
}
138+
139+
/// Executes the compiled test binary and returns its output.
140+
///
141+
/// If a RUNNER environment variable is present, it will use that to run the binary.
142+
#[doc(hidden)]
143+
pub fn __run_test<P: AsRef<Path>>(test_binary: P) -> Result<String> {
144+
let runner = env::var("RUNNER").unwrap_or_default();
145+
let mut cmd;
146+
if runner.is_empty() {
147+
cmd = Command::new(test_binary.as_ref());
148+
} else {
149+
let mut args = runner.split_whitespace();
150+
cmd = Command::new(args.next().unwrap());
151+
cmd.args(args);
152+
};
153+
154+
cmd.arg(test_binary.as_ref());
155+
let output = cmd.output()?;
156+
157+
if !output.status.success() {
158+
return Err(std::str::from_utf8(&output.stderr)?.into());
159+
}
160+
161+
Ok(std::str::from_utf8(&output.stdout)?.to_string())
162+
}

0 commit comments

Comments
 (0)