1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
// Headers that we don't want to expose
static SKIP_HEADERS: &[&str] = &[
// dependend on NTL
r"^NTL-interface\.h$",
// platform dependent variables. In principle, it could be interesting to
// expose them, but since we ship the rust bindings, instead of building
// them at each compilation, it makes not much sense.
r"^config\.h$",
r"^flint-config\.h$",
r"^flint-mparam\.h$",
r"^thread_pool\.h$",
// Irrelevant, or too low-level (platform-dependent)
r"^crt_helpers\.h$",
r"^gettimeofday\.h$",
r"^machine_vectors\.h$",
r"^gmpcompat\.h$",
r"^longlong.*\.h$",
r"^fft_small\.h$",
r"^profiler\.h$",
r"^test_helpers\.h$",
r"^.*templates\.h$",
r"^.*_tables\.h$",
r"^.*impl\.h$",
// depend on GMP. FIXME
r"^fft\.h$",
r"^mpn_extras\.h$",
];
// Some public headers repeat declarations from other headers. In principle
// bindgen can handle that, but since we call bindgen separately on every header
// file, it loses the information. We Keep this as a small hand-curated list
// rather than adding complex global dedup logic.
static SKIP_ITEMS: &[(&str, &[&str])] = &[
(
"flint.h",
&["n_randlimb", "n_randtest", "n_randtest_not_zero"],
),
(
"gr_generic.h",
&[
"gr_generic_ctx_predicate",
"gr_generic_ctx_predicate_true",
"gr_generic_ctx_predicate_false",
],
),
("mpn_mod.h", &["gr_ctx_init_mpn_mod"]),
];
pub struct BindingGeneration {
// where to find `flint/flint.h`
flint_include_dir: PathBuf,
// a folder where we copy the header files before (maybe) patching.
tmp_dir: tempfile::TempDir,
// Target file.
flint_rs: PathBuf,
}
impl BindingGeneration {
pub fn new(flint_include_dir: PathBuf, flint_rs: PathBuf) -> Result<Self> {
let tmp_dir = tempfile::tempdir()?;
Ok(Self {
flint_include_dir,
flint_rs,
tmp_dir,
})
}
// Temporary bindgen workaround for FLINT headers where `mpoly_void_ring_t` is
// an array typedef over an anonymous struct. Bindgen then emits unstable
// `_bindgen_ty_*` names in unrelated headers. Naming the struct gives bindgen a
// stable Rust type. Remove this when upstream FLINT has the fix.
fn patch_flint_mpoly_void_ring_type(&self) -> Result<()> {
let header = &self.tmp_dir.path().join("mpoly_types.h");
let source = std::fs::read_to_string(header)
.with_context(|| format!("Failed to read `{}`", header.display()))?;
if source.contains("mpoly_void_ring_struct") {
return Ok(());
}
let patched = source.replace(
"} mpoly_void_ring_t[1];",
"} mpoly_void_ring_struct;\n\ntypedef mpoly_void_ring_struct mpoly_void_ring_t[1];",
);
anyhow::ensure!(
patched != source,
"Could not find anonymous `mpoly_void_ring_t` declaration in `{}`",
header.display()
);
std::fs::write(header, patched)
.with_context(|| format!("Failed to patch `{}`", header.display()))?;
Ok(())
}
// Copy all FLINT headers to self.overlay_dir, and apply patches.
// Returns all FLINT headers that bindgen should visit.
fn flint_headers(&self) -> Result<Vec<PathBuf>> {
let skip_patterns = SKIP_HEADERS
.iter()
.map(|pattern| regex::Regex::new(pattern))
.collect::<std::result::Result<Vec<_>, _>>()
.context("Invalid SKIP_HEADERS regex")?;
let header_dir = &self.flint_include_dir.join("flint");
anyhow::ensure!(
header_dir.join("flint.h").is_file(),
"Cannot find FLINT header `flint/flint.h` in `{}`",
self.flint_include_dir.display()
);
let mut headers = Vec::new();
for entry in std::fs::read_dir(&header_dir)? {
let path = entry?.path();
if path.extension().and_then(std::ffi::OsStr::to_str) != Some("h") {
continue;
}
let Some(file_name) = path.file_name().and_then(std::ffi::OsStr::to_str) else {
continue;
};
// Copy ALL the header files
let overlay = self.tmp_dir.path().join(file_name);
println!("cargo::rerun-if-changed={}", path.display());
std::fs::copy(&path, &overlay)
.with_context(|| format!("Failed to copy `{}`", path.display()))?;
if skip_patterns.iter().any(|re| re.is_match(file_name)) {
continue;
}
// Only return the header files that bindgen should inspect
headers.push(overlay);
}
headers.sort();
self.patch_flint_mpoly_void_ring_type()?;
Ok(headers)
}
pub fn generate_bindings(&self) -> Result<()> {
use std::io::Write;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
let headers = self.flint_headers()?;
// FLINT implements many inline functions by compiling module-specific
// `inlines.c` files into libflint. Defining the matching `*_INLINES_C`
// macros tells headers to expose those functions as external
// declarations, so bindgen can generate Rust bindings without creating
// wrapper C files.
let inline_macro_pattern =
regex::Regex::new(r"(?m)^\s*#\s*ifdef\s+([A-Z0-9_]+_INLINES_C)\b")
.context("Invalid inline macro regex")?;
let mut inline_macros = Vec::new();
for header in &headers {
let source = std::fs::read_to_string(header)
.with_context(|| format!("Failed to read `{}`", header.display()))?;
inline_macros.extend(
inline_macro_pattern
.captures_iter(&source)
.map(|captures| captures[1].to_owned()),
);
}
inline_macros.sort();
inline_macros.dedup();
let _flint_rs = std::fs::File::create(&self.flint_rs)?;
let mut flint_rs = std::io::BufWriter::new(_flint_rs);
write!(flint_rs, "/* automatically generated by rust-bindgen */")?;
let header_strings = headers
.iter()
.map(|header| {
let h = header.to_str().context("Non unicode header path")?;
Ok(h.to_owned())
})
.collect::<Result<Vec<_>>>()?;
let mut generated = std::iter::repeat_with(|| None)
.take(header_strings.len())
.collect::<Vec<Option<(String, String)>>>();
let next_header = Arc::new(AtomicUsize::new(0));
let worker_count = std::thread::available_parallelism()
.map_or(1, usize::from)
.min(header_strings.len());
// Bindgen is invoked once per header. This keeps each clang parse small
// and makes the work easy to parallelize. The final file is assembled in
// sorted header order, so output remains deterministic.
std::thread::scope(|scope| -> Result<()> {
let mut tasks = Vec::with_capacity(worker_count);
for _ in 0..worker_count {
let next_header = Arc::clone(&next_header);
let header_strings = &header_strings;
let inline_macros = &inline_macros;
tasks.push(
scope.spawn(move || -> Result<Vec<(usize, String, String)>> {
let mut generated = Vec::new();
loop {
let index = next_header.fetch_add(1, Ordering::Relaxed);
let Some(h) = header_strings.get(index).cloned() else {
break;
};
let header_name = Path::new(&h)
.file_name()
.and_then(std::ffi::OsStr::to_str)
.context("Header path has no file name")?;
let mut builder = bindgen::Builder::default()
.clang_arg("-DFLINT_NOSTDARG")
.disable_header_comment()
.header(&h)
.allowlist_file(regex::escape(&h))
.allowlist_recursively(false)
.blocklist_var(".*")
.blocklist_function(".*_mpn.*")
.blocklist_function(".*_mpz.*")
.derive_default(true)
.derive_copy(false)
.derive_debug(false)
.default_non_copy_union_style(
bindgen::NonCopyUnionStyle::ManuallyDrop,
)
.generate_cstr(true)
.merge_extern_blocks(true)
.rust_target(bindgen::RustTarget::stable(82, 0).ok().unwrap())
.rust_edition(bindgen::RustEdition::Edition2021)
.layout_tests(false)
.formatter(bindgen::Formatter::Prettyplease);
for inline_macro in inline_macros {
builder = builder.clang_arg(format!("-D{inline_macro}"));
}
for (_, items) in SKIP_ITEMS
.iter()
.filter(|(header, _)| *header == header_name)
{
for item in *items {
builder = builder.blocklist_function(format!("^{item}$"));
}
}
let bindings = builder
.generate()
.context("Failed to generate FLINT type bindings")?;
let stem = Path::new(&h)
.file_stem()
.and_then(std::ffi::OsStr::to_str)
.context("Header path has no file stem")?
.to_owned();
// Each bindgen run starts anonymous names at
// `_bindgen_ty_1`. Prefix them with the header stem
// before concatenating all generated fragments.
generated.push((
index,
h,
bindings
.to_string()
.replace("_bindgen_", &format!("_{stem}_bindgen_")),
));
}
Ok(generated)
}),
);
}
for task in tasks {
for (index, h, bindings) in task.join().expect("bindgen worker panicked")? {
generated[index] = Some((h, bindings));
}
}
Ok(())
})?;
for entry in generated {
let (h, bindings) = entry.context("Missing generated bindings")?;
let file_name = std::path::Path::new(&h)
.file_name()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or(&h);
write!(flint_rs, "\n\n/* {} */\n\n{}", file_name, bindings)?;
}
println!("cargo::rerun-if-env-changed=KEEP_BINDGEN_OUTPUT");
if std::env::var_os("KEEP_BINDGEN_OUTPUT").is_some() {
std::fs::copy(&self.flint_rs, "bindgen/flint.rs")
.context("Failed to copy generated bindings")?;
}
Ok(())
}
}