Skip to content

Commit d95d75a

Browse files
committed
fix: support header_contents() with clang_macro_fallback
Fixes #3351 clang_macro_fallback() silently produced no output when headers were provided via header_contents() instead of header(). This happened because try_ensure_fallback_translation_unit() only looked at input_headers (populated by .header()), returning None when it was empty. Fix: materialize input_header_contents to disk in the fallback build directory so clang can consume them for PCH compilation. Both input_headers and input_header_contents are now included in the fallback PCH, so mixed .header() + .header_contents() setups work. The materialized paths are sanitized (roots stripped, ".." resolved) to prevent escaping the build directory, and tracked in FallbackTranslationUnit for cleanup on drop. Also stop using std::mem::take on input_header_contents in Builder::generate(), preserving the data for the fallback TU (and fixing a pre-existing bug where serialize_items in codegen couldn't see header contents). Also store the original user-provided name in input_header_contents at header_contents() time so the fallback can reconstruct relative paths without depending on the current working directory.
1 parent b7b501f commit d95d75a

File tree

6 files changed

+302
-31
lines changed

6 files changed

+302
-31
lines changed

bindgen-tests/tests/tests.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,213 @@ fn test_macro_fallback_non_system_dir() {
617617
}
618618
}
619619

620+
#[test]
621+
fn test_macro_fallback_header_contents() {
622+
let tmpdir = tempfile::tempdir().unwrap();
623+
let actual = builder()
624+
.disable_header_comment()
625+
.header_contents(
626+
"test.h",
627+
"#define UINT32_C(c) c ## U\n\
628+
#define SIMPLE 42\n\
629+
#define COMPOUND UINT32_C(69)\n",
630+
)
631+
.clang_macro_fallback()
632+
.clang_macro_fallback_build_dir(tmpdir.path())
633+
.clang_arg("--target=x86_64-unknown-linux")
634+
.generate()
635+
.unwrap()
636+
.to_string();
637+
638+
let actual = format_code(actual).unwrap();
639+
640+
let expected = format_code(
641+
"pub const SIMPLE: u32 = 42;\npub const COMPOUND: u32 = 69;\n",
642+
)
643+
.unwrap();
644+
645+
assert_eq!(expected, actual);
646+
}
647+
648+
#[test]
649+
fn test_macro_fallback_multiple_header_contents() {
650+
let tmpdir = tempfile::tempdir().unwrap();
651+
let actual = builder()
652+
.disable_header_comment()
653+
.header_contents("defs.h", "#define UINT32_C(c) c ## U\n")
654+
.header_contents(
655+
"test.h",
656+
"#include \"defs.h\"\n\
657+
#define MY_CONST UINT32_C(28)\n",
658+
)
659+
.clang_macro_fallback()
660+
.clang_macro_fallback_build_dir(tmpdir.path())
661+
.clang_arg("--target=x86_64-unknown-linux")
662+
.generate()
663+
.unwrap()
664+
.to_string();
665+
666+
let actual = format_code(actual).unwrap();
667+
668+
// UINT32_C is a function-like macro, should not appear as a constant.
669+
// MY_CONST should be evaluated by the fallback.
670+
assert!(
671+
actual.contains("pub const MY_CONST: u32 = 28;"),
672+
"Expected MY_CONST constant in output:\n{actual}"
673+
);
674+
}
675+
676+
#[test]
677+
fn test_macro_fallback_mixed_header_and_header_contents() {
678+
let tmpdir = tempfile::tempdir().unwrap();
679+
let actual = builder()
680+
.disable_header_comment()
681+
.header(concat!(
682+
env!("CARGO_MANIFEST_DIR"),
683+
"/tests/headers/issue-753.h"
684+
))
685+
.header_contents(
686+
"extra.h",
687+
"#define UINT32_C(c) c ## U\n\
688+
#define EXTRA_CONST UINT32_C(99)\n",
689+
)
690+
.clang_macro_fallback()
691+
.clang_macro_fallback_build_dir(tmpdir.path())
692+
.generate()
693+
.unwrap()
694+
.to_string();
695+
696+
let actual = format_code(actual).unwrap();
697+
698+
// Constants from the real header (issue-753.h defines UINT32_C and uses it)
699+
assert!(
700+
actual.contains("pub const CONST: u32 = 5;"),
701+
"Expected CONST from real header in output:\n{actual}"
702+
);
703+
// Constants from the virtual header via fallback
704+
assert!(
705+
actual.contains("pub const EXTRA_CONST: u32 = 99;"),
706+
"Expected EXTRA_CONST from header_contents in output:\n{actual}"
707+
);
708+
}
709+
710+
#[test]
711+
fn test_macro_fallback_header_contents_duplicate_basename() {
712+
let tmpdir = tempfile::tempdir().unwrap();
713+
let actual = builder()
714+
.disable_header_comment()
715+
.header_contents(
716+
"defs.h",
717+
"#define UINT32_C(c) c ## U\n\
718+
#define FROM_ROOT UINT32_C(11)\n",
719+
)
720+
.header_contents(
721+
"sub/defs.h",
722+
"#define UINT32_C(c) c ## U\n\
723+
#define FROM_SUB UINT32_C(22)\n",
724+
)
725+
.clang_macro_fallback()
726+
.clang_macro_fallback_build_dir(tmpdir.path())
727+
.clang_arg("--target=x86_64-unknown-linux")
728+
.generate()
729+
.unwrap()
730+
.to_string();
731+
732+
let actual = format_code(actual).unwrap();
733+
734+
assert!(
735+
actual.contains("pub const FROM_ROOT: u32 = 11;"),
736+
"Expected FROM_ROOT in output:\n{actual}"
737+
);
738+
assert!(
739+
actual.contains("pub const FROM_SUB: u32 = 22;"),
740+
"Expected FROM_SUB in output:\n{actual}"
741+
);
742+
}
743+
744+
#[test]
745+
fn test_macro_fallback_header_contents_absolute_name() {
746+
// Absolute names must not escape the build dir — they should be
747+
// materialized safely under it, and the original file must not be
748+
// clobbered or deleted.
749+
let tmpdir = tempfile::tempdir().unwrap();
750+
let victim = tmpdir.path().join("victim.h");
751+
fs::write(&victim, "// original content\n").unwrap();
752+
753+
let abs_name = victim.to_str().unwrap();
754+
let build_dir = tmpdir.path().join("fallback_build");
755+
fs::create_dir_all(&build_dir).unwrap();
756+
757+
let actual = builder()
758+
.disable_header_comment()
759+
.header_contents(
760+
abs_name,
761+
"#define UINT32_C(c) c ## U\n\
762+
#define ABS_CONST UINT32_C(55)\n",
763+
)
764+
.clang_macro_fallback()
765+
.clang_macro_fallback_build_dir(&build_dir)
766+
.clang_arg("--target=x86_64-unknown-linux")
767+
.generate()
768+
.unwrap()
769+
.to_string();
770+
771+
// The fallback-only constant should be evaluated.
772+
assert!(
773+
actual.contains("pub const ABS_CONST: u32 = 55;"),
774+
"Expected ABS_CONST in output:\n{actual}"
775+
);
776+
777+
// The original file must not have been deleted by FallbackTU drop.
778+
assert!(
779+
victim.exists(),
780+
"Original file at {abs_name} was deleted by fallback cleanup"
781+
);
782+
assert_eq!(
783+
fs::read_to_string(&victim).unwrap(),
784+
"// original content\n",
785+
"Original file at {abs_name} was overwritten by materialization"
786+
);
787+
}
788+
789+
#[test]
790+
fn test_macro_fallback_header_contents_parent_dir_escape() {
791+
// Names with ".." must not escape the build dir or delete files
792+
// outside it during FallbackTranslationUnit cleanup.
793+
let tmpdir = tempfile::tempdir().unwrap();
794+
let victim = tmpdir.path().join("victim.h");
795+
fs::write(&victim, "// must survive\n").unwrap();
796+
797+
let build_dir = tmpdir.path().join("build");
798+
fs::create_dir_all(&build_dir).unwrap();
799+
800+
let actual = builder()
801+
.disable_header_comment()
802+
.header_contents(
803+
"../victim.h",
804+
"#define UINT32_C(c) c ## U\n\
805+
#define ESCAPE_CONST UINT32_C(77)\n",
806+
)
807+
.clang_macro_fallback()
808+
.clang_macro_fallback_build_dir(&build_dir)
809+
.clang_arg("--target=x86_64-unknown-linux")
810+
.generate()
811+
.unwrap()
812+
.to_string();
813+
814+
assert!(
815+
actual.contains("pub const ESCAPE_CONST: u32 = 77;"),
816+
"Expected ESCAPE_CONST in output:\n{actual}"
817+
);
818+
819+
// The file outside build_dir must not have been clobbered.
820+
assert!(
821+
victim.exists(),
822+
"File outside build dir was deleted by fallback cleanup"
823+
);
824+
assert_eq!(fs::read_to_string(&victim).unwrap(), "// must survive\n",);
825+
}
826+
620827
#[test]
621828
// Doesn't support executing sh file on Windows.
622829
// We may want to implement it in Rust so that we support all systems.

