Skip to content

Commit e21490a

Browse files
committed
[Rust] Add FlowGraphLayout and the accompanying APIs for custom flow graph layouts
1 parent 4e2d293 commit e21490a

File tree

12 files changed

+587
-7
lines changed

12 files changed

+587
-7
lines changed

Cargo.lock

Lines changed: 48 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ members = [
99
"arch/riscv",
1010
"arch/msp430",
1111
"rust/plugin_examples/data_renderer",
12+
"rust/plugin_examples/flowgraph_layout",
1213
"view/minidump",
1314
"plugins/dwarf/dwarf_import",
1415
"plugins/dwarf/dwarf_import/demo",

rust/examples/flowgraph.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use binaryninja::{
1212
disassembly::{DisassemblyTextLine, InstructionTextToken, InstructionTextTokenKind},
1313
flowgraph::{EdgePenStyle, FlowGraph, ThemeColor},
1414
};
15+
use std::time::Duration;
1516

1617
pub struct GraphPrinter;
1718

@@ -131,8 +132,9 @@ fn main() {
131132
test_graph();
132133

133134
for func in bv.functions().iter().take(5) {
134-
// TODO: Why are the nodes empty? Python its empty until its shown...
135135
let graph = func.create_graph(FunctionViewType::MediumLevelIL, None);
136+
// It is important to call this, otherwise no nodes will be placed.
137+
graph.request_layout_and_wait(Duration::from_secs(5));
136138
let func_name = func.symbol().short_name();
137139
let title = func_name.to_string_lossy();
138140
bv.show_graph_report(&title, &graph);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "example_flowgraph_layout"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["cdylib"]
8+
9+
[dependencies]
10+
binaryninjacore-sys = { path = "../../binaryninjacore-sys" }
11+
binaryninja = { path = "../.." }
12+
rust-sugiyama = "0.4.0"
13+
petgraph = "0.8"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# FlowGraph Layout Example
2+
3+
This example implements a simple flow graph layout using the rust crate `rust-sugiyama`. After building and placing this
4+
in the plugin directory, override the default flow graph layout setting `rendering.graph.defaultLayout`.
5+
6+
This example is complete _except_ for edge routing, we simply draw the edge from the outgoing nodes bottom to the
7+
incoming nodes top, a real layout would route the edges around nodes to avoid overlaps and other rendering oddities.
8+
9+
## Building
10+
11+
```sh
12+
# Build from the root directory (binaryninja-api)
13+
cargo build -p example_flowgraph_layout
14+
# Link binary on macOS
15+
ln -sf $PWD/target/debug/libexample_data_renderer.dylib ~/Library/Application\ Support/Binary\ Ninja/plugins
16+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
fn main() {
2+
let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH")
3+
.expect("DEP_BINARYNINJACORE_PATH not specified");
4+
5+
println!("cargo::rustc-link-lib=dylib=binaryninjacore");
6+
println!("cargo::rustc-link-search={}", link_path.to_str().unwrap());
7+
8+
#[cfg(target_os = "linux")]
9+
{
10+
println!(
11+
"cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}",
12+
link_path.to_string_lossy()
13+
);
14+
}
15+
16+
#[cfg(target_os = "macos")]
17+
{
18+
let crate_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME not set");
19+
let lib_name = crate_name.replace('-', "_");
20+
println!(
21+
"cargo::rustc-link-arg=-Wl,-install_name,@rpath/lib{}.dylib",
22+
lib_name
23+
);
24+
}
25+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
use binaryninja::flowgraph::edge::Point;
2+
use binaryninja::flowgraph::layout::{register_flowgraph_layout, FlowGraphLayout};
3+
use binaryninja::flowgraph::{FlowGraph, FlowGraphNode};
4+
use binaryninja::rc::Ref;
5+
use std::collections::HashMap;
6+
7+
pub struct StableGraphBuilder;
8+
9+
impl StableGraphBuilder {
10+
pub fn new() -> Self {
11+
Self {}
12+
}
13+
14+
pub fn build(
15+
self,
16+
nodes: &[FlowGraphNode],
17+
) -> petgraph::stable_graph::StableDiGraph<Ref<FlowGraphNode>, ()> {
18+
let mut graph = petgraph::stable_graph::StableDiGraph::<Ref<FlowGraphNode>, ()>::new();
19+
let mut node_idx_map = HashMap::<Ref<FlowGraphNode>, petgraph::graph::NodeIndex>::new();
20+
for node in nodes {
21+
let owned_node = node.to_owned();
22+
node_idx_map.insert(owned_node.clone(), graph.add_node(owned_node));
23+
}
24+
for node in nodes {
25+
let node_idx = node_idx_map.get(node).unwrap();
26+
for edge in &node.outgoing_edges() {
27+
let target_node_idx = node_idx_map.get(&edge.target).unwrap();
28+
graph.add_edge(*node_idx, *target_node_idx, ());
29+
}
30+
}
31+
graph
32+
}
33+
}
34+
35+
struct SugiyamaLayout;
36+
37+
impl FlowGraphLayout for SugiyamaLayout {
38+
fn layout(&self, graph: &FlowGraph, nodes: &[FlowGraphNode]) -> bool {
39+
let mut config = rust_sugiyama::configure::Config::default();
40+
config.vertex_spacing = 5.0;
41+
42+
let vertex_size = |_, node: &Ref<FlowGraphNode>| {
43+
let (width, height) = node.size();
44+
(width as f64 * 1.2, height as f64)
45+
};
46+
let pet_graph = StableGraphBuilder::new().build(nodes);
47+
let layouts = rust_sugiyama::from_graph(&pet_graph, &vertex_size, &config);
48+
49+
// Position graph nodes
50+
for (nodes, _, _) in &layouts {
51+
for (node_idx, (x, y)) in nodes {
52+
let node = pet_graph.node_weight(*node_idx).unwrap();
53+
node.set_position(*x as i32, *y as i32);
54+
}
55+
}
56+
57+
// Add edges to graph nodes
58+
for (nodes, _, _) in &layouts {
59+
for (node_idx, (x, y)) in nodes {
60+
let node = pet_graph.node_weight(*node_idx).unwrap();
61+
let (width, height) = node.size();
62+
for (edge_idx, edge) in node.outgoing_edges().iter().enumerate() {
63+
let from_point_x = x + (width as f64 / 2.0);
64+
let from_point_y = y + height as f64;
65+
let from_point = Point {
66+
x: from_point_x as f32,
67+
y: from_point_y as f32,
68+
};
69+
let (target_node_x, target_node_y) = edge.target.position();
70+
let (target_node_width, _) = edge.target.size();
71+
let to_point_x = target_node_x as f64 + (target_node_width as f64 / 2.0);
72+
let to_point_y = target_node_y;
73+
let to_point = Point {
74+
x: to_point_x as f32,
75+
y: to_point_y as f32,
76+
};
77+
// NOTE: This does not do proper routing, this will add edge points from the outgoing node
78+
// to the target node, this will lead to lines overlapping nodes and other rendering oddities.
79+
// The reason we do not do proper routing is because that is quite a bit more code with some
80+
// dependence on a navigation algorithm like a-star.
81+
node.set_outgoing_edge_points(edge_idx, &[from_point, to_point]);
82+
}
83+
}
84+
}
85+
86+
// Calculate graph size and node visibility
87+
let mut min_x = f32::MAX;
88+
let mut min_y = f32::MAX;
89+
let mut max_x = f32::MIN;
90+
let mut max_y = f32::MIN;
91+
92+
for node in nodes {
93+
let (node_x, node_y) = node.position();
94+
let (node_width, node_height) = node.size();
95+
96+
// Initialize per-node bounds based on the node's current box
97+
let mut min_node_x = node_x;
98+
let mut max_node_x = node_x + node_width;
99+
let mut min_node_y = node_y;
100+
let mut max_node_y = node_y + node_height;
101+
102+
for edge in &node.outgoing_edges() {
103+
for point in &edge.points {
104+
let px = point.x;
105+
let py = point.y;
106+
107+
// Update Global Graph Bounds
108+
min_x = min_x.min(px);
109+
min_y = min_y.min(py);
110+
max_x = max_x.max(px + 1.0);
111+
max_y = max_y.max(py + 1.0);
112+
113+
// Update Node Visibility Bounds
114+
min_node_x = min_node_x.min(px as i32);
115+
max_node_x = max_node_x.max(px as i32 + 1);
116+
min_node_y = min_node_y.min(py as i32);
117+
max_node_y = max_node_y.max(py as i32 + 1);
118+
}
119+
}
120+
121+
node.set_visibility_region(
122+
min_node_x,
123+
min_node_y,
124+
(max_node_x - min_node_x),
125+
(max_node_y - min_node_y),
126+
);
127+
}
128+
129+
// Set final graph dimensions
130+
if min_x != f32::MAX {
131+
let (horiz_node_margin, vert_node_margin) = graph.node_margins();
132+
let final_graph_width = (max_x - min_x) as i32 + horiz_node_margin * 2;
133+
let final_graph_height = (max_y - min_y) as i32 + vert_node_margin * 2;
134+
graph.set_size(final_graph_width, final_graph_height);
135+
}
136+
137+
true
138+
}
139+
}
140+
141+
/// # Safety
142+
/// This function is called from Binary Ninja once to initialize the plugin.
143+
#[allow(non_snake_case)]
144+
#[unsafe(no_mangle)]
145+
pub unsafe extern "C" fn CorePluginInit() -> bool {
146+
// Register flow graph layout
147+
register_flowgraph_layout("Sugiyama", SugiyamaLayout);
148+
true
149+
}

0 commit comments

Comments
 (0)