rllvm 0.7.1

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

Quick start

brew install h1994st/tap/rllvm llvm   # macOS or Linux with Homebrew
# Or: cargo install rllvm
# LLVM on Ubuntu/Debian: sudo apt install llvm llvm-dev clang libclang-dev
rllvm-cc hello.c -o hello
rllvm-get-bc hello       # writes hello.bc
rllvm-info hello.bc      # target, function, block, instruction counts

On first run, tool paths are detected from llvm-config and written to ~/.rllvm/config.toml.

Claude Code plugin

In Claude Code, the rllvm plugin covers the whole workflow, from installing rllvm to reading an answer:

/plugin marketplace add h1994st/rllvm
/plugin install rllvm@rllvm
Part What it does
setup skill Installs rllvm and rllvm-query with your approval, picks an LLVM every reader of the bitcode accepts, configures, and troubleshoots
capture skill Chooses how to capture your build — C, C++, Objective-C, Rust, mixed, a compilation database, cross, WebAssembly, eBPF — and what to extract
query skill Picks the query that answers your question and reports what the answer does not cover
complete-callgraph skill, /rllvm:complete-callgraph Resolves unresolved indirect calls into a labeled call-graph overlay, never reported as proven
Overlay view, /overlay-view A read-only pane showing each unresolved field, its candidates, and the overlay’s edges with confidence and verification, unsaved ones included, for the catalog that last answered
MCP server rllvm-query mcp, configured automatically
Cache warning A PostToolUse hook notes once per session when the facts cache passes query_cache_warn_mb; needs jq

Builds run as ordinary shell commands you approve, never inside the server. See the plugin example.

Coming from wllvm or gllvm?

rllvm follows the CC/CXX → build → extract workflow that wllvm and gllvm established. rllvm-cc and rllvm-cxx stand in for wllvm/gclang, rllvm-get-bc for extract-bc/get-bc.

Your build commands do not change. What you can capture, and what you can do with it afterwards, does.

  🌟 rllvm wllvm / gllvm
🦀 Languages C, C++, Objective-C, Rust C, C++, Fortran
🔍 Analysis Source-level queries, MCP server ❌
🤖 Agents Claude Code plugin: setup, capture, queries ❌
📋 No wrapper build Import compile_commands.json ❌
🎯 Targets Native, cross, WebAssembly, eBPF Native; cross with a target objcopy
🔗 LTO Three modes, dispatched on object content -flto unlikely to survive extraction
♻️ Rebuilds Cache validated against inputs No bitcode cache
📦 Moving a build tree Paths relative to a root Absolute bitcode paths
⚙️ Setup Detected from llvm-config Environment variables

How it works

The wrappers run Clang normally and also emit bitcode. Each object gets a custom section recording its bitcode path. The linker concatenates those sections, so the finished binary carries a list of every module that went into it; extraction reads the paths back out and merges the modules.

source.c → rllvm-cc → object + bitcode
                         ↓
                      linker → executable
                                   ↓
                              rllvm-get-bc → whole-program.bc

Rust captures bitcode per crate: linked crates carry paths through a marker object, library crates in archive members.

This is why capture only covers what you build through the wrappers. Linking a prebuilt native library contributes no bitcode, so build every dependency you need for a whole-program view.

Capturing bitcode

Languages

C, C++ and Objective-C

rllvm-cc wraps clang and rllvm-cxx wraps clang++. Objective-C (.m) and Objective-C++ (.mm) sources go through the same two wrappers.

rllvm-cc hello.m -o hello -framework Foundation
rllvm-get-bc hello       # writes hello.bc
# Autotools
CC=rllvm-cc CXX=rllvm-cxx ./configure && make
rllvm-get-bc path/to/program

# CMake
CC=rllvm-cc CXX=rllvm-cxx cmake -S . -B build && cmake --build build
rllvm-get-bc build/my_program

Add OBJC=rllvm-cc OBJCXX=rllvm-cxx for Objective-C projects. CMake also accepts -DCMAKE_TOOLCHAIN_FILE=path/to/rllvm/cmake/rllvm-toolchain.cmake, which sets all four compilers; see the CMake and Objective-C examples.

Rust

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

Wrapped dependency crates contribute modules when their archive members reach the link. You can also extract an .rlib directly, or invoke the wrapper without Cargo:

rllvm-get-bc 'target/debug/deps/libmylib-<hash>.rlib'
rllvm-rustc main.rs -o app && rllvm-get-bc app

cargo check and procedural-macro crates pass through without capture, and prebuilt dependencies — including the standard library — are not rebuilt. Neither is the allocator shim rustc adds to a staticlib (__rust_alloc and its siblings); rllvm-info lists such members as carrying no bitcode. Your LLVM readers must be compatible with the version rustc -vV reports. Use RLLVM_LOG_LEVEL=3 for diagnostics under Cargo.

