How to create a Control-Flow Graph of a Rust crate?

I am trying to create a Control-Flow Graph:

a representation using graph notation, of all paths that might be traversed through a program during its execution

The graph is not of a specific run, but rather a control-flow description of the static program.

The expectation is to generate a .dot file and the convert it to .png for visualisation.

What I tried so far

  • Using rustc unstable flags
    The rustc debugging guide shows some interesting flags. I passed them to cargo with RUSTCFLAGS="-Zdump-mir=main -Zdump-mir-dataflow -Zdump-mir-graphviz"; cargo +nightly build.

    No need for the mir-dataflow though, just the control-flow diagram. That command (or a very similar one) ran successfully once, producing the mir-dump/ but subsequent runs —even removing target/— did nothing. Same using cargo clean.
    The expectation is that the MIR would be quite helpful, but HIR or just the source-code's CFG directly would be better.

  • Another try were cargo-callgraph and callgraph.rs cited in this reply to a post. But none of them succeeded installing with cargo install --git URL --locked. I think the docs are probably saying that one should clone the repo, and put the file to test under test/ to build the callgraph. I didn't try that yet.

So, can cargo +nightly currently do this without any package nor plugin ? Any maintained package that you recommend (or insist on the above)?

If you don't pass -Zdump-mir-dataflow it should dump the cfg without dataflow. And it should work more than once.

For HIR and AST building a CFG is non-trivial given that you can nest control flow within expressions. Borrowck used to run on the AST while attempting to produce a CFG. This was just too buggy as it turns out, hence why borrowck now runs on MIR.

cargo-callgraph produces a call graph, not a control-flow graph.

Yes, I've tried that, but it did produce the mir-dump only once.

My tree looks like:

.
├── Cargo.lock
├── Cargo.toml
├── src
├── target

The first time, it added mir-dump/ which I originally deleted (to re-run a different variant of the command and not mix the results up). It did not generate it again (even after using clean or deleting target/)


For the rest, they were mostly alternatives that would at least give some visual information about the program, but indeed, not exactly what I try to get.

I just noticed that you spelled it as RUSTCFLAGS. The correct env var is RUSTFLAGS.

True, but nothing changed (I think I tried with both flags earlier, which is why it worked sometime.):

RUSTFLAGS="-Zdump-mir=main -Zdump-mir-graphviz -Zgraphviz-dark-mode";\
cargo +nightly build
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s

echo $RUSTFLAGS
-Zdump-mir=main -Zdump-mir-graphviz -Zgraphviz-dark-mode

Then list the tree:

tree -L 1
.
├── Cargo.lock
├── Cargo.toml
├── src
├── target
RUSTFLAGS="-Zdump-mir=main -Zdump-mir-graphviz -Zgraphviz-dark-mode";\
cargo +nightly build

This does two things. First it sets the RUSTFLAGS shell variable (not env var!) and then it runs cargo. You need to either remove the ;\ and put both on the same line or use export before RUSTFLAGS to actually set the RUSTFLAGS env var for cargo.

echo $RUSTFLAGS

echo $RUSTFLAGS prints either a shell variable or env var. You can use env | grep RUSTFLAGS to see just the env var named RUSTFLAGS.

I used ;\ only here though, it was to split the command (didn't know it made a difference though).

So this command still gets no dump folder:

RUSTFLAGS="-Zdump-mir=main -Zdump-mir-graphviz -Zgraphviz-dark-mode" cargo +nightly build

I also tried CARGO_ENCODED_RUSTFLAGS, same fate.

PS: Actually that last option isn't sound, because it needs a specific separator between the options, other than a space.

For a multi-line shell command you should be using \ alone at the end of the line — no other characters.

This produced the folder now:

cargo clean && RUSTFLAGS="-Zdump-mir=main -Zdump-mir-graphviz -Zgraphviz-dark-mode" cargo +nightly build

However, there are 2186 files within mir-dump (1400 being .dots).

Unsure how to narrow it down to a few files (one .dot ideally).


PS: I think the command I needed may have been:

cargo clean && RUSTFLAGS="-Zgraphviz-dark-mode -Zunpretty=mir-cfg" cargo +nightly build

If I understand this part correctly.:

The -Z unpretty=mir-cfg flag can be used to create a graphviz MIR control-flow diagram for the whole crate: ...

The .dot is printed to stdout using the command in the reply above, but it's still too large so I wonder whether it can be restricted to my local crate without dependencies (assume that's why it's so large).

-Zunpretty only prints for the local crate already.

It outputs 28 thousand lines for my ~60-lines crate. So I suspected it includes all dependencies.

Maybe the MIR ends up being 1000 lines and the dot file increases that by 28.

I think i'll leave it for now though.