When working on a C or C++ project for a long time, the size and complexity of the code can grow significantly.
Having an up-to-date UML diagram can be extremely helpful to understand the structure of the code and the relationships between various classes and components, especially for those who want an overview without reading every single source file.
Doxygen is a very popular tool for generating documentation from source code, and it also supports UML diagram generation.
However, for my purposes, I wanted something more comprehensive and customizable, preferably easy to export to Mermaid so I could integrate the diagrams directly into Markdown documentation.
That’s how I found clang-uml, a tool that uses the Clang front-end to analyze C/C++ code and generate UML diagrams in various formats, including plantuml and GraphML alongside Mermaid.
Getting the compilation database
To use clang-uml, you need to provide a compilation database in JSON format.
This is a very interesting standard that describes the compilation process of a C/C++ project in a structured way, so it can then be analyzed by other tools like clang-uml.
There are several ways to obtain this file, including using CMake with the CMAKE_EXPORT_COMPILE_COMMANDS option, or tools like Bear.
Unfortunately, Bazel does not support this feature natively.
However, there are several open source projects that can help.
Among these, I have to mention bazel-compile-commands-extractor, which promises to be a simple and well-integrated solution, but I couldn’t get it to work, probably due to the non-exactly-standard features I use in my project.
On the other hand, I had good results with bazel-compile-commands.
It’s a standalone executable, which makes it even easier to use without touching the build system at all.
You simply download the correct binary from the releases page and run it with the bazel options and target(s) you want to generate the compilation database for:
bazel-compile-commands -b "--flag1" -b "--flag2" //my/project:target
Configuring clang-uml
The first step is to install clang-uml.
On Debian/Ubuntu-based systems, you can install it using apt:
sudo add-apt-repository ppa:bkryza/clang-uml
sudo apt update
sudo apt install clang-uml
For other operating systems, you can follow the instructions provided in the documentation, which I will often refer to throughout this guide.
clang-uml can be configured via .clang-uml.
There are many available options worth exploring to adapt diagram generation to your needs.
# .clang-uml
compilation_database_dir: .
output_directory: docs/diagrams
remove_compile_flags:
- -fno-canonical-system-headers
- -Wlogical-op
diagrams:
lucid_class_diagram:
type: class
generate_method_arguments: abbreviated
generate_concept_requirements: false
glob:
- "lucid/model/*.cpp"
- "lucid/verification/*.cpp"
using_namespace:
- lucid
- std
include:
namespaces:
- lucid
exclude:
namespaces:
- lucid::detail
- lucid::internal
- lucid::exception
- lucid::log
- lucid::random
access:
- private
- protected
element_types:
- concept
elements:
- lucid::StatsTag
- lucid::Null
subclasses:
- lucid::BaseScopedValue
specializations:
- lucid::BaseScopedValue<T, Tags...>
- lucid::BaseScopedValue<T, Tags>
- lucid::BaseScopedValue<T,Tags>
- lucid::BaseScopedValue<T,Tags...>
Dockerfile
To make my life easier in the future, I created a Dockerfile that automates the entire process.
In a controlled environment, all the tools are installed and the UML diagram is generated from the source code.
I’m using lucid as an example, but of course you just need to adapt the build targets and options to your needs.
FROM ubuntu:24.04 AS build
# Initial setup
RUN apt-get update && \
export DEBIAN_FRONTEND=noninteractive && \
apt-get install -y --no-install-recommends curl software-properties-common gpg-agent && \
apt-get autoremove -y && \
apt-get clean -y
# Install bazel
ARG BAZELISK_VERSION=v1.26.0
ARG BAZELISK_DOWNLOAD_SHA=6539c12842ad76966f3d493e8f80d67caa84ec4a000e220d5459833c967c12bc
RUN curl -fSsL -o /usr/local/bin/bazel https://github.com/bazelbuild/bazelisk/releases/download/${BAZELISK_VERSION}/bazelisk-linux-amd64 \
&& ([ "${BAZELISK_DOWNLOAD_SHA}" = "dev-mode" ] || echo "${BAZELISK_DOWNLOAD_SHA} /usr/local/bin/bazel" | sha256sum --check --status - ) \
&& chmod 0755 /usr/local/bin/bazel
# Install required packages
ARG APT_PACKAGES="git build-essential npm nodejs unzip"
RUN export DEBIAN_FRONTEND=noninteractive && \
apt-get install -y --no-install-recommends ${APT_PACKAGES} && \
apt-get autoremove -y && \
apt-get clean -y
RUN npm install -g @mermaid-js/mermaid-cli
RUN add-apt-repository ppa:bkryza/clang-uml -y && \
apt-get update && \
apt-get install -y clang-uml
WORKDIR /tools
RUN curl -fSsL -o bazel-compile-commands.zip \
https://github.com/kiron1/bazel-compile-commands/releases/download/v0.20.1/bazel-compile-commands_0.20.1-linux_amd64.zip \
-o bazel-compile-commands.zip && \
unzip bazel-compile-commands.zip && \
rm bazel-compile-commands.zip && \
mv /tools/usr/bin/bazel-compile-commands /usr/local/bin/bazel-compile-commands && \
chmod 0755 /usr/local/bin/bazel-compile-commands && \
mv /tools/usr/bin/bazel-clangd-wrapper /usr/local/bin/bazel-clangd-wrapper && \
chmod 0755 /usr/local/bin/bazel-clangd-wrapper
WORKDIR /app
COPY . .
RUN bazel build "--enable_alglib_build=False" \
"--enable_gurobi_build=False" \
"--enable_highs_build=False" \
"--enable_soplex_build=False" \
"--enable_matplotlib_build" //lucid/...
RUN bazel-compile-commands -b "--enable_alglib_build=False" \
-b "--enable_gurobi_build=False" \
-b "--enable_highs_build=False" \
-b "--enable_soplex_build=False" \
-b "--enable_matplotlib_build=False" //lucid/...
RUN clang-uml -g mermaid
RUN printf '\n\
import re \n\
target = "docs/diagrams/lucid_class_diagram.mmd" \n\
with open(target, "r", encoding="utf-8") as f: \n\
content = f.read() \n\
matches = re.findall(r"class C_(\\d+)", content) \n\
unique_classes = set(matches) \n\
for i, c in enumerate(unique_classes): \n\
content = content.replace(f"C_{c}", f"C_{i}") \n\
content = content.replace("[const] ", "").replace("std::", "").replace("$", "") \n\
lines = content.splitlines() \n\
filtered_lines = tuple( \n\
filter( \n\
lambda line: "[default" not in line \n\
and "to_string" not in line \n\
and "clone" not in line \n\
and "+operator" not in line, \n\
lines, \n\
) \n\
) \n\
content = "\\n".join(filtered_lines) \n\
with open(target.replace(".mmd", "_updated.mmd"), "w", encoding="utf-8") as f: \n\
f.write(content) \n\
' > extract.py
RUN python3 extract.py
ENTRYPOINT [ "/bin/bash" ]
FROM alpine:latest AS final
COPY --from=build /app/docs/diagrams/lucid_class_diagram_updated.mmd /lucid_class_diagram.mmd
ENTRYPOINT [ "/bin/sh" ]
When you want to get the updated UML diagram, just run the Docker container created with this Dockerfile.
# Build the Docker image
docker build -t bazel-clang-uml .
# Create the container
id=$(docker create bazel-clang-uml)
# Copy the generated file out of the container
docker cp $id:/lucid_class_diagram.mmd ./diagram.mmd
# Remove the container
docker rm $id