scx_utils/bpf_builder.rs
1// Copyright (c) Meta Platforms, Inc. and affiliates.
2//
3// This software may be used and distributed according to the terms of the
4// GNU General Public License version 2.
5
6use crate::clang_info::ClangInfo;
7use anyhow::Context;
8use anyhow::Result;
9use anyhow::anyhow;
10use glob::glob;
11use libbpf_cargo::SkeletonBuilder;
12use libbpf_rs::Linker;
13use std::collections::BTreeSet;
14use std::env;
15use std::path::Path;
16use std::path::PathBuf;
17
18#[derive(Debug)]
19/// # Build helpers for sched_ext schedulers with Rust userspace component
20///
21/// This is to be used from `build.rs` of a cargo project which implements a
22/// [sched_ext](https://github.com/sched-ext/scx) scheduler with C BPF
23/// component and Rust userspace component. `BpfBuilder` provides everything
24/// necessary to build the BPF component and generate Rust bindings.
25/// BpfBuilder provides the followings.
26///
27/// 1. *`vmlinux.h` and other common BPF header files*
28///
29/// All sched_ext BPF implementations require `vmlinux.h` and many make use
30/// of common constructs such as
31/// [`user_exit_info`](https://github.com/sched-ext/scx/blob/main/scheds/include/common/user_exit_info.h).
32/// `BpfBuilder` makes these headers available when compiling BPF source
33/// code and generating bindings for it. The included headers can be browsed
34/// at <https://github.com/sched-ext/scx/tree/main/scheds/include>.
35///
36/// These headers can be superseded using environment variables which will
37/// be discussed later.
38///
39/// 2. *Header bindings using `bindgen`*
40///
41/// If enabled with `.enable_intf()`, the input `.h` file is processed by
42/// `bindgen` to generate Rust bindings. This is useful in establishing
43/// shared constants and data types between the BPF and user components.
44///
45/// Note that the types generated with `bindgen` are different from the
46/// types used by the BPF skeleton even when they are the same types in BPF.
47/// This is a source of ugliness and we are hoping to address it by
48/// improving `libbpf-cargo` in the future.
49///
50/// 3. *BPF compilation and generation of the skeleton and its bindings*
51///
52/// If enabled with `.enable_skel()`, the input `.bpf.c` file is compiled
53/// and its skeleton and bindings are generated using `libbpf-cargo`.
54///
55/// ## An Example
56///
57/// This section shows how `BpfBuilder` can be used in an example project.
58/// For a concrete example, take a look at
59/// [`scx_rusty`](https://github.com/sched-ext/scx/tree/main/scheds/rust/scx_rusty).
60///
61/// A minimal source tree using all features would look like the following:
62///
63/// ```text
64/// scx_hello_world
65/// |-- Cargo.toml
66/// |-- build.rs
67/// \-- src
68/// |-- main.rs
69/// |-- bpf_intf.rs
70/// |-- bpf_skel.rs
71/// \-- bpf
72/// |-- intf.h
73/// \-- main.c
74/// ```
75///
76/// The following three files would contain the actual implementation:
77///
78/// - `src/main.rs`: Rust userspace component which loads the BPF blob and
79/// interacts it using the generated bindings.
80///
81/// - `src/bpf/intf.h`: C header file definining constants and structs
82/// that will be used by both the BPF and userspace components.
83///
84/// - `src/bpf/main.c`: C source code implementing the BPF component -
85/// including `struct sched_ext_ops`.
86///
87/// And then there are boilerplates to generate the bindings and make them
88/// available as modules to `main.rs`.
89///
90/// - `Cargo.toml`: Includes `scx_utils` in the `[build-dependencies]`
91/// section.
92///
93/// - `build.rs`: Uses `scx_utils::BpfBuilder` to build and generate
94/// bindings for the BPF component. For this project, it can look like the
95/// following.
96///
97/// ```should_panic
98/// fn main() {
99/// scx_utils::BpfBuilder::new()
100/// .unwrap()
101/// .enable_intf("src/bpf/intf.h", "bpf_intf.rs")
102/// .enable_skel("src/bpf/main.bpf.c", "bpf")
103/// .build()
104/// .unwrap();
105/// }
106/// ```
107///
108/// - `bpf_intf.rs`: Import the bindings generated by `bindgen` into a
109/// module. Above, we told `.enable_intf()` to generate the bindings into
110/// `bpf_intf.rs`, so the file would look like the following. The `allow`
111/// directives are useful if the header is including `vmlinux.h`.
112///
113/// ```ignore
114/// #![allow(non_upper_case_globals)]
115/// #![allow(non_camel_case_types)]
116/// #![allow(non_snake_case)]
117/// #![allow(dead_code)]
118///
119/// include!(concat!(env!("OUT_DIR"), "/bpf_intf.rs"));
120/// ```
121///
122/// - `bpf_skel.rs`: Import the BPF skeleton bindings generated by
123/// `libbpf-cargo` into a module. Above, we told `.enable_skel()` to use the
124/// skeleton name `bpf`, so the file would look like the following.
125///
126/// ```ignore
127/// include!(concat!(env!("OUT_DIR"), "/bpf_skel.rs"));
128/// ```
129///
130/// ## Compiler Flags and Environment Variables
131///
132/// BPF being its own CPU architecture and independent runtime environment,
133/// build environment and steps are already rather complex. The need to
134/// interface between two different languages - C and Rust - adds further
135/// complexities. `BpfBuilder` automates most of the process. The determined
136/// build environment is recorded in the `build.rs` output and can be
137/// obtained with a command like the following:
138///
139/// ```text
140/// $ grep '^scx_utils:clang=' target/release/build/scx_rusty-*/output
141/// ```
142///
143/// While the automatic settings should work most of the time, there can be
144/// times when overriding them is necessary. The following environment
145/// variables can be used to customize the build environment.
146///
147/// - `BPF_CLANG`: The clang command to use. (Default: `clang`)
148///
149/// - `BPF_CFLAGS`: Compiler flags to use when building BPF source code. If
150/// specified, the flags from this variable are the only flags passed to
151/// the compiler. `BpfBuilder` won't generate any flags including `-I`
152/// flags for the common header files and other `CFLAGS` related variables
153/// are ignored.
154///
155/// - `BPF_BASE_CFLAGS`: Override the non-include part of cflags.
156///
157/// - `BPF_EXTRA_CFLAGS_PRE_INCL`: Add cflags before the automic include
158/// search path options. Header files in the search paths added by this
159/// variable will supercede the automatic ones.
160///
161/// - `BPF_EXTRA_CFLAGS_POST_INCL`: Add cflags after the automic include
162/// search path options. Header paths added by this variable will be
163/// searched only if the target header file can't be found in the
164/// automatic header paths.
165///
166/// - `RUSTFLAGS`: This is a generic `cargo` flag and can be useful for
167/// specifying extra linker flags.
168///
169/// A common case for using the above flags is using the latest `libbpf`
170/// from the kernel tree. Let's say the kernel tree is at `$KERNEL` and
171/// `libbpf`. The following builds `libbpf` shipped with the kernel:
172///
173/// ```test
174/// $ cd $KERNEL
175/// $ make -C tools/bpf/bpftool
176/// ```
177///
178/// To link the scheduler against the resulting `libbpf`:
179///
180/// ```test
181/// $ env BPF_EXTRA_CFLAGS_POST_INCL=$KERNEL/tools/bpf/bpftool/libbpf/include \
182/// RUSTFLAGS="-C link-args=-lelf -C link-args=-lz -C link-args=-lzstd \
183/// -L$KERNEL/tools/bpf/bpftool/libbpf" cargo build --release
184/// ```
185pub struct BpfBuilder {
186 clang: ClangInfo,
187 cflags: Vec<String>,
188 out_dir: PathBuf,
189 sources: BTreeSet<String>,
190
191 intf_input_output: Option<(String, String)>,
192 skel_input_name: Option<(String, String)>,
193}
194
195impl BpfBuilder {
196 const BPF_H_TAR: &'static [u8] = include_bytes!(concat!(env!("OUT_DIR"), "/bpf_h.tar"));
197
198 fn install_bpf_h<P: AsRef<Path>>(dest: P) -> Result<()> {
199 let mut ar = tar::Archive::new(Self::BPF_H_TAR);
200 ar.unpack(dest)?;
201 Ok(())
202 }
203
204 fn determine_cflags<P>(clang: &ClangInfo, out_dir: P) -> Result<Vec<String>>
205 where
206 P: AsRef<Path> + std::fmt::Debug,
207 {
208 let bpf_h = out_dir
209 .as_ref()
210 .join("scx_utils-bpf_h")
211 .to_str()
212 .ok_or(anyhow!(
213 "{:?}/scx_utils-bph_h can't be converted to str",
214 &out_dir
215 ))?
216 .to_string();
217 Self::install_bpf_h(&bpf_h)?;
218
219 let mut cflags = Vec::<String>::new();
220
221 cflags.append(&mut match env::var("BPF_BASE_CFLAGS") {
222 Ok(v) => v.split_whitespace().map(|x| x.into()).collect(),
223 _ => clang.determine_base_cflags()?,
224 });
225
226 cflags.append(&mut match env::var("BPF_EXTRA_CFLAGS_PRE_INCL") {
227 Ok(v) => v.split_whitespace().map(|x| x.into()).collect(),
228 _ => vec![],
229 });
230
231 cflags.push(format!(
232 "-I{}/arch/{}",
233 &bpf_h,
234 &clang.kernel_target().unwrap()
235 ));
236 cflags.push(format!("-I{}", &bpf_h));
237 cflags.push(format!("-I{}/bpf-compat", &bpf_h));
238
239 cflags.append(&mut match env::var("BPF_EXTRA_CFLAGS_POST_INCL") {
240 Ok(v) => v.split_whitespace().map(|x| x.into()).collect(),
241 _ => vec![],
242 });
243
244 Ok(cflags)
245 }
246
247 /// Create a new `BpfBuilder` struct. Call `enable` and `set` methods to
248 /// configure and `build` method to compile and generate bindings. See
249 /// the struct documentation for details.
250 pub fn new() -> Result<Self> {
251 let out_dir = PathBuf::from(env::var("OUT_DIR")?);
252
253 let clang = ClangInfo::new()?;
254 let cflags = match env::var("BPF_CFLAGS") {
255 Ok(v) => v.split_whitespace().map(|x| x.into()).collect(),
256 _ => Self::determine_cflags(&clang, &out_dir)?,
257 };
258
259 println!("scx_utils:clang={:?} {:?}", &clang, &cflags);
260
261 Ok(Self {
262 clang,
263 cflags,
264 out_dir,
265
266 sources: BTreeSet::new(),
267 intf_input_output: None,
268 skel_input_name: None,
269 })
270 }
271
272 /// Enable generation of header bindings using `bindgen`. `@input` is
273 /// the `.h` file defining the constants and types to be shared between
274 /// BPF and Rust components. `@output` is the `.rs` file to be
275 /// generated.
276 pub fn enable_intf(&mut self, input: &str, output: &str) -> &mut Self {
277 self.intf_input_output = Some((input.into(), output.into()));
278 self
279 }
280
281 /// Enable compilation of BPF code and generation of the skeleton and
282 /// its Rust bindings. `@input` is the `.bpf.c` file containing the BPF
283 /// source code and `@output` is the `.rs` file to be generated.
284 pub fn enable_skel(&mut self, input: &str, name: &str) -> &mut Self {
285 self.skel_input_name = Some((input.into(), name.into()));
286 self.sources.insert(input.into());
287
288 self
289 }
290
291 fn input_insert_deps(&self, deps: &mut BTreeSet<String>) -> () {
292 let (input, _) = match &self.intf_input_output {
293 Some(pair) => pair,
294 None => return,
295 };
296
297 // Tell cargo to invalidate the built crate whenever the wrapper changes
298 deps.insert(input.to_string());
299 }
300
301 fn bindgen_bpf_intf(&self) -> Result<()> {
302 let (input, output) = match &self.intf_input_output {
303 Some(pair) => pair,
304 None => return Ok(()),
305 };
306
307 // The bindgen::Builder is the main entry point to bindgen, and lets
308 // you build up options for the resulting bindings.
309 let bindings = bindgen::Builder::default()
310 // Should run clang with the same -I options as BPF compilation.
311 .clang_args(
312 self.cflags
313 .iter()
314 .chain(["-target".into(), "bpf".into()].iter()),
315 )
316 // The input header we would like to generate bindings for.
317 .header(input)
318 // Tell cargo to invalidate the built crate whenever any of the
319 // included header files changed.
320 .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
321 .generate()
322 .context("Unable to generate bindings")?;
323
324 bindings
325 .write_to_file(self.out_dir.join(output))
326 .context("Couldn't write bindings")
327 }
328
329 pub fn add_source(&mut self, input: &str) -> &mut Self {
330 self.sources.insert(input.into());
331 self
332 }
333
334 pub fn compile_link_gen(&mut self) -> Result<()> {
335 let input = match &self.skel_input_name {
336 Some((name, _)) => name,
337 None => return Ok(()),
338 };
339
340 // Better to hardcode the name because it gets embedded in
341 // all the skeleton struct/member definitions.
342 let linkobj = self.out_dir.join("bpf.bpf.o");
343 let mut linker = Linker::new(&linkobj)?;
344
345 for filename in self.sources.iter() {
346 let name = Path::new(filename).file_name().unwrap().to_str().unwrap();
347 let obj = self.out_dir.join(name.replace(".bpf.c", ".bpf.o"));
348
349 let output = SkeletonBuilder::new()
350 .debug(true)
351 .source(filename)
352 .obj(&obj)
353 .clang(&self.clang.clang)
354 .clang_args(&self.cflags)
355 .build()?;
356
357 for line in String::from_utf8_lossy(output.stderr()).lines() {
358 println!("cargo:warning={}", line);
359 }
360
361 linker.add_file(&obj)?;
362 }
363
364 linker.link()?;
365
366 self.bindgen_bpf_intf()?;
367
368 let skel_path = self.out_dir.join("bpf_skel.rs");
369
370 SkeletonBuilder::new()
371 .obj(&linkobj)
372 .clang(&self.clang.clang)
373 .clang_args(&self.cflags)
374 .generate(&skel_path)?;
375
376 let mut deps = BTreeSet::new();
377 self.add_src_deps(&mut deps, input)?;
378 for filename in self.sources.iter() {
379 deps.insert(filename.to_string());
380 }
381
382 self.gen_cargo_reruns(Some(&deps))?;
383
384 Ok(())
385 }
386
387 fn gen_bpf_skel(&self, deps: &mut BTreeSet<String>) -> Result<()> {
388 let (input, name) = match &self.skel_input_name {
389 Some(pair) => pair,
390 None => return Ok(()),
391 };
392
393 let obj = self.out_dir.join(format!("{}.bpf.o", name));
394 let skel_path = self.out_dir.join(format!("{}_skel.rs", name));
395
396 let output = SkeletonBuilder::new()
397 .source(input)
398 .obj(&obj)
399 .clang(&self.clang.clang)
400 .clang_args(&self.cflags)
401 .build_and_generate(&skel_path)?;
402
403 for line in String::from_utf8_lossy(output.stderr()).lines() {
404 println!("cargo:warning={}", line);
405 }
406
407 self.add_src_deps(deps, input)?;
408
409 Ok(())
410 }
411
412 fn add_src_deps(&self, deps: &mut BTreeSet<String>, input: &str) -> Result<()> {
413 let c_path = PathBuf::from(input);
414 let dir = c_path
415 .parent()
416 .ok_or(anyhow!("Source {:?} doesn't have parent dir", c_path))?
417 .to_str()
418 .ok_or(anyhow!("Parent dir of {:?} isn't a UTF-8 string", c_path))?;
419
420 for path in glob(&format!("{}/*.[hc]", dir))?.filter_map(Result::ok) {
421 deps.insert(
422 path.to_str()
423 .ok_or(anyhow!("Path {:?} is not a valid string", path))?
424 .to_string(),
425 );
426 }
427
428 Ok(())
429 }
430
431 fn gen_cargo_reruns(&self, dependencies: Option<&BTreeSet<String>>) -> Result<()> {
432 println!("cargo:rerun-if-env-changed=BPF_CLANG");
433 println!("cargo:rerun-if-env-changed=BPF_CFLAGS");
434 println!("cargo:rerun-if-env-changed=BPF_BASE_CFLAGS");
435 println!("cargo:rerun-if-env-changed=BPF_EXTRA_CFLAGS_PRE_INCL");
436 println!("cargo:rerun-if-env-changed=BPF_EXTRA_CFLAGS_POST_INCL");
437 if let Some(deps) = dependencies {
438 for dep in deps.iter() {
439 println!("cargo:rerun-if-changed={}", dep);
440 }
441 };
442
443 for source in self.sources.iter() {
444 println!("cargo:rerun-if-changed={}", source);
445 }
446
447 Ok(())
448 }
449
450 /// Build and generate the enabled bindings.
451 pub fn build(&self) -> Result<()> {
452 let mut deps = BTreeSet::new();
453
454 self.input_insert_deps(&mut deps);
455
456 self.bindgen_bpf_intf()?;
457 self.gen_bpf_skel(&mut deps)?;
458 self.gen_cargo_reruns(Some(&deps))?;
459 Ok(())
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use regex::Regex;
466 use sscanf::sscanf;
467
468 use crate::builder::ClangInfo;
469
470 #[test]
471 fn test_bpf_builder_new() {
472 let td = tempfile::tempdir().unwrap();
473 unsafe { std::env::set_var("OUT_DIR", td.path()) };
474
475 let res = super::BpfBuilder::new();
476 assert!(res.is_ok(), "Failed to create BpfBuilder ({:?})", &res);
477 }
478
479 #[test]
480 fn test_vmlinux_h_ver_sha1() {
481 let clang_info = ClangInfo::new().unwrap();
482
483 let mut ar = tar::Archive::new(super::BpfBuilder::BPF_H_TAR);
484 let mut found = false;
485
486 let pattern = Regex::new(r"arch\/.*\/vmlinux-.*.h").unwrap();
487
488 for entry in ar.entries().unwrap() {
489 let entry = entry.unwrap();
490 let file_name = entry.header().path().unwrap();
491 let file_name_str = file_name.to_string_lossy().to_owned();
492 if file_name_str.contains(&clang_info.kernel_target().unwrap()) {
493 found = true;
494 }
495 if !pattern.find(&file_name_str).is_some() {
496 continue;
497 }
498
499 println!("checking {file_name_str}");
500
501 let (arch, ver, sha1) =
502 sscanf!(file_name_str, "arch/{String}/vmlinux-v{String}-g{String}.h").unwrap();
503 println!(
504 "vmlinux.h: arch={:?} ver={:?} sha1={:?}",
505 &arch, &ver, &sha1,
506 );
507
508 assert!(
509 regex::Regex::new(r"^([1-9][0-9]*\.[1-9][0-9][a-z0-9-]*)$")
510 .unwrap()
511 .is_match(&ver)
512 );
513 assert!(
514 regex::Regex::new(r"^[0-9a-z]{12}$")
515 .unwrap()
516 .is_match(&sha1)
517 );
518 }
519
520 assert!(found);
521 }
522}