See the Rust example.

Wrapper options

Every compiler flag reaches the real compiler, including -c, -v, --help and --version. Wrapper options are long-only, prefixed --rllvm-, and go first:

--rllvm-compiler <PATH>   Override clang/clang++ (C/C++ wrappers only)
--rllvm-verbose[=LEVEL]   Bare flag is 1; 3 logs subcommands, 4 enables trace
--rllvm-help              Print wrapper help
--rllvm-version           Print wrapper version
rllvm-cc --rllvm-verbose=3 -pthread -c hello.c -o hello.o
rllvm-cc @compile.rsp

Response files follow Clang’s GNU UTF-8 syntax, including quoting and nested references. Large generated commands use them automatically.

See the wrapper options example.

From a compilation database

rllvm-compdb compiles selected entries from an existing compile_commands.json, so you can capture bitcode without rebuilding through the wrappers:

rllvm-compdb list build/ > compilations.json
rllvm-compdb generate build/ --source src/example.c --output-dir analysis/example

list reports entry and configuration IDs without compiling. generate takes all entries by default; repeat --source or --entry to narrow, and combine them to intersect. Only direct clang/clang++ drivers are supported, and the output directory must be new. Modules are read with the llvm-dis in the configured llvm_bindir, so generate refuses a compiler from a newer LLVM major before compiling anything; Apple clang’s version names no LLVM major, so it only draws a warning.

These modules describe the current source tree, not membership in a real link. Use wrapper capture when participation in the actual build matters.

See the compilation database example.

Caching

Off by default. RLLVM_CACHE=1 enables it, storing in ~/.rllvm/cache:

RLLVM_CACHE=1 cmake --build build

Native compilation still runs — a hit only skips the extra bitcode compilation, after checking preprocessed inputs, dependencies, compiler, command, directory and environment. Keep it off when side inputs that preprocessing cannot see, such as optimization profiles, change between builds.

Build modes and targets

Linkers

The platform’s default linker and LLD both work. -fuse-ld=lld selects ld64.lld on macOS and ld.lld on Linux:

rllvm-cc -fuse-ld=lld lib.c app.c -o app
rllvm-get-bc app -o app.bc

See the LLD example.

LTO

Select lto_mode in the config or RLLVM_LTO_MODE, and use the same mode when compiling and linking:

Mode Captured bitcode Support
marker (default) Per-source modules recorded in LTO objects Full/ThinLTO; ELF/Mach-O; C/C++
save-temps Full-LTO linker’s merged, optimized module Separate links and combined source/link invocations
skip No additional capture; emits a warning Explicitly disabling capture for LTO
RLLVM_LTO_MODE=save-temps rllvm-cc -flto hello.c -o hello
rllvm-get-bc hello -o hello.bc

save-temps needs real LTO inputs — adding -flto only at link time is not enough — and ThinLTO has no single merged module, so use marker for it. A static library of -flto objects extracts without linking: its members are bitcode, and each one is taken as a module. COFF and WebAssembly reject marker and direct you to skip.

See the LTO example.

Cross-compilation

Pass --target=<triple> as you would to Clang. The triple reaches the link as well as the compile, so a one-shot build works:

rllvm-cc --target=aarch64-unknown-linux-gnu -fuse-ld=lld -nostdlib \
  lib.c app.c -o app
rllvm-get-bc app -o app.bc     # app.bc carries the cross triple

The recorded section follows the object’s format, not the host’s, so a Linux ELF built on macOS extracts like any other. Linking ELF off a non-ELF host needs LLD, and libc needs a sysroot as usual. Set llvm_objcopy_filepath for targets the internal fallback does not model, such as RISC-V.

Universal (multiple -arch) builds are unsupported; build and extract one architecture at a time.

See the cross-compilation example.

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

rllvm-cc --target=bpf -O2 -g -c prog.c -o prog.o
rllvm-get-bc prog.o -o prog.bc

WebAssembly linking needs a matching wasm-ld from LLD; see the WebAssembly example. eBPF works without special handling: libbpf skips rllvm’s section on load and preserves it through linking. That linker requires BTF, so compile with -g; see the eBPF example.

Extracting bitcode

Merge strategies

rllvm-get-bc app                                # one whole-program .bc
rllvm-get-bc --merge-strategy archive libfoo.a  # writes libfoo.bca
rllvm-get-bc --merge-strategy partial app       # merge by directory, then combine
rllvm-get-bc -m app                             # also write app.bc.manifest

