Since Compiler Interrupts is an LLVM pass, we want to extend cargo to support applying a third-party LLVM pass to the binary during the compilation process seamlessly. cargo-compiler-interrupts made specifically to integrate the Compiler Interrupts in just one command.
- If the library hasn't been installed yet, run
cargo-lib-ci installto install the library first. Make sure you have Rust and LLVM toolchain installed. - Register the Compiler Interrupts handler in your program. Compiler Interrupts APIs are provided through the
compiler-interruptspackage. You can check out theci-demoin thecompiler-interruptspackage for more detailed usages.
fn interrupt_handler(ic: i64) {
println!("Compiler interrupt called with instruction count: {}", ic);
}
fn main() {
unsafe {
compiler_interrupts::register(1000, 1000, interrupt_handler);
}
// your code
for _ in 0..100 {
println!("hello world!");
}
}- Run
cargo-build-cito start the compilation and integration processes. - Run
cargo-run-cito run the CI-integrated binary.
cargo-compiler-interrupts provides three binaries:
Compile and integrate the Compiler Interrupts to a package
Usage: cargo-build-ci [OPTIONS] [-- <CARGO_BUILD_ARGS>...]
Arguments:
[CARGO_BUILD_ARGS]... Arguments for `cargo` invocation
Options:
--skip <CRATES> Crates to skip the integration (space-delimited)
--debug Enable debugging mode for Compiler Interrupts library
--log <LEVEL> Log level [default: warn] [possible values: trace, debug, info, warn, error]
-h, --help Print help information
-V, --version Print version information
Run a Compiler Interrupts-integrated binary
Usage: cargo-run-ci [OPTIONS] [-- <ARGS>...] [-- <CARGO_RUN_ARGS>...]
Arguments:
[ARGS]... Arguments for the binary
[CARGO_RUN_ARGS]... Arguments for `cargo` invocation
Options:
--bin <NAME> Name of the binary
--log <LEVEL> Log level [default: warn] [possible values: trace, debug, info, warn, error]
-h, --help Print help information
-V, --version Print version information
Manage the Compiler Interrupts library
Usage: cargo-lib-ci [OPTIONS] [COMMAND]
Commands:
install Install the Compiler Interrupts library
uninstall Uninstall the Compiler Interrupts library
update Update the Compiler Interrupts library
config Configure the Compiler Interrupts library
help Print this message or the help of the given subcommand(s)
Options:
--log <LEVEL> Log level [default: warn] [possible values: trace, debug, info, warn, error]
-h, --help Print help information
-V, --version Print version information
cargo build-ciwill invokecargo buildwithRUSTC_LOG=rustc_codegen_ssa::back::link=infoto output internal linker invocations. It also adds a bunch of extra flags to allrustcinvocations. Extra flags are:--emit=llvm-ir— emit LLVM IR bitcode in the LLVM assembly language format.-C save-temps=y— all temporary output files during the compilation.-C passes=...— LLVM optimization passes for optimizing CI overhead.
- After
cargo buildcompleted, we should have these:- Output from
cargo buildcontains internal linker commands that are generated byrustcfor every library and binary. - Object
*.ofiles and IR bitcode in the LLVM assembly language*.llfiles in the$CARGO_TARGET_DIR/<build_mode>/depsdirectory. Moreover, each file should have a corresponding intermediate version that containsrcgu(rust codegen unit) in their name. - Rust static library with extra metadata
*.rlibfiles. These files are generated if the project has extra modules and dependencies.
- Output from
- Run
opton all intermediate IR bitcode*.llfiles to integrate the Compiler Interrupts. All CI-integrated files have the suffix_ciin their name. - Run
llcto convert CI-integrated IR bitcode*.llfiles to object*.ofiles. - Parse the output from
cargo buildto get the linker command for the binary. The linker command consists of a variety of arguments relating to the output file, linking rust-std/system libraries, and specifying*.rlibdependencies for the binary. - Find the allocator shim, which is a special intermediate object file that contains the symbols for the Rust memory allocator.
rustcautomatically generates the allocator shim behind the scene. - Replace the object file in the
*.rlibwith the CI-integrated one. - Execute the linker command again to output the final CI-integrated binary.
- All CI-integrated artifacts are output to
$CARGO_TARGET_DIR/<build_mode>/deps-ci. CI-integrated binary has their name appended with-cisuffix.
cargo buildoutputs the artifacts for us to replace the object files with the CI-integrated one, then we invoke the linker one more time to output the new CI-integrated binary. Therefore, we have to compile the binary twice.- Assuming the Compiler Interrupts does not depend on built-in
optoptimizations, we can make some changes torustcso that it can load and register a third-party LLVM pass during the compilation, hence eliminating theoptstage and linking after that, making the process done in one go. As a matter of fact,clangsupports loading and registering a third-party LLVM pass by runningclang -Xclang -load -Xclang mypass.so, albeit the usage is more complicated thanoptand does not support built-in passes fromopt. Currently, there is a request to the Rust compiler team to enable this functionality. - Since we have to depend on the build output,
cargo-compiler-interruptsmight not be robust against major changes. - Compiler Interrupts integration is not fast on huge IR bitcode from crates such as
clap,derive,proc,regex,serde,syn,toml,... We roughly estimate the integration process takes about an hour for 500,000 lines of IR bitcode on an x86-64 quad-core machine.