ghidra 0.0.3

Typed Rust bindings for an embedded Ghidra JVM
Documentation

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 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

[dependencies]
ghidra = "0.1"

End-to-end example

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

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 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:

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

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.