rllvm-get-bc accepts executables, objects and archives, and -o chooses the output path. The default links every module into one .bc; -b is shorthand for archive mode. Archive outputs are rewritten from the current modules, so removed members do not persist.

See the merge strategies example.

Catalogs

A catalog is a JSON record of which modules were found and where they came from. Use one to select a subset, or to feed queries:

rllvm-info app --json > catalog.json
rllvm-get-bc app --module MODULE_ID --output-dir analysis/selected
rllvm-get-bc analysis/selected/catalog.json -o selected.bc

--module, --source and --configuration are repeatable: alternatives within one option combine, different options intersect, and an unmatched selector fails. --output-dir must be new, and copies hash-checked modules into it with relative paths so the directory can move.

A catalog describes the evidence it collected and the scope it selected — not proven whole-program completeness. See the format reference and the catalogs example.

Moving a build tree

Recorded paths are absolute by default. Record them relative to a root instead, then supply that root’s new location:

RLLVM_BITCODE_ROOT="$PWD/build" cmake --build build
# after moving build/ to moved-build/:
rllvm-get-bc --bitcode-root moved-build moved-build/my_program

The root must contain the bitcode files, including a central bitcode_store_path if you use one. A root that cannot contain the bitcode is reported rather than silently ignored. See the relocatable example.

Analyzing bitcode

Queries

rllvm-query answers source-level questions about captured bitcode. It links LLVM statically, so it ships as its own crate and formula:

brew install h1994st/tap/rllvm-query   # brings its own llvm
# Or: cargo install rllvm-query
# If llvm-config is not on PATH: LLVM_SYS_231_PREFIX="$(brew --prefix llvm)"

On Ubuntu/Debian, install libpolly-N-dev alongside llvm-N-dev and libclang-N-dev.

rllvm-query --catalog catalog.json defs parse_frame            # where it is defined
rllvm-query --catalog catalog.json at parser.c 4               # what is at a source line
rllvm-query --catalog catalog.json callers parse_frame         # who calls it
rllvm-query --catalog catalog.json callees main                # what it calls
rllvm-query --catalog catalog.json uses parse_frame            # where its address is taken
rllvm-query --catalog catalog.json reach main parse_frame      # a path between two functions
rllvm-query --catalog catalog.json slice main parse_frame      # every function on such a path
rllvm-query --catalog catalog.json closure parse_frame in      # everything that reaches it
rllvm-query --catalog catalog.json externals                   # unbound symbols
rllvm-query --catalog catalog.json ffi-exports                 # Rust functions C can call
rllvm-query --catalog catalog.json indirect-targets parser.c:8 # targets of an indirect call
rllvm-query --catalog catalog.json resolution-candidates       # indirect calls grouped by field, with candidates

An indirect call site, and an address stored or placed in a table, names the record field it goes through (via ops@8 (on_event), into ops@8) when the IR proves one; the member name needs -g.

Answers print as text. Add --json for the full machine-readable envelope, or --full to print the scope, analysis, uncertainty and provenance blocks as text too. The MCP server always speaks JSON.

Each invocation loads the whole catalog. To ask many questions, pipe them in, one per line, and the catalog loads once:

printf '%s\n' 'callees main' 'callers parse_frame' \
  | rllvm-query --catalog catalog.json

Each answer follows a == <query> line; with --json, each is one line of JSON. Every line is checked before the catalog is read.

Each answer’s analysis.cache (and --full) reports hits, misses and disk use. RLLVM_QUERY_CACHE=0 turns it off.

rllvm-query cache               # the facts cache's location and disk use
rllvm-query cache clear         # delete every cached entry
rllvm-query cache clear --stale # delete only generations this binary no longer reads

An answer whose facts cache is over query_cache_warn_mb adds a footer note naming rllvm-query cache clear.

Every answer carries the results plus what it could not see: which modules failed to parse, which call sites are indirect, which symbols bind ambiguously, and whether each location’s source has changed since it was compiled. See the queries example and the staleness example.

A symbol can be named three ways — the mangled symbol, its demangled reading, or a bare identifier that searches:

rllvm-query --catalog catalog.json defs _Z5twiceIiET_S0_
rllvm-query --catalog catalog.json defs 'int twice<int>(int)'
rllvm-query --catalog catalog.json defs twice

See the name resolution example.

Completing the call graph

An agent that resolves an indirect call (from resolution-candidates, say) records it in an overlay: a JSON-lines file beside the catalog, <catalog stem>.overlay.jsonl unless --overlay PATH names another. The catalog itself is never changed.

echo '{"op":"add","via_field":{"record":"ops","offset":8},"to":"h3","confidence":"high","provenance":["init: o->on_event = h3"]}' \
  | rllvm-query --catalog catalog.json overlay record  # validate and append, all or none