bindgen/clang.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1915,6 +1915,9 @@ impl Drop for TranslationUnit {
19151915
pub(crate) struct FallbackTranslationUnit {
19161916
file_path: String,
19171917
pch_path: String,
1918+
/// Header files materialized from `header_contents()` that must remain on
1919+
/// disk while the PCH is in use (clang validates source file existence).
1920+
materialized_headers: Vec<String>,
19181921
idx: Box<Index>,
19191922
tu: TranslationUnit,
19201923
}
@@ -1931,6 +1934,7 @@ impl FallbackTranslationUnit {
19311934
file: String,
19321935
pch_path: String,
19331936
c_args: &[Box<str>],
1937+
materialized_headers: Vec<String>,
19341938
) -> Option<Self> {
19351939
// Create empty file
19361940
OpenOptions::new()
@@ -1951,6 +1955,7 @@ impl FallbackTranslationUnit {
19511955
Some(FallbackTranslationUnit {
19521956
file_path: file,
19531957
pch_path,
1958+
materialized_headers,
19541959
tu: f_translation_unit,
19551960
idx: f_index,
19561961
})
@@ -1989,6 +1994,9 @@ impl Drop for FallbackTranslationUnit {
19891994
fn drop(&mut self) {
19901995
let _ = std::fs::remove_file(&self.file_path);
19911996
let _ = std::fs::remove_file(&self.pch_path);
1997+
for path in &self.materialized_headers {
1998+
let _ = std::fs::remove_file(path);
1999+
}
19922000
}
19932001
}
19942002

bindgen/codegen/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5364,7 +5364,8 @@ pub(crate) mod utils {
53645364
}
53655365

53665366
if !context.options().input_header_contents.is_empty() {
5367-
for (name, contents) in &context.options().input_header_contents {
5367+
for (name, contents, _) in &context.options().input_header_contents
5368+
{
53685369
writeln!(code, "// {name}\n{contents}")?;
53695370
}
53705371

bindgen/ir/context.rs

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2042,24 +2042,76 @@ If you encounter an error missing from this list, please file an issue or a PR!"
20422042
&mut self,
20432043
) -> Option<&mut clang::FallbackTranslationUnit> {
20442044
if self.fallback_tu.is_none() {
2045-
let file = format!(
2046-
"{}/.macro_eval.c",
2045+
let build_dir: String =
20472046
match self.options().clang_macro_fallback_build_dir {
2048-
Some(ref path) => path.as_os_str().to_str()?,
2049-
None => ".",
2050-
}
2051-
);
2047+
Some(ref path) => path.as_os_str().to_str()?.to_owned(),
2048+
None => ".".to_owned(),
2049+
};
2050+
let file = format!("{build_dir}/.macro_eval.c");
20522051

20532052
let index = clang::Index::new(false, false);
20542053

2055-
let mut header_names_to_compile = Vec::new();
2056-
let mut header_paths = Vec::new();
2057-
let mut header_includes = Vec::new();
2058-
let [input_headers @ .., single_header] =
2059-
&self.options().input_headers[..]
2054+
// Materialize input_header_contents to disk for the fallback
2055+
// translation unit. header_contents() provides virtual files
2056+
// that only exist as unsaved buffers in the main TU — the
2057+
// fallback TU needs real files on disk for PCH compilation.
2058+
// The files must remain on disk while the PCH is in use
2059+
// (clang validates source file existence when loading a PCH).
2060+
//
2061+
// Both input_headers and input_header_contents are included
2062+
// so the PCH captures macro definitions from all sources.
2063+
let materialized_paths: Vec<String> = self
2064+
.options()
2065+
.input_header_contents
2066+
.iter()
2067+
.filter_map(|(_, contents, original_name)| {
2068+
// Sanitize the user-provided name so it always
2069+
// resolves under build_dir:
2070+
// - Root/prefix components are dropped (absolute
2071+
// paths can't bypass build_dir via Path::join).
2072+
// - ".." is resolved against the accumulated path
2073+
// but clamped to empty (can't escape build_dir).
2074+
// - "." is skipped.
2075+
// This preserves directory structure for collision
2076+
// avoidance (e.g. "a.h" vs "dir/a.h") while
2077+
// preventing any path from escaping build_dir.
2078+
use std::path::Component;
2079+
let mut parts: Vec<&std::ffi::OsStr> = Vec::new();
2080+
for c in Path::new(original_name.as_ref()).components() {
2081+
match c {
2082+
Component::Normal(s) => parts.push(s),
2083+
Component::ParentDir => {
2084+
parts.pop();
2085+
}
2086+
_ => {}
2087+
}
2088+
}
2089+
let relative: std::path::PathBuf = parts.iter().collect();
2090+
let disk_path = Path::new(&build_dir).join(&relative);
2091+
if let Some(parent) = disk_path.parent() {
2092+
std::fs::create_dir_all(parent).ok()?;
2093+
}
2094+
std::fs::write(&disk_path, contents.as_ref()).ok()?;
2095+
Some(disk_path.to_str()?.to_owned())
2096+
})
2097+
.collect();
2098+
2099+
let effective_headers: Vec<Box<str>> = self
2100+
.options()
2101+
.input_headers
2102+
.iter()
2103+
.cloned()
2104+
.chain(materialized_paths.iter().map(|p| p.clone().into()))
2105+
.collect();
2106+
2107+
let [input_headers @ .., single_header] = &effective_headers[..]
20602108
else {
20612109
return None;
20622110
};
2111+
2112+
let mut header_names_to_compile = Vec::new();
2113+
let mut header_paths = Vec::new();
2114+
let mut header_includes = Vec::new();
20632115
for input_header in input_headers {
20642116
let path = Path::new(input_header.as_ref());
20652117
if let Some(header_path) = path.parent() {
@@ -2071,17 +2123,15 @@ If you encounter an error missing from this list, please file an issue or a PR!"
20712123
} else {
20722124
header_paths.push(".");
20732125
}
2126+
// Use full path for -include to avoid basename
2127+
// collisions between headers in different directories.
2128+
header_includes.push(input_header.as_ref().to_string());
20742129
let header_name = path.file_name()?.to_str()?;
2075-
header_includes.push(header_name.to_string());
20762130
header_names_to_compile
20772131
.push(header_name.split(".h").next()?.to_string());
20782132
}
20792133
let pch = format!(
2080-
"{}/{}",
2081-
match self.options().clang_macro_fallback_build_dir {
2082-
Some(ref path) => path.as_os_str().to_str()?,
2083-
None => ".",
2084-
},
2134+
"{build_dir}/{}",
20852135
header_names_to_compile.join("-") + "-precompile.h.pch"
20862136
);
20872137

@@ -2118,8 +2168,12 @@ If you encounter an error missing from this list, please file an issue or a PR!"
21182168
c_args.push(arg.clone());
21192169
}
21202170
}
2121-
self.fallback_tu =
2122-
Some(clang::FallbackTranslationUnit::new(file, pch, &c_args)?);
2171+
self.fallback_tu = Some(clang::FallbackTranslationUnit::new(
2172+
file,
2173+
pch,
2174+
&c_args,
2175+
materialized_paths,
2176+
)?);
21232177
}
21242178

21252179
self.fallback_tu.as_mut()

0 commit comments

Comments
 (0)