Synergy of Graphviz and the C/C ++ Preprocessor

Igor Plastov
The Startup
Published in
4 min readAug 9, 2020

This article tells how to make Graphviz graphs drawing easy and faster by using C preprocessor.

The key point is that the dot graph language that Graphviz (graph visualization software) uses is preprocessable in its syntax. This is what the Graphviz developers intended. Thanks to their foresight, when describing graphs, we can use the following features (I am citing Wikipedia from memory):

  • replacing the corresponding digraphs and trigraphs with the equivalent symbols “#” and “\”;
  • removing escaped line feed characters;
  • Replacing inline and block comments with empty lines (removing surrounding spaces and tabs);
  • inserting (including) the content of an arbitrary file (#include);
  • macro substitution (#define);
  • conditional compilation (#if, #ifdef, #elif, #else, #endif).

Now let’s demonstrate the new opportunities in practice. As an example, let’s take the graph from my article about MediaStreamer2, it is in the figure below.

The graph is large enough and if you describe it “head-on” you will need a lot of manual error-free work. If you look closely, you can isolate duplicate elements with a difference only in the content of some fields. This is how the nodes of the graph look like m1, m2, m2_1, m2_2, m3, m4. These are the first candidates for small-scale mechanization using macros. Create a header file for our main dot file. Let’s call it common.dot:

// The common.dot file contains definitions for the main graph file.#define Q(x) #x         // Auxiliary macro for "quoting" a string inside a macro.#define QUOTE(x) Q(x)   // A macro to "quote" a string within another macro.// We define a macro for a conditional image of the internal
// fields of the mblk_t structure.
// The macro will only show some of the structure fields.
#define msg_staff \
<p> *prev \
|<n> *next \
|<c> *cont \
|<d> *data \
| other\nstuff
// We define a macro for the conditional image of
// the mblk_t structure itself.
#define msg(name, ... ) \
name[xlabel=mblk_t label=QUOTE(<h> name | msg_staff) \
];
// We define a macro for a conditional image of
// the dblk_t structure itself.
#define dbuf(name ...) \
name[label=QUOTE(<h> name) xlabel="dblk_t"];
// We define a macro for a conditional image of
// the queue_t structure.
// Some of the fields of this structure coincide with
// the fields of the mblk_t structure,
// therefore, the msg_staff macro is substituted
// into the definition.
#define queue(name, ...) \
name[ xlabel="queue_t" label=QUOTE(<h> name | \
msg_staff)];

Now it’s time to write the main graph file. Let’s call it my_graph.dot:

// file my_graph.dot// Including mocro definitions file.
#include "common.dot"
digraph queue_demo
{
rankdir=LR;
node[shape=Mrecord];
// Using the macros we defined above,
// we create nodes that will symbolize data blocks on the graph.
dbuf(d1);
dbuf(d2);
dbuf(d2_1);
dbuf(d2_2);
dbuf(d3);
dbuf(d4);
// We create nodes that will symbolize messages on the graph,
// to which the data blocks are bound.
msg(m1);
msg(m2);
msg(m2_1);
msg(m2_2);
msg(m3);
msg(m4);
// We create an instance of the control queue structure.
// The node will be named q1.
queue(q1);
// We describe the connections of data blocks
// with message nodes.
m1:d->d1;
m2:d->d2;
m2_1:d->d2_1;
m2_2:d->d2_2;
m3:d->d3;
m4:d->d4;
// We Describe the connections of messages to each other.
m1:n -> m2:h;
m1:p -> q1:h;
m2:n -> m3:h;
m2:c->m2_1:h;
m2_1:c->m2_2:h;
m3:n -> m4:h;
m2:p -> m1:h;
m3:p -> m2:h;
m4:p -> m3:h;
// Describe the connection of messages to the queue structure.
q1:n->m1:h;
q1:p->m4:h;
m4:n -> q1:h[color=blue]; // Color the edge blue.
}

We process the file with a preprocessor:

$ cpp my_graph.dot -o my_graph_res.dot

The result will be placed in the my_graph_res.dot file. As a result of the preprocessor operation, the graph description file will take the form:

# 1 "<built-in>"
# 1 "<command-line>"
# 1 "my_graph.dot"
# 1 "common.dot" 1
# 3 "my_graph.dot" 2
digraph queue_demo
{
rankdir=LR;
node[shape=Mrecord];
d1[label="<h> d1" xlabel="dblk_t"];;
d2[label="<h> d2" xlabel="dblk_t"];;
d2_1[label="<h> d2_1" xlabel="dblk_t"];;
d2_2[label="<h> d2_2" xlabel="dblk_t"];;
d3[label="<h> d3" xlabel="dblk_t"];;
d4[label="<h> d4" xlabel="dblk_t"];;
m1[xlabel=mblk_t label="<h> m1 | <p> *prev |<n> *next |<c> *cont |<d> *data | other\nstuff" ];;
m2[xlabel=mblk_t label="<h> m2 | <p> *prev |<n> *next |<c> *cont |<d> *data | other\nstuff" ];;
m2_1[xlabel=mblk_t label="<h> m2_1 | <p> *prev |<n> *next |<c> *cont |<d> *data | other\nstuff" ];;
m2_2[xlabel=mblk_t label="<h> m2_2 | <p> *prev |<n> *next |<c> *cont |<d> *data | other\nstuff" ];;
m3[xlabel=mblk_t label="<h> m3 | <p> *prev |<n> *next |<c> *cont |<d> *data | other\nstuff" ];;
m4[xlabel=mblk_t label="<h> m4 | <p> *prev |<n> *next |<c> *cont |<d> *data | other\nstuff" ];;
q1[ xlabel="queue_t" label="<h> q1 | <p> *prev |<n> *next |<c> *cont |<d> *data | other\nstuff"];; m1:d->d1;
m2:d->d2;
m2_1:d->d2_1;
m2_2:d->d2_2;
m3:d->d3;
m4:d->d4;
m1:n -> m2:h;
m1:p -> q1:h;
m2:n -> m3:h;
m2:c->m2_1:h;
m2_1:c->m2_2:h;
m3:n -> m4:h;
m2:p -> m1:h;
m3:p -> m2:h;
m4:p -> m3:h;
q1:n->m1:h;
q1:p->m4:h;
m4:n -> q1:h[color=blue];
}

As you can see, all macros are expanded and substituted. The lines are longer and more complex. It remains to transfer the file for rendering to one of the utilities of the Graphviz package (for example dot) or to render on the website: https://sketchviz.com/new

The result will be like this:

The graph differs in the arrangement of nodes from the original at the beginning of the article, due to the fact that for the sake of consistency, I changed the order of declaration of nodes.

If you wish, you can go further and move the connection of queue nodes into the macro.

--

--