rllvm-query --catalog catalog.json overlay list        # edges, verdicts, sites covered
rllvm-query --catalog catalog.json overlay compact     # rewrite as the current edges

An add names a field (via_field, every unresolved site through it) or one site, and attaches only where LLVM left the call unbounded. verify and retract name an edge’s key. The file is bound to one build: after a rebuild it refuses to load, naming both fingerprints, as it does when written by an rllvm-query that numbers call sites differently, and a write refuses if another writer changed the file since it was read. To start again after a refusal, move or delete the file, or name another with --overlay (MCP: path). Overlay edges are hypotheses, never proof, and no answer uses them unless asked:

rllvm-query --catalog catalog.json reach main handler --include-overlay
rllvm-query --catalog catalog.json closure handler in --include-overlay --min-confidence medium
rllvm-query --catalog catalog.json slice main handler --include-overlay --emit-module slice.bc

--include-overlay makes reach, closure and slice also walk overlay edges, never refuted ones, and --min-confidence low|medium|high (default low) drops weaker ones. Each step through one is labeled agent with the edge’s provenance, a path or slice using one ends not proven, and closure lists what only agent edges reach apart. An overlay file that cannot be read is an error, never a direct-only answer, as is a missing file that --overlay names. To check an edge in scope, slice --emit-module writes the slice’s definitions as one small module, cut out with the llvm-extract and llvm-nm in the configured llvm_bindir and joined with the configured llvm-link; an alias comes with the function it stands for, and an empty slice writes nothing. See the call-graph overlay example.

MCP server

rllvm-query mcp

Serves the same queries as JSON-RPC 2.0 tools over stdio. Point a client at it:

{
  "mcpServers": {
    "rllvm": {
      "command": "rllvm-query",
      "args": ["mcp"]
    }
  }
}

The client chooses what to analyze with load_catalog (a catalog JSON) or inventory (a binary, archive or .bc), and can keep several loaded at once. Each is analyzed once and answers from memory after that.

Four tools keep the call-graph overlay over MCP. record_edges takes the records overlay record reads, all or none, attaching the overlay beside the catalog first if none is; reach, closure and slice walk them at once with include_overlay (and min_confidence), and slice’s emit_module needs a catalog from load_catalog. Only save_overlay writes, appending to the file. load_overlay attaches the default file or path (required after inventory) and refuses to drop unsaved records unless discard_pending is set; list_overlay shows the edges, saved or not. unload_catalog reports any unsaved records it dropped.

See the MCP example. In Claude Code, the plugin configures this server for you.

Configuration

The TOML file lives at $RLLVM_CONFIG or ~/.rllvm/config.toml. rllvm-init --dry-run previews detection; --llvm-prefix chooses a toolchain and -o selects the file to write.

Configuration keys
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
llvm_bindir No Absolute path to the directory holding the LLVM tools without a key of their own (llvm-nm, llvm-dis, llvm-extract); rllvm-init records it (default: llvm-config --bindir)
rustc_filepath No Compiler for direct Rust invocation; RLLVM_REAL_RUSTC overrides; defaults to rustc on PATH
bitcode_store_path No Directory for bitcode files (must be absolute; created if missing)
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 extra C/C++ bitcode work (default: false)
cache_enabled No Reuse C/C++ bitcode across rebuilds; overridden by RLLVM_CACHE (default: false)
cache_dir No Cache directory; also holds rllvm-query’s query-facts/ (default: ~/.rllvm/cache)
query_cache No Reuse rllvm-query’s extracted per-module facts; overridden by RLLVM_QUERY_CACHE (default: true)
query_cache_warn_mb No Facts cache size, in MB, past which rllvm-query warns (default: 1024)
log_level No 0=error (default), 1=warn, 2=info, 3=debug, 4+=trace; RLLVM_LOG_LEVEL overrides

Benchmarks

Clean build-only time relative to native compilation, on an Apple M4 with LLVM 23.1.2:

Workload Cache disabled Primed C/C++ cache
nghttp2 C (CMake) 1.86× 1.65×
nghttp2 C++ (CMake) 1.88× 1.28×
Quiche (Cargo) 1.28× 1.23×

C/C++ capture adds a bitcode compilation per source; Rust emits bitcode in the same rustc invocation. Unchanged rebuilds stay near native time, but extraction repeats its merge every run.

See the baseline for conditions and the benchmark guide for reproduction.

wllvm and gllvm built the wrapper-and-extract approach this tool follows.

rules_rllvm is a separate Bazel-native project and does not use these binaries.

License

Apache-2.0