# ghidra
Typed Rust bindings for an embedded Ghidra JVM. Use when you need to drive
Ghidra programmatically from Rust - loading a project, importing programs,
running analysis, decompiling functions, querying the type graph.
This crate stops at Ghidra primitives. Higher-level extraction or schema
mapping belongs in consumer crates.
Status: pre-1.0, API may change.
## Prerequisites
- A Ghidra installation (the [nixpkgs](https://search.nixos.org/packages?query=ghidra)
`ghidra` package works; pin a version you control if reproducibility matters).
- JDK 21 on `PATH` and reachable as `JAVA_HOME`.
- The `GHIDRA_INSTALL_DIR` environment variable pointing at the Ghidra install.
The crate's `build.rs` invokes `javac` and `jar` against the jars under
`$GHIDRA_INSTALL_DIR/Ghidra` to compile the JNI bridge.
The included `flake.nix` provides a dev shell with all of the above wired up.
Run `nix develop` (or use `direnv`).
## Install
```toml
[dependencies]
ghidra = "0.1"
```
## End-to-end example
```rust
use ghidra::{
AnalysisOptions, DecompileOptions, Ghidra, LaunchOptions, ProgramLoadOptions,
};
let ghidra = Ghidra::start(LaunchOptions::default())?;
let project = ghidra.open_or_create_project(".ghidra-projects/example", "example")?;
let loaded = project.open_or_import_program_with_options(
ProgramLoadOptions::new("target/example-binary")?
.with_language("x86:LE:64:default")
.with_compiler("gcc"),
)?;
let program = loaded.program;
program.analyze_with_options(AnalysisOptions::if_needed())?;
let functions = program.functions()?;
let decompiler = program.open_decompiler()?;
let result = decompiler.decompile_with_options(
&functions[0],
DecompileOptions::new(60),
)?;
if let Some(c) = &result.c {
println!("{c}");
}
program.save()?;
# Ok::<_, ghidra::Error>(())
```
`open_or_import_program` is the lower-effort variant: it imports with all
defaults and hands back a bare `Program<'project>` instead of a `LoadedProgram`
(which carries the import report).
## Handle hierarchy
```text
Ghidra - owns GhidraRuntime, the lifetime root
└─ Project - a directory + project name on disk
└─ Program<'project> - borrows from Project
├─ Decompiler<'program> - borrows from Program
└─ ProgramTransaction<'program>
```
Lifetimes are real. A `Program` borrows `&'project Project`. A `Decompiler` and
`ProgramTransaction` borrow `&'program Program`. You cannot return a `Program`
out of a function that owns its `Project`. If you need the program to live
longer than the call site, own the `Ghidra` and `Project` higher up.
`Ghidra::start` is idempotent: calling it twice returns a clone of the same
JVM-backed runtime. The JVM is process-global - you cannot run two independent
Ghidra runtimes in one process.
## `Program` API by task
`Program` exposes ~40 methods. See the [rustdoc](https://docs.rs/ghidra) for
the full list. Grouped by task:
- **Lifecycle:** `name`, `analyze`, `analyze_with_options`, `save`, `close`,
`start_transaction`.
- **Function enumeration:** `functions`, `function_at`, `function_containing`,
`function_summary`, `function_summaries`.
- **Listings:** `instructions_for_function`, `instructions_in_range`,
`instruction_at`, `data_at`, `data_containing`, `memory_blocks`.
- **Symbols and references:** `symbols`, `symbols_at`, `find_symbols`,
`references_from`, `references_to`.
- **Per-function summaries:** `callers`, `callees`, `strings`, `constants`,
`data_refs`, `imports`, `exports`, `source_map`, `function_plate_comment`,
`basic_block_count`, `instruction_count`.
- **Graphs:** `control_flow_graph` (one), `control_flow_graphs` (batch),
`call_graph`.
- **Decompilation:** `open_decompiler`, `decompile_function`,
`decompile_function_with_options`. The session form is preferred for
multi-function loops because it amortizes decompiler init cost.
- **Whole-program metadata:** `program.metadata()` returns a `ProgramMetadata`
containing canonical functions, symbols, and the recursive type graph.
`Decompiler::program_metadata()` returns the same shape but is the version
the decompiler observed during this session. Wrap either with
`metadata.type_index()` to follow `type_id` references.
- **Addresses:** `parse_address` returns a typed `Address`; most query methods
take `&Address` rather than `&str`.
## Transactions
Mutations require an explicit transaction. Drop without `commit()` to roll
back:
```rust
let txn = program.start_transaction("annotate entry point")?;
txn.set_function_plate_comment(&functions[0], "reviewed entry point")?;
txn.commit()?; // explicit commit
program.save()?; // persist to disk
// vs.
{
let txn = program.start_transaction("scratch edit")?;
txn.set_function_plate_comment(&functions[0], "tentative")?;
// dropped without commit -> rolled back
}
# Ok::<_, ghidra::Error>(())
```
`commit` and `rollback` consume the transaction; you cannot reuse it.
## Errors
`ghidra::Error` is `#[non_exhaustive]`. The variants you'll encounter:
| variant | meaning |
| ------------------------------- | ------------------------------------------------------------------------ |
| `Runtime { operation, source }` | Bootstrap, classpath, or JVM-start failure. |
| `Jni(_)` | Low-level JNI failure. Usually a programming error in the bridge. |
| `JavaException { class_name, message, stack_trace, .. }` | A Java exception escaped the bridge. Inspect `message` and `stack_trace`. |
| `ClosedHandle { handle_type }` | You used a `Project`/`Program`/`Decompiler`/`Transaction` after `close`. |
| `Json { operation, source }` | Round-trip of bridge data into Rust types failed. Bug if it happens. |
| `Io { operation, path, source }`| Filesystem access (project directory, binary). |
| `InvalidInput { message }` | API contract violation (empty path, malformed address text). |
## Concurrency
Every method blocks on a JNI round-trip. Run from a dedicated blocking thread
if you're inside an async runtime. The JVM is process-global, but a single
`Program` is not internally synchronized; share access through your own mutex
if you need parallel readers.
## Development
```bash
nix develop # or `direnv allow` for auto-loading
just check # type-check
just lint # clippy
just test # unit + non-live integration tests
just test-live # full lifecycle test against a real Ghidra JVM
just doc # rustdoc
```
## License
Apache-2.0. See [LICENSE](LICENSE).