Quando si continua a lavorare a lungo su un progetto C o C++, l’estensione e quindi complessità del codice può crescere notevolmente.
Avere a disposizione un diagramma UML aggiornato può essere di grande aiuto per comprendere la struttura del codice e le relazioni tra le varie classi e componenti, soprattutto per chi vuole avere una visione d’insieme senza dover leggere ogni singolo file sorgente.
Doxygen è uno strumento molto popolare per generare documentazione a partire dal codice sorgente, e supporta anche la generazione di diagrammi UML.
Tuttavia, per i miei scopi, volevo qualcosa di più comprensivo e personalizzabile, possibilmente facile da esportare in Mermaid per poter integrare i diagrammi direttamente nella documentazione scritta in Markdown.
Così mi sono imbattuto in clang-uml, uno strumento che utilizza il front-end di Clang per analizzare il codice C/C++ e generare diagrammi UML in vari formati, inclusi plantuml e GraphML, oltre che Mermaid.
Ottenere il compilation database
Per poter utilizzare clang-uml, è necessario fornire un compilation database in formato JSON.
Si tratta di uno strumento molto affascinante, che consente di descrivere il processo di compilazione di un progetto C/C++ in modo strutturato, e che quindi può essere analizzato da altri strumenti come clang-uml.
Vi sono diversi modi per ottenere questo file, incluso l’utilizzo di CMake con l’opzione CMAKE_EXPORT_COMPILE_COMMANDS, o strumenti come Bear.
Purtroppo, Bazel non supporta questa funzione nativamente.
Tuttavia, esistono diversi progetti open source in grado di metterci una pezza.
Fra questi, devo citare bazel-compile-commands-extractor, che promette di essere una soluzione semplice e ben integrata, che tuttavia non sono riuscito ad utilizzare, probabilmente a causa della customizzazione del mio progetto, che immagino utilizzi funzionalità non proprio standard.
D’altra parte, mi sono trovato bene con bazel-compile-commands.
Si tratta di un eseguibile standalone, il che lo rende ancora più semplice da utilizzare senza andare a toccare il build system in alcun modo.
È sufficiente scaricare il binario corretto dalla pagina delle release e eseguirlo con le flag da passare a bazel e il target (o i target) per cui si vuole generare il database:
bazel-compile-commands -b "--flag1" -b "--flag2" //my/project:target
Configurare clang-uml
Il primo passo è installare clang-uml.
Su sistemi basati su Debian/Ubuntu, è possibile installarlo utilizzando apt:
sudo add-apt-repository ppa:bkryza/clang-uml
sudo apt update
sudo apt install clang-uml
Per altri sistemi operativi, è possibile seguire le istruzioni fornite nella documentazione, a cui farò spesso riferimento nel proseguo di questa guida.
clang-uml può essere configurato tramite .clang-uml.
Ci sono tantissime opzioni disponibili che vale la pena esplorare per adattare la generazione dei diagrammi alle proprie esigenze.
# .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
Per semplificarmi la vita in futuro, ho creato un Dockerfile che automatizza l’intero processo.
In un ambiente controllato, vengono installati tutti i tool e viene generato il diagramma UML a partire dal codice sorgente.
Sto usando lucid come esempio, ma ovviamente basta adattare i build target e opzioni alle proprie esigenze.
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" ]
Quando si vuole ottenere il diagramma UML aggiornato, basta eseguire il container Docker creato con questo Dockerfile.
# Build l'immagine Docker
docker build -t bazel-clang-uml .
# Crea il container
id=$(docker create bazel-clang-uml)
# Copia il file generato fuori dal container
docker cp $id:/lucid_class_diagram.mmd ./diagram.mmd
# Rimuovi il container
docker rm $id