Skip to content

Commit cb0f2e0

Browse files
committed
ctest: add defines for specific headers and language selection
1 parent 586ccc1 commit cb0f2e0

File tree

13 files changed

+230
-98
lines changed

13 files changed

+230
-98
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ctest/src/generator.rs

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use crate::ffi_items::FfiItems;
1313
use crate::template::{CTestTemplate, RustTestTemplate};
1414
use crate::translator::translate_primitive_type;
1515
use crate::{
16-
Const, Field, MapInput, Parameter, Result, Static, Struct, TranslationError, Type, Union,
17-
VolatileItemKind, expand,
16+
BoxStr, Const, Field, Language, MapInput, Parameter, Result, Static, Struct, TranslationError,
17+
Type, Union, VolatileItemKind, expand,
1818
};
1919

2020
/// A function that takes a mappable input and returns its mapping as `Some`, otherwise
@@ -35,14 +35,16 @@ type CEnum = Box<dyn Fn(&str) -> bool>;
3535
#[derive(Default)]
3636
#[expect(missing_debug_implementations)]
3737
pub struct TestGenerator {
38-
pub(crate) headers: Vec<String>,
38+
pub(crate) headers: Vec<(BoxStr, Vec<BoxStr>)>,
3939
pub(crate) target: Option<String>,
4040
pub(crate) includes: Vec<PathBuf>,
4141
out_dir: Option<PathBuf>,
4242
pub(crate) flags: Vec<String>,
43-
pub(crate) defines: Vec<(String, Option<String>)>,
43+
pub(crate) global_defines: Vec<(String, Option<String>)>,
4444
cfg: Vec<(String, Option<String>)>,
4545
mapped_names: Vec<MappedName>,
46+
/// The programming language to generate tests in.
47+
pub(crate) language: Language,
4648
pub(crate) skips: Vec<Skip>,
4749
pub(crate) verbose_skip: bool,
4850
pub(crate) volatile_items: Vec<VolatileItem>,
@@ -100,7 +102,53 @@ impl TestGenerator {
100102
/// .header("bar.h");
101103
/// ```
102104
pub fn header(&mut self, header: &str) -> &mut Self {
103-
self.headers.push(header.to_string());
105+
self.headers.push((header.into(), vec![]));
106+
self
107+
}
108+
109+
/// Add a header to be included as part of the generated C file, as well as defines for it.
110+
///
111+
/// The generated C test will be compiled by a C compiler, and this can be
112+
/// used to ensure that all the necessary header files are included to test
113+
/// all FFI definitions. The defines are only set for the inclusion of that header file, and are
114+
/// undefined immediately after.
115+
///
116+
/// # Examples
117+
///
118+
/// ```no_run
119+
/// use ctest::TestGenerator;
120+
///
121+
/// let mut cfg = TestGenerator::new();
122+
/// cfg.header_with_defines("foo.h", Vec::<String>::new())
123+
/// .header_with_defines("bar.h", vec!["DEBUG", "DEPRECATED"]);
124+
/// ```
125+
pub fn header_with_defines(
126+
&mut self,
127+
header: &str,
128+
defines: impl IntoIterator<Item = impl AsRef<str>>,
129+
) -> &mut Self {
130+
self.headers.push((
131+
header.into(),
132+
defines.into_iter().map(|d| d.as_ref().into()).collect(),
133+
));
134+
self
135+
}
136+
137+
/// Sets the programming language, by default it is C.
138+
///
139+
/// This determines what compiler is chosen to compile the C/C++ tests, as well as adding
140+
/// external linkage to the tests if set to C++, so that they can be used in Rust.
141+
///
142+
/// # Examples
143+
///
144+
/// ```no_run
145+
/// use ctest::{TestGenerator, Language};
146+
///
147+
/// let mut cfg = TestGenerator::new();
148+
/// cfg.language(Language::CXX);
149+
/// ```
150+
pub fn language(&mut self, language: Language) -> &mut Self {
151+
self.language = language;
104152
self
105153
}
106154

@@ -585,7 +633,7 @@ impl TestGenerator {
585633

586634
/// Set a `-D` flag for the C compiler being called.
587635
///
588-
/// This can be used to define various variables to configure how header
636+
/// This can be used to define various global variables to configure how header
589637
/// files are included or what APIs are exposed from header files.
590638
///
591639
/// # Examples
@@ -598,7 +646,7 @@ impl TestGenerator {
598646
/// .define("_WIN32_WINNT", Some("0x8000"));
599647
/// ```
600648
pub fn define(&mut self, k: &str, v: Option<&str>) -> &mut Self {
601-
self.defines
649+
self.global_defines
602650
.push((k.to_string(), v.map(std::string::ToString::to_string)));
603651
self
604652
}
@@ -999,7 +1047,7 @@ impl TestGenerator {
9991047
ensure_trailing_newline(&mut c_file);
10001048

10011049
// Generate the C/Cxx side of the tests.
1002-
let c_output_path = output_file_path.with_extension("c");
1050+
let c_output_path = output_file_path.with_extension(self.language.extension());
10031051
File::create(&c_output_path)
10041052
.map_err(GenerationError::OsError)?
10051053
.write_all(c_file.as_bytes())

ctest/src/lib.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,27 @@ pub(crate) enum MapInput<'a> {
7474
UnionFieldType(&'a Union, &'a Field),
7575
}
7676

77+
/// The language used to generate the tests.
78+
#[derive(Debug, Default, Clone)]
79+
#[non_exhaustive]
80+
pub enum Language {
81+
/// The C Programming Language.
82+
#[default]
83+
C,
84+
/// The C++ Programming Language.
85+
CXX,
86+
}
87+
88+
impl Language {
89+
/// Return the file extension of the programming language.
90+
pub(crate) fn extension(&self) -> &str {
91+
match self {
92+
Self::C => "c",
93+
Self::CXX => "cpp",
94+
}
95+
}
96+
}
97+
7798
/* The From impls make it easier to write code in the test templates. */
7899

79100
impl<'a> From<&'a Const> for MapInput<'a> {

ctest/src/runner.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
77
use std::process::Command;
88

99
use crate::generator::GenerationError;
10-
use crate::{EDITION, Result, TestGenerator};
10+
use crate::{EDITION, Language, Result, TestGenerator};
1111

1212
/// Generate all tests for the given crate and output the Rust side to a file.
1313
#[doc(hidden)]
@@ -34,7 +34,7 @@ pub fn generate_test(
3434
.map_err(|_| GenerationError::EnvVarNotFound("HOST, HOST_PLATFORM".to_string()))?;
3535

3636
let mut cfg = cc::Build::new();
37-
cfg.file(output_file_path.with_extension("c"));
37+
cfg.file(output_file_path.with_extension(generator.language.extension()));
3838
cfg.host(&host);
3939

4040
if target.contains("msvc") {
@@ -74,10 +74,12 @@ pub fn generate_test(
7474
cfg.flag(flag);
7575
}
7676

77-
for (k, v) in &generator.defines {
77+
for (k, v) in &generator.global_defines {
7878
cfg.define(k, v.as_ref().map(|s| &s[..]));
7979
}
8080

81+
cfg.cpp(matches!(generator.language, Language::CXX));
82+
8183
let stem: &str = output_file_path.file_stem().unwrap().to_str().unwrap();
8284
cfg.target(&target)
8385
.out_dir(output_file_path.parent().unwrap())

ctest/src/template.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ impl RustTestTemplate {
3535
#[template(path = "test.c")]
3636
pub(crate) struct CTestTemplate {
3737
pub template: TestTemplate,
38-
pub headers: Vec<String>,
38+
pub headers: Vec<(BoxStr, Vec<BoxStr>)>,
3939
}
4040

4141
impl CTestTemplate {

ctest/templates/test.c

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,25 @@
88
#include <stdint.h>
99
#include <stdio.h>
1010

11-
{%- for header in self.headers +%}
11+
{%- for (header, defines) in self.headers +%}
12+
{%- for define in defines +%}
1213

14+
#define {{ define }}
15+
{%- endfor +%}
1316
#include <{{ header }}>
17+
{%- for define in defines +%}
18+
#undef {{ define }}
19+
{%- endfor +%}
1420
{%- endfor +%}
1521

22+
#if defined(__cplusplus)
23+
#define CTEST_ALIGNOF(T) alignof(T)
24+
#define CTEST_EXTERN extern "C"
25+
#else
26+
#define CTEST_ALIGNOF(T) _Alignof(T)
27+
#define CTEST_EXTERN
28+
#endif
29+
1630
typedef void (*ctest_void_func)(void);
1731

1832
{%- for const_cstr in ctx.const_cstr_tests +%}
@@ -21,7 +35,7 @@ static char *ctest_const_{{ const_cstr.id }}_val_static = {{ const_cstr.c_val }}
2135

2236
// Define a function that returns a pointer to the value of the constant to test.
2337
// This will later be called on the Rust side via FFI.
24-
char *ctest_const_cstr__{{ const_cstr.id }}(void) {
38+
CTEST_EXTERN char *ctest_const_cstr__{{ const_cstr.id }}(void) {
2539
return ctest_const_{{ const_cstr.id }}_val_static;
2640
}
2741
{%- endfor +%}
@@ -32,25 +46,25 @@ static {{ constant.c_ty }} ctest_const_{{ constant.id }}_val_static = {{ constan
3246

3347
// Define a function that returns a pointer to the value of the constant to test.
3448
// This will later be called on the Rust side via FFI.
35-
{{ constant.c_ty }} *ctest_const__{{ constant.id }}(void) {
49+
CTEST_EXTERN {{ constant.c_ty }} *ctest_const__{{ constant.id }}(void) {
3650
return &ctest_const_{{ constant.id }}_val_static;
3751
}
3852
{%- endfor +%}
3953

4054
{%- for item in ctx.size_align_tests +%}
4155

4256
// Return the size of a type.
43-
uint64_t ctest_size_of__{{ item.id }}(void) { return sizeof({{ item.c_ty }}); }
57+
CTEST_EXTERN uint64_t ctest_size_of__{{ item.id }}(void) { return sizeof({{ item.c_ty }}); }
4458

4559
// Return the alignment of a type.
46-
uint64_t ctest_align_of__{{ item.id }}(void) { return _Alignof({{ item.c_ty }}); }
60+
CTEST_EXTERN uint64_t ctest_align_of__{{ item.id }}(void) { return CTEST_ALIGNOF({{ item.c_ty }}); }
4761
{%- endfor +%}
4862

4963
{%- for alias in ctx.signededness_tests +%}
5064

5165
// Return `1` if the type is signed, otherwise return `0`.
5266
// Casting -1 to the aliased type if signed evaluates to `-1 < 0`, if unsigned to `MAX_VALUE < 0`
53-
uint32_t ctest_signededness_of__{{ alias.id }}(void) {
67+
CTEST_EXTERN uint32_t ctest_signededness_of__{{ alias.id }}(void) {
5468
{{ alias.c_ty }} all_ones = ({{ alias.c_ty }}) -1;
5569
return all_ones < 0;
5670
}
@@ -59,12 +73,12 @@ uint32_t ctest_signededness_of__{{ alias.id }}(void) {
5973
{%- for item in ctx.field_size_offset_tests +%}
6074

6175
// Return the offset of a struct/union field.
62-
uint64_t ctest_offset_of__{{ item.id }}__{{ item.field.ident() }}(void) {
76+
CTEST_EXTERN uint64_t ctest_offset_of__{{ item.id }}__{{ item.field.ident() }}(void) {
6377
return offsetof({{ item.c_ty }}, {{ item.c_field }});
6478
}
6579

6680
// Return the size of a struct/union field.
67-
uint64_t ctest_size_of__{{ item.id }}__{{ item.field.ident() }}(void) {
81+
CTEST_EXTERN uint64_t ctest_size_of__{{ item.id }}__{{ item.field.ident() }}(void) {
6882
return sizeof((({{ item.c_ty }}){}).{{ item.c_field }});
6983
}
7084
{%- endfor +%}
@@ -75,7 +89,7 @@ uint64_t ctest_size_of__{{ item.id }}__{{ item.field.ident() }}(void) {
7589
// This field can have a normal data type, or it could be a function pointer or an array, which
7690
// have different syntax. A typedef is used for convenience, but the syntax must be precomputed.
7791
typedef {{ item.volatile_keyword }}{{ item.field_return_type }};
78-
ctest_field_ty__{{ item.id }}__{{ item.field.ident() }}
92+
CTEST_EXTERN ctest_field_ty__{{ item.id }}__{{ item.field.ident() }}
7993
ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}({{ item.c_ty }} *b) {
8094
return &b->{{ item.c_field }};
8195
}
@@ -92,7 +106,7 @@ ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}({{ item.c_ty }} *b) {
92106
// Tests whether the struct/union/alias `x` when passed by value to C and back to Rust
93107
// remains unchanged.
94108
// It checks if the size is the same as well as if the padding bytes are all in the correct place.
95-
{{ item.c_ty }} ctest_roundtrip__{{ item.id }}(
109+
CTEST_EXTERN {{ item.c_ty }} ctest_roundtrip__{{ item.id }}(
96110
{{ item.c_ty }} value,
97111
const uint8_t is_padding_byte[sizeof({{ item.c_ty }})],
98112
uint8_t value_bytes[sizeof({{ item.c_ty }})]
@@ -130,7 +144,8 @@ ctest_field_ptr__{{ item.id }}__{{ item.field.ident() }}({{ item.c_ty }} *b) {
130144

131145
{%- for item in ctx.foreign_fn_tests +%}
132146

133-
ctest_void_func ctest_foreign_fn__{{ item.id }}(void) {
147+
// Return a function pointer.
148+
CTEST_EXTERN ctest_void_func ctest_foreign_fn__{{ item.id }}(void) {
134149
return (ctest_void_func){{ item.c_val }};
135150
}
136151
{%- endfor +%}
@@ -142,7 +157,7 @@ ctest_void_func ctest_foreign_fn__{{ item.id }}(void) {
142157
{%- for static_ in ctx.foreign_static_tests +%}
143158

144159
// Return a pointer to the static variable content.
145-
void *ctest_static__{{ static_.id }}(void) {
160+
CTEST_EXTERN void *ctest_static__{{ static_.id }}(void) {
146161
// FIXME(ctest): Not correct due to casting the function to a data pointer.
147162
return (void *)&{{ static_.c_val }};
148163
}

ctest/tests/basic.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@ use pretty_assertions::assert_eq;
1111
///
1212
/// The files will be generated in a unique temporary directory that gets
1313
/// deleted when it goes out of scope.
14-
fn default_generator(opt_level: u8, header: &str) -> Result<(TestGenerator, tempfile::TempDir)> {
14+
fn default_generator(
15+
opt_level: u8,
16+
header: Option<&str>,
17+
) -> Result<(TestGenerator, tempfile::TempDir)> {
1518
// FIXME(mbyx): Remove this in favor of not-unsafe alternatives.
1619
unsafe { env::set_var("OPT_LEVEL", opt_level.to_string()) };
1720
let temp_dir = tempfile::tempdir()?;
1821
let mut generator = TestGenerator::new();
19-
generator
20-
.out_dir(&temp_dir)
21-
.include("tests/input")
22-
.header(header);
22+
generator.out_dir(&temp_dir).include("tests/input");
23+
if let Some(header) = header {
24+
generator.header(header);
25+
}
2326

2427
Ok((generator, temp_dir))
2528
}
@@ -84,7 +87,7 @@ fn test_entrypoint_hierarchy() {
8487
let crate_path = include_path.join("hierarchy/lib.rs");
8588
let library_path = "hierarchy.out.a";
8689

87-
let (mut gen_, out_dir) = default_generator(1, "hierarchy.h").unwrap();
90+
let (mut gen_, out_dir) = default_generator(1, Some("hierarchy.h")).unwrap();
8891
check_entrypoint(&mut gen_, out_dir, crate_path, library_path, include_path);
8992
}
9093

@@ -95,7 +98,7 @@ fn test_skip_simple() {
9598
let crate_path = include_path.join("simple.rs");
9699
let library_path = "simple.out.with-skips.a";
97100

98-
let (mut gen_, out_dir) = default_generator(1, "simple.h").unwrap();
101+
let (mut gen_, out_dir) = default_generator(1, Some("simple.h")).unwrap();
99102
gen_.skip_const(|c| c.ident() == "B" || c.ident() == "A")
100103
.skip_c_enum(|e| e == "Color")
101104
.skip_alias(|a| a.ident() == "Byte")
@@ -114,7 +117,7 @@ fn test_map_simple() {
114117
let crate_path = include_path.join("simple.rs");
115118
let library_path = "simple.out.with-renames.a";
116119

117-
let (mut gen_, out_dir) = default_generator(1, "simple.h").unwrap();
120+
let (mut gen_, out_dir) = default_generator(1, Some("simple.h")).unwrap();
118121
gen_.rename_constant(|c| (c.ident() == "B").then(|| "C_B".to_string()))
119122
.alias_is_c_enum(|e| e == "Color")
120123
.skip_signededness(|ty| ty == "Color");
@@ -129,7 +132,9 @@ fn test_entrypoint_macro() {
129132
let crate_path = include_path.join("macro.rs");
130133
let library_path = "macro.out.a";
131134

132-
let (mut gen_, out_dir) = default_generator(1, "macro.h").unwrap();
135+
let (mut gen_, out_dir) = default_generator(1, None).unwrap();
136+
gen_.header_with_defines("macro.h", vec!["SUPPRESS_ERROR"]);
137+
133138
check_entrypoint(&mut gen_, out_dir, crate_path, library_path, include_path);
134139
}
135140

ctest/tests/input/hierarchy.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
#include <stdbool.h>
22
#include <stddef.h>
33

4+
#ifdef SUPPRESS_ERROR
5+
#error Expected SUPPRESS_ERROR to not be defined (testing per-file defines)
6+
#endif
7+
48
typedef unsigned int in6_addr;
59

610
#define ON true

0 commit comments

Comments
 (0)