Skip to content

Commit fd96f8a

Browse files
committed
add the coreapi function counter script
1 parent 4bf1377 commit fd96f8a

2 files changed

Lines changed: 267 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "count-core-api-use"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
clang = "2.0.0"
8+
pyo3 = { version = "0.28.2", features = ["auto-initialize"] }
9+
serde = { version = "1.0.228", features = ["derive"] }
10+
serde_json = "1.0.149"
11+
syn = { version = "2.0.117", features = ["full", "visit"] }
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
use std::ffi::OsStr;
2+
use std::path::Path;
3+
use std::{collections::HashMap, path::PathBuf};
4+
5+
use clang::{Clang, EntityKind, Index};
6+
use serde::Deserialize;
7+
use syn::ExprCall;
8+
use syn::visit::Visit;
9+
10+
fn main() {
11+
let functions = get_function_coreapi();
12+
13+
let c = count_c(&functions);
14+
let python = count_python(&functions);
15+
let rust = count_rust(&functions);
16+
17+
println!("|rs|py|cpp|Function Name|");
18+
println!("|-|-|-|--------------|");
19+
let print_emoji = |x, e| print!("{}|", if x > 0 { e } else { '🚧' });
20+
for (func_i, func) in functions.iter().enumerate() {
21+
print!("|");
22+
print_emoji(rust[func_i], '🦀');
23+
print_emoji(python[func_i], '🐍');
24+
print_emoji(c[func_i], '🐀');
25+
println!("{func}|");
26+
}
27+
}
28+
29+
fn get_function_coreapi() -> Vec<String> {
30+
let clang = Clang::new().expect("Failed to load libclang");
31+
let index = Index::new(&clang, false, false);
32+
33+
// parse the core api binaryninjacore.h
34+
let core_api = index
35+
.parser("../../binaryninjacore.h")
36+
.arguments(&["-xc++", "-D\"_cplusplus\""])
37+
.parse()
38+
.expect("Failed to parse");
39+
// get the functions
40+
let mut functions: Vec<String> = vec![];
41+
core_api.get_entity().visit_children(|entity, _parent| {
42+
if entity.get_kind() == EntityKind::FunctionDecl {
43+
if let Some(name) = entity.get_name() {
44+
functions.push(name.clone());
45+
}
46+
}
47+
clang::EntityVisitResult::Recurse
48+
});
49+
functions
50+
}
51+
52+
fn count_c(functions_order: &[String]) -> Vec<usize> {
53+
let build_dir = "../../build/compile_commands.json";
54+
55+
let json = std::fs::read_to_string(&build_dir).expect(
56+
r#"Failed to load compile_commands.json, please compile binaryninja-api using\n
57+
`cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build -S .`"#,
58+
);
59+
60+
#[derive(Debug, Clone, Deserialize)]
61+
struct Command {
62+
#[serde(rename = "directory")]
63+
_directory: String,
64+
command: String,
65+
file: String,
66+
#[serde(rename = "output")]
67+
_output: String,
68+
}
69+
let commands: Vec<(String, Vec<String>)> = serde_json::from_str::<Vec<Command>>(&json)
70+
.expect("Unexpected `compile_commands.json` contents")
71+
.into_iter()
72+
.map(|x| {
73+
let mut args = vec![];
74+
let mut command_iter = x.command.split_whitespace();
75+
if let Some(command_raw) = command_iter.next() {
76+
let command = Path::new(command_raw).file_name();
77+
match command.and_then(OsStr::to_str) {
78+
Some("g++" | "clang++") => args.push("-xc++".into()),
79+
Some("gcc" | "clang") => args.push("-xc".into()),
80+
_ => {}
81+
}
82+
}
83+
84+
loop {
85+
let Some(arg) = command_iter.next() else {
86+
break;
87+
};
88+
match arg {
89+
// remove the filename from the arguments
90+
file if file == x.file => {}
91+
// remove the compile flag
92+
"-c" => {}
93+
// remove the output file name
94+
"-o" => {
95+
let _filename = command_iter.next();
96+
}
97+
// add the other args
98+
_ => args.push(arg.into()),
99+
}
100+
}
101+
102+
(x.file, args)
103+
})
104+
.collect();
105+
106+
let mut functions: HashMap<String, usize> =
107+
functions_order.iter().map(|x| (x.to_string(), 0)).collect();
108+
109+
let clang = Clang::new().expect("Failed to load libclang");
110+
let index = Index::new(&clang, false, false);
111+
112+
// Iterate over all compile commands
113+
for (filename, args) in commands {
114+
// parse the file
115+
let parsed = index
116+
.parser(&filename)
117+
.arguments(&args)
118+
.parse()
119+
.expect("Failed to parse");
120+
121+
// check if the file use functions from coreapi
122+
parsed.get_entity().visit_children(|entity, _parent| {
123+
// only function calls, resolved or not
124+
if matches!(
125+
entity.get_kind(),
126+
EntityKind::CallExpr | EntityKind::OverloadedDeclRef
127+
) {
128+
if let Some(name) = entity.get_name() {
129+
functions.entry(name).and_modify(|x| *x += 1);
130+
}
131+
}
132+
clang::EntityVisitResult::Recurse
133+
});
134+
}
135+
136+
function_in_order(functions_order, functions)
137+
}
138+
139+
// TODO parse the python coreapi, don't reuse the c one
140+
fn count_python(functions_order: &[String]) -> Vec<usize> {
141+
let mut functions: HashMap<String, usize> =
142+
functions_order.iter().map(|x| (x.to_string(), 0)).collect();
143+
144+
// check all file inside python, get all .py files
145+
let files = get_all_files(Path::new("../../python"), "py");
146+
147+
use pyo3::prelude::*;
148+
Python::attach(|py| -> PyResult<()> {
149+
let ast = PyModule::import(py, "ast")?;
150+
let ast_call = ast.getattr("Call")?;
151+
let ast_name = ast.getattr("Name")?;
152+
let ast_attr = ast.getattr("Attribute")?;
153+
let isinstance = py.eval(c"isinstance", None, None)?;
154+
for file in files {
155+
let data = std::fs::read(file).expect("Unable to read python file");
156+
let tree = ast.call_method1("parse", (data,))?;
157+
let walk = ast.call_method1("walk", (&tree,))?;
158+
for node in walk.try_iter()? {
159+
let node = node?;
160+
let is_call: bool = isinstance.call((&node, &ast_call), None)?.extract()?;
161+
if is_call {
162+
let func = node.getattr("func")?;
163+
let func_name = if isinstance
164+
.call((&func, &ast_name), None)?
165+
.extract::<bool>()?
166+
{
167+
Some(func.getattr("id")?.extract::<String>()?)
168+
} else if isinstance
169+
.call((&func, &ast_attr), None)?
170+
.extract::<bool>()?
171+
{
172+
Some(func.getattr("attr")?.extract::<String>()?)
173+
} else {
174+
None
175+
};
176+
177+
if let Some(func_name) = func_name {
178+
functions.entry(func_name).and_modify(|x| *x += 1);
179+
}
180+
}
181+
}
182+
}
183+
Ok(())
184+
})
185+
.expect("Unable to execute python");
186+
187+
function_in_order(functions_order, functions)
188+
}
189+
190+
fn count_rust(functions_order: &[String]) -> Vec<usize> {
191+
// visitor logic
192+
struct CallCounter {
193+
functions: HashMap<String, usize>,
194+
}
195+
impl<'ast> Visit<'ast> for CallCounter {
196+
fn visit_expr_call(&mut self, node: &'ast ExprCall) {
197+
if let syn::Expr::Path(ref path) = *node.func {
198+
if let Some(ident) = path.path.get_ident() {
199+
self.functions
200+
.entry(ident.to_string())
201+
.and_modify(|x| *x += 1);
202+
}
203+
}
204+
syn::visit::visit_expr_call(self, node);
205+
}
206+
}
207+
let mut counter = CallCounter {
208+
functions: functions_order.iter().map(|x| (x.to_string(), 0)).collect(),
209+
};
210+
211+
// parse and check all the .rs files
212+
for file in get_all_files(Path::new("../../rust"), "rs") {
213+
let file_content = std::fs::read_to_string(file).expect("Unable to read rust file");
214+
215+
let syn = syn::parse_file(&file_content).expect("Unable to parse rust file");
216+
217+
// visit rust file
218+
counter.visit_file(&syn);
219+
}
220+
221+
function_in_order(functions_order, counter.functions)
222+
}
223+
224+
fn function_in_order(functions_order: &[String], functions: HashMap<String, usize>) -> Vec<usize> {
225+
functions_order
226+
.into_iter()
227+
.map(|func| functions[func.as_str()])
228+
.collect()
229+
}
230+
231+
fn get_all_files(dir: &Path, ext: &str) -> Vec<PathBuf> {
232+
let mut files = vec![];
233+
let mut directories = vec![dir.to_owned()];
234+
loop {
235+
let Some(dir) = directories.pop() else {
236+
break;
237+
};
238+
239+
for entry in std::fs::read_dir(dir).expect("Unable to find the python dir") {
240+
let entry = entry.expect("invalid entry in the python directory");
241+
let ftype = entry
242+
.file_type()
243+
.expect("Unable to identify file type in python directory");
244+
if ftype.is_dir() {
245+
// check the directory after this one
246+
directories.push(entry.path());
247+
} else if ftype.is_file() || ftype.is_symlink() {
248+
// if a python file, check it
249+
if Path::new(&entry.file_name()).extension() == Some(OsStr::new(ext)) {
250+
files.push(entry.path());
251+
}
252+
}
253+
}
254+
}
255+
files
256+
}

0 commit comments

Comments
 (0)