rllvm 0.4.0

Extract whole-program LLVM bitcode from any build.

Point your build system at rllvm’s compiler wrappers, build normally, then pull a single .bc for the whole program back out of the finished binary.

cargo install rllvm
  1. parse.cparse.bc
  2. lexer.clexer.bc
  3. codegen.ccodegen.bc
  4. util.cutil.bc
  5. main.cmain.bc
__RLLVM,__rllvm_bc prog.bc

Features

Quick start

Install LLVM/Clang, then rllvm:

brew install llvm                                    # macOS
sudo apt install llvm llvm-dev clang libclang-dev    # Ubuntu / Debian

cargo install rllvm

Build something and extract its bitcode:

rllvm-cc -o hello hello.c
rllvm-get-bc hello              # produces hello.bc

Or point an existing project at it:

export CC=rllvm-cc CXX=rllvm-cxx

./configure && make             # autotools
cmake -B build && cmake --build build

rllvm-get-bc build/my_program

On first run rllvm writes a config with tool paths discovered from llvm-config.

Usage

Extracting

rllvm-get-bc hello                          # executable  -> hello.bc
rllvm-get-bc libfoo.a                       # archive     -> libfoo.a.bc
rllvm-get-bc -b libfoo.a                    # bitcode archive -> libfoo.bca
rllvm-get-bc --merge-strategy partial prog  # stage the merge by directory
rllvm-get-bc -m hello                       # also write hello.bc.manifest
rllvm-get-bc -o out.bc hello

Wrapper flags

Wrapper options are long-only and prefixed --rllvm-, so they cannot collide with a compiler flag. Everything else — including -c, -v, --help and --version — goes straight to the compiler, because build systems identify the compiler by running $CC --version.

--rllvm-compiler <PATH>   Override the wrapped compiler path
--rllvm-verbose[=LEVEL]   Log verbosity; bare flag is level 1, max 4
--rllvm-help              Print help for the wrapper
--rllvm-version           Print the wrapper version

A -- separator is still accepted, so existing shim scripts keep working.

CMake toolchain file

cmake -B build -DCMAKE_TOOLCHAIN_FILE=path/to/rllvm/cmake/rllvm-toolchain.cmake
cmake --build build
rllvm-get-bc build/my_program

See examples/cmake/.

Rust and cargo

RUSTC_WRAPPER=rllvm-rustc cargo build
rllvm-get-bc target/debug/my_program

Every crate in the graph contributes, so the extracted module covers dependencies as well as the binary’s own code. A library crate works on its own:

rllvm-get-bc target/debug/deps/libmylib-<hash>.rlib

WebAssembly

rllvm-cc --target=wasm32-unknown-unknown -c -o lib.o lib.c
rllvm-cc --target=wasm32-unknown-unknown -nostdlib -Wl,--no-entry \
    -o app.wasm lib.o main.o
rllvm-get-bc app.wasm -o app.bc

Linking needs wasm-ld, which ships with LLD rather than LLVM and must match your LLVM version. See examples/wasm/.

Relocatable bitcode paths

By default an object records the absolute path of its bitcode, which pins it to the directory that built it. Set a root to record paths relative to it, then name the root again when extracting:

export RLLVM_BITCODE_ROOT=/path/to/build
make

# later, after the tree has moved:
rllvm-get-bc --bitcode-root /new/path/to/build prog -o prog.bc

Objects built without a root keep absolute paths and are unaffected — the extractor tells the two apart by the leading separator, so both forms can appear in the same binary.

Configuration

A TOML file, created on first run with paths inferred from llvm-config. It lives at $RLLVM_CONFIG if set, otherwise ~/.rllvm/config.toml.

