Graphs¶
Understand comes with many built-in graphs such as call trees and control flow graphs. Graphs can also be defined through plugins, like the butterfly graph. Users can export graphs from the Python API.
Discovery¶
Available graphs depend on the target: a Db,
an Ent, or an Arch. For
example, the “Control Flow” graph is typically available for function entities
whereas a “UML Class Diagram” is available for class entities. The “Architecture
Graph” is only available for architectures.
Use Graph.list to discover available graphs.
The Graph objects returned can be used to query
information about the graph such as the name
and variant. Note that both the name
and variant are necessary to uniquely identify a graph.
Graphs support options.
Use Graph.options followed by
Options.list to discover available options.
List catalog graphs for the open project (pass a Db so
availability matches the database):
import understand
db = understand.open("/path/to/myproject.und")
for g in understand.Graph.list(db):
print(g.name(), "-", g.variant())
Generation¶
Call db.draw(),
ent.draw(), or
arch.draw() to generate graphs for projects,
entities, and architectures respectively.
The graph and variant parameters are used to identify the graph to
generate. If graph is an Graph object, both
values are determined from the Graph object. If graph is a string with multiple
variants and no variant is specified, a default variant is chosen.
The draw command can save the graph to disk, return the graph as bytes or
return a read-only GraphContext object.
Use filename to save to disk (returning None) or format to return
the graph. The filename and format parameters are mutually exclusive.
When using filename, the format is determined by the extension.
Supported extensions and format arguments are jpg, png, svg, vsdx, vdx, and
dot. Additionally, format can be "raw" to return a GraphContext instead of
bytes. Note that while all graphs technically have dot output, not all dot files
are meant to be used with Graphviz. Graphs using custom
Understand layout algorithms will have custom attributes in the dot files to
support them.
Graphs can have options. See the understand.Options
class for how to format the options argument. For Relationship graphs use
secondent=EntityUniqueName to indicate the second entity.
If an error occurs, an UnderstandError will be raised. Some possible errors are:
Draw Too Large - failed to allocate memory for the image
Unknown Graph - the graph name did not match a known graph
Unsupported File Type - unsupported file format
Save a graph to disk by passing a filename (extension selects the format):
import understand
db = understand.open("/path/to/myproject.und")
fn = next(db.ents("function ~unresolved ~unknown"))
fn.draw("Called By", f"callby_{fn.name()}.png")
Return a graph in memory with format (mutually exclusive with filename).
Use format="raw" for a read-only GraphContext.
Pass a Graph from Graph.list
so the name and variant match the catalog:
import understand
db = understand.open("/path/to/myproject.und")
g = understand.Graph.list(db)[0]
png = db.draw(g, format="png")
ctx = db.draw(g, format="raw")
Plugin writing¶
A graph plugin must have a name function and a draw function. If the name is used by other graphs, the style function should also be defined to provide the variant name. The draw function creates the graph. At least one test_ function should be defined to indicate when the graph is available.
The parameter graph passed to init and draw is an
GraphContext object. It represents the root of the graph. Use
the cluster, node,
and edge methods to add
clusters, nodes, and
edges to the graph. Each of those objects has a
set method to change the Graphviz attributes. Default attributes can be
set on the root graph context with the default
method. Nodes and edges can also be synced to entities,
references or file locations.
Use options to
access an Options object. The options object can be used
to define and retrieve custom options.
You can also provide a legend for your graph using
legend to obtain a Legend object.
Use define to create entries. Use
set to update existing entries.
The following sample script is a call tree. It can be run as a project level graph which graphs all functions in a database, or as an entity level graph showing the call tree from the initial entity.
import understand
def name():
"""
Required, the name of the graph.
"""
return "Calls"
def style():
"""
Optional, the name as it appears in the graph variants drop down.
This defaults to "Custom"
"""
return "Python Template"
def test_global(db):
"""
Optional, return True if this graph is a project level graph
If True, this graph will appear in the top level Graph Menu.
"""
return True
def test_entity(ent):
"""
Optional, return True if the graph exists for the given entity
If True, this graph will appear in the Graphical Views menu for the
entity.
"""
return ent.kind().check("function ~unknown ~unresolved");
def test_architecture(arch):
"""
Optional, return True if the graph exists for the given architecture
If True, this graph will appear in the Graphical Views menu for the
architecture.
"""
return True;
def init(graph, target):
"""
Initialize the graph
This function is called once on graph creation. Use it to define the
available graph options and/or a legend.
"""
# Define options through the options object
graph.options().define("Fill", ["On","Off"], "Off");
# Use isinstance on the target to see if it's an entity, architecture, or
# project level graph (if your plugin supports multiple). You can use the
# target to customize available options.
if isinstance(target, understand.Ent):
graph.options().define("Depth", ["1", "2", "3"], "3")
# Defining a legend is optional. You can add multiple entries to the legend.
graph.legend().define("func", "roundedrect", "Function", "blue", "#FFFFFF")
def grabNode(graph, nodes, ent):
"""
This is a custom function for this script to get a graphviz node
"""
if ent in nodes:
node = nodes[ent]
else:
# passing an ent to the node object will automatically sync the entity.
node = graph.node(ent.name(),ent)
if ent.kind().check("unresolved"):
# Set graph, node, and edge attributes with the set function.
# See Graphviz documentation for available attributes.
node.set("shape","octagon")
node.set("color","gray")
node.set("fillcolor","white")
nodes[ent] = node
return node
def draw(graph, target):
"""
Draw the graph
The second argument can be a database, architecture, or an entity depending
which test functions return True.
"""
fore = "blue"
back = "#FFFFFF"
# Use options to lookup the current values
if graph.options().lookup("Fill") == "On":
fore = "#000000"
back = "blue"
# Use set to update the legend outside of init
graph.legend().set("func","fore",fore)
graph.legend().set("func","back",back)
# Use set to change graph, node, and edge attributes
graph.set("rankdir", "LR")
# Use default to set the default attributes for graphs, nodes, and edges
graph.default("color", fore, "node")
graph.default("fillcolor", back, "node")
graph.default("style","filled,rounded","node")
graph.default("shape","box","node")
# store the ent->graphviz node so that each entity node appears only
# once no matter how many calls to it there are.
nodes = dict()
# avoid visiting any node more than once
visited = set()
# If the graph can be more than one type, use isinstance to determine the type
depth = 1
curLevel = []
if isinstance(target, understand.Db):
# This sample graphs all functions in the database for a project level
# graph
curLevel = target.ents("function ~unknown ~unresolved")
elif isinstance(target, understand.Arch):
# Graph all functions in the architecture
for ent in target.ents(False):
if ent.kind().check("function ~unknown ~unresolved"):
curLevel.append(ent)
elif ent.kind().check("file"):
for ref in ent.refs("define", "function ~unknown ~unresolved", True):
curLevel.append(ref.ent())
# Create clusters with cluster
cluster = graph.cluster(target.name(), target)
# add nodes to the cluster
for ent in curLevel:
grabNode(cluster, nodes, ent)
else:
# For an entity level graph, generate a calls tree
curLevel.append(target)
depth = int(graph.options().lookup("Depth"))
# Loop over the levels of the tree
while depth > 0:
depth -= 1
nextLevel = []
for ent in curLevel:
# avoid visiting nodes multiple times
if ent in visited:
continue
visited.add(ent)
# Get a graphviz node for the entity
tail = grabNode(graph,nodes,ent)
# Add edges for each call
for ref in ent.refs("call",unique=True):
headEnt = ref.ent()
nextLevel.append(headEnt)
head = grabNode(graph,nodes,headEnt)
# create an edge
edge = graph.edge(tail,head)
# Use sync so that clicking on the edge will visit the reference
edge.sync(ref)
curLevel = nextLevel