Vai al contenuto

UML in clang

Pubblicato:

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