Key Required Description
llvm_config_filepath Yes Absolute path to llvm-config
clang_filepath Yes Absolute path to clang
clangxx_filepath Yes Absolute path to clang++
llvm_ar_filepath Yes Absolute path to llvm-ar
llvm_link_filepath Yes Absolute path to llvm-link
llvm_objcopy_filepath No Absolute path to llvm-objcopy; preferred for embedding, with an internal fallback
rustc_filepath No Absolute path to rustc (default: rustc on PATH)
bitcode_store_path No Directory for intermediate bitcode files (must be absolute)
bitcode_root No Record embedded paths relative to this root (default: absolute)
llvm_link_flags No Extra flags for llvm-link
lto_ldflags No Extra flags for link-time optimization
bitcode_generation_flags No Extra flags for bitcode generation (e.g. -flto)
lto_mode No How -flto builds record bitcode: marker (default), save-temps, skip; RLLVM_LTO_MODE overrides
is_configure_only No Skip bitcode generation entirely (default: false)
cache_enabled No Reuse bitcode across rebuilds; also RLLVM_CACHE=1 (default: false)
log_level No 0=error (default), 1=warn, 2=info, 3=debug, 4+=trace
llvm_config_filepath = '/opt/homebrew/opt/llvm/bin/llvm-config'
clang_filepath = '/opt/homebrew/opt/llvm/bin/clang'
clangxx_filepath = '/opt/homebrew/opt/llvm/bin/clang++'
llvm_ar_filepath = '/opt/homebrew/opt/llvm/bin/llvm-ar'
llvm_link_filepath = '/opt/homebrew/opt/llvm/bin/llvm-link'
log_level = 3

How it works

The wrappers run clang normally and, for each source, also emit a .bc. The absolute path of that .bc is written into a custom section of the object file, newline-terminated. The linker concatenates those sections, so the finished binary carries a list of every translation unit that went into it. rllvm-get-bc reads that list and links the bitcode into one module.

source.c ──► rllvm-cc ──► object file (with embedded .bc path)
                              │
                              ▼
executable ◄── linker ◄── object files
                              │
                              ▼
                        rllvm-get-bc ──► whole-program.bc

rllvm-rustc does the same per crate. A crate that links carries the path in a marker object added to the link; a crate that produces an .rlib carries it in the archive’s members, so a dependency brings its bitcode wherever it is used.

LTO

With -flto the compiler writes a bitcode module where an object file belongs, so there is no section to record a path in. lto_mode picks what happens instead.

marker (default) compiles a marker module naming the bitcode and merges it into the LTO object with llvm-link. Covers full and thin LTO, ELF and Mach-O, C and C++, and mixes with objects built without -flto. Costs one extra compile and one llvm-link per translation unit.

save-temps appends the linker’s save-temps flag, then collects the whole-program module the LTO pipeline merged, recording its path instead of per-unit paths. No per-unit compile. Full LTO only — ThinLTO builds no such module, and that case warns and collects nothing rather than failing the build. Needs a separate link step through rllvm-cc: a single-step rllvm-cc -flto a.c b.c -o prog is a compile, not an LTO link, so save-temps cannot hook it. The inputs must be LTO bitcode: -flto in LDFLAGS alone produces no merged module, and rllvm errors. The collected module is post-optimization — the module the linker generated code from.

skip generates nothing and warns — the old default behaviour.

An LTO link pulls in only the archive members it uses, so an unused member’s bitcode path never reaches the binary. COFF and WASM are not supported under marker; -flto there is an error directing to lto_mode = "skip". Under save-temps, a link producing no merged module is an error.

Relationship to gllvm and wllvm

rllvm started as a Rust port of gllvm (Go) and wllvm (Python), and keeps the same workflow: set CC/CXX, build, extract. It has since added WebAssembly support, a Rust wrapper, relocatable bitcode paths, and merge strategies.

The name follows the same convention: g for Go, w for whole-program-llvm, r for Rust.

rules_rllvm borrowed the name for the idea, not the implementation. It is pure Bazel rules and runs none of these binaries, so there r expands to rules — the name is recursive.

If gllvm or wllvm already work for you, there is no urgency to switch.

License

Apache